blob: aea3207bfc91744d5d552bf58f3a7ad31a352917 [file] [log] [blame]
// Copyright 2020-present the Material Components for iOS authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import "MDCTextControlStyleUnderlined.h"
#import <Foundation/Foundation.h>
#include "MaterialAvailability.h"
#import "MDCTextControlState.h"
#import "MDCTextControl.h"
#import "UIBezierPath+MDCTextControlStyle.h"
#import "MDCTextControlVerticalPositioningReferenceUnderlined.h"
static const CGFloat kUnderlinedContainerStyleUnderlineThicknessNormal = 1.0f;
static const CGFloat kUnderlinedContainerStyleUnderlineThicknessEditing = 2.0f;
static const CGFloat kUnderlinedFloatingLabelScaleFactor = 0.75f;
static const CGFloat kUnderlinedHorizontalEdgePaddingDefault = 2;
@interface MDCTextControlStyleUnderlined () <CAAnimationDelegate>
@property(strong, nonatomic) CAShapeLayer *normalUnderlineLayer;
@property(strong, nonatomic) CAShapeLayer *editingUnderlineLayer;
@property(strong, nonatomic, readonly, class) NSString *editingUnderlineGrowKey;
@property(strong, nonatomic, readonly, class) NSString *editingUnderlineShrinkKey;
@property(strong, nonatomic, readonly, class) NSString *normalUnderlineGrowKey;
@property(strong, nonatomic, readonly, class) NSString *normalUnderlineShrinkKey;
@property(strong, nonatomic, readonly, class) NSString *editingUnderlineThicknessKey;
@property(strong, nonatomic, readonly, class) NSString *normalUnderlineThicknessKey;
@property(strong, nonatomic) NSMutableDictionary<NSNumber *, UIColor *> *underlineColors;
@property(nonatomic, assign) CGRect mostRecentBounds;
@property(nonatomic, assign) BOOL isEditing;
@property(nonatomic, assign) CGFloat containerHeight;
@property(nonatomic, assign) NSTimeInterval animationDuration;
@end
@implementation MDCTextControlStyleUnderlined
#pragma mark Object Lifecycle
- (instancetype)init {
self = [super init];
if (self) {
[self commonMDCTextControlStyleUnderlinedInit];
}
return self;
}
#pragma mark Setup
- (void)commonMDCTextControlStyleUnderlinedInit {
self.mostRecentBounds = CGRectZero;
self.normalUnderlineThickness = kUnderlinedContainerStyleUnderlineThicknessNormal;
self.editingUnderlineThickness = kUnderlinedContainerStyleUnderlineThicknessEditing;
[self setUpUnderlineColors];
[self setUpUnderlineSublayers];
}
- (void)setUpUnderlineColors {
self.underlineColors = [[NSMutableDictionary alloc] init];
UIColor *underlineColor = UIColor.labelColor;
self.underlineColors[@(MDCTextControlStateNormal)] = underlineColor;
self.underlineColors[@(MDCTextControlStateEditing)] = underlineColor;
self.underlineColors[@(MDCTextControlStateDisabled)] = underlineColor;
}
- (void)setUpUnderlineSublayers {
self.normalUnderlineLayer = [[CAShapeLayer alloc] init];
self.editingUnderlineLayer = [[CAShapeLayer alloc] init];
}
#pragma mark Accessors
- (UIColor *)underlineColorForState:(MDCTextControlState)state {
return self.underlineColors[@(state)];
}
- (void)setUnderlineColor:(nonnull UIColor *)underlineColor forState:(MDCTextControlState)state {
self.underlineColors[@(state)] = underlineColor;
}
- (void)setEditingUnderlineThickness:(CGFloat)editingUnderlineThickness {
[self setEditingUnderlineThickness:editingUnderlineThickness animated:NO];
}
- (void)setNormalUnderlineThickness:(CGFloat)normalUnderlineThickness {
[self setNormalUnderlineThickness:normalUnderlineThickness animated:NO];
}
- (void)setEditingUnderlineThickness:(CGFloat)thickness animated:(BOOL)animated {
_editingUnderlineThickness = thickness;
UIBezierPath *targetUnderlineBezier = [self targetEditingUnderlineBezier];
[self.editingUnderlineLayer removeAllAnimations];
if (animated) {
[CATransaction begin];
[CATransaction setAnimationDuration:self.animationDuration];
[self.editingUnderlineLayer addAnimation:[self pathAnimationTo:targetUnderlineBezier]
forKey:self.class.editingUnderlineThicknessKey];
[CATransaction commit];
} else {
self.editingUnderlineLayer.path = targetUnderlineBezier.CGPath;
}
}
- (void)setNormalUnderlineThickness:(CGFloat)thickness animated:(BOOL)animated {
_normalUnderlineThickness = thickness;
UIBezierPath *targetUnderlineBezier = [self targetNormalUnderlineBezier];
[self.normalUnderlineLayer removeAllAnimations];
if (animated) {
[CATransaction begin];
[CATransaction setAnimationDuration:self.animationDuration];
[self.normalUnderlineLayer addAnimation:[self pathAnimationTo:targetUnderlineBezier]
forKey:self.class.normalUnderlineThicknessKey];
[CATransaction commit];
} else {
self.normalUnderlineLayer.path = targetUnderlineBezier.CGPath;
}
}
#pragma mark MDCTextControl
- (void)applyStyleToTextControl:(UIView<MDCTextControl> *)textControl
animationDuration:(NSTimeInterval)animationDuration {
[self applyUnderlineStyleToView:textControl
state:textControl.textControlState
containerFrame:textControl.containerFrame
animationDuration:animationDuration];
}
- (void)removeStyleFrom:(id<MDCTextControl>)textControl {
[self.normalUnderlineLayer removeFromSuperlayer];
[self.editingUnderlineLayer removeFromSuperlayer];
}
- (id<MDCTextControlVerticalPositioningReference>)
positioningReferenceWithFloatingFontLineHeight:(CGFloat)floatingLabelHeight
normalFontLineHeight:(CGFloat)normalFontLineHeight
textRowHeight:(CGFloat)textRowHeight
numberOfTextRows:(CGFloat)numberOfTextRows
density:(CGFloat)density
preferredContainerHeight:(CGFloat)preferredContainerHeight
isMultilineTextControl:(BOOL)isMultilineTextControl {
return [[MDCTextControlVerticalPositioningReferenceUnderlined alloc]
initWithFloatingFontLineHeight:floatingLabelHeight
normalFontLineHeight:normalFontLineHeight
textRowHeight:textRowHeight
numberOfTextRows:numberOfTextRows
density:density
preferredContainerHeight:preferredContainerHeight
isMultilineTextControl:isMultilineTextControl];
}
- (UIFont *)floatingFontWithNormalFont:(UIFont *)font {
CGFloat scaleFactor = kUnderlinedFloatingLabelScaleFactor;
CGFloat floatingFontSize = font.pointSize * scaleFactor;
return [font fontWithSize:floatingFontSize];
}
- (nonnull MDCTextControlHorizontalPositioningReference *)horizontalPositioningReference {
MDCTextControlHorizontalPositioningReference *positioningReference =
[[MDCTextControlHorizontalPositioningReference alloc] init];
positioningReference.leadingEdgePadding = kUnderlinedHorizontalEdgePaddingDefault;
positioningReference.trailingEdgePadding = kUnderlinedHorizontalEdgePaddingDefault;
return positioningReference;
}
#pragma mark Custom Styling
- (void)applyUnderlineStyleToView:(UIView *)view
state:(MDCTextControlState)state
containerFrame:(CGRect)containerFrame
animationDuration:(NSTimeInterval)animationDuration {
BOOL didChangeBounds = NO;
if (!CGRectEqualToRect(self.mostRecentBounds, view.bounds)) {
didChangeBounds = YES;
self.mostRecentBounds = view.bounds;
}
self.containerHeight = CGRectGetMaxY(containerFrame);
self.isEditing = state == MDCTextControlStateEditing;
self.animationDuration = animationDuration;
self.normalUnderlineLayer.fillColor = [self.underlineColors[@(state)] CGColor];
self.editingUnderlineLayer.fillColor = [self.underlineColors[@(state)] CGColor];
BOOL styleIsBeingAppliedToView = NO;
if (self.editingUnderlineLayer.superlayer != view.layer) {
[view.layer insertSublayer:self.editingUnderlineLayer atIndex:0];
styleIsBeingAppliedToView = YES;
}
if (self.normalUnderlineLayer.superlayer != view.layer) {
[view.layer insertSublayer:self.normalUnderlineLayer atIndex:0];
styleIsBeingAppliedToView = YES;
}
UIBezierPath *targetEditingUnderlineBezier = [self targetEditingUnderlineBezier];
UIBezierPath *targetNormalUnderlineBezier = [self targetNormalUnderlineBezier];
if (animationDuration <= 0 || styleIsBeingAppliedToView || didChangeBounds) {
[self.normalUnderlineLayer removeAllAnimations];
[self.editingUnderlineLayer removeAllAnimations];
self.normalUnderlineLayer.path = targetNormalUnderlineBezier.CGPath;
self.editingUnderlineLayer.path = targetEditingUnderlineBezier.CGPath;
return;
}
CABasicAnimation *preexistingEditingUnderlineShrinkAnimation =
(CABasicAnimation *)[self.editingUnderlineLayer
animationForKey:self.class.editingUnderlineShrinkKey];
CABasicAnimation *preexistingEditingUnderlineGrowAnimation =
(CABasicAnimation *)[self.editingUnderlineLayer
animationForKey:self.class.editingUnderlineGrowKey];
CABasicAnimation *preexistingNormalUnderlineGrowAnimation =
(CABasicAnimation *)[self.normalUnderlineLayer
animationForKey:self.class.normalUnderlineGrowKey];
CABasicAnimation *preexistingNormalUnderlineShrinkAnimation =
(CABasicAnimation *)[self.normalUnderlineLayer
animationForKey:self.class.normalUnderlineShrinkKey];
[CATransaction begin];
{
[CATransaction setAnimationDuration:animationDuration];
if (self.isEditing) {
if (preexistingEditingUnderlineShrinkAnimation) {
[self.editingUnderlineLayer removeAnimationForKey:self.class.editingUnderlineShrinkKey];
}
BOOL needsEditingUnderlineGrowAnimation = NO;
if (preexistingEditingUnderlineGrowAnimation) {
CGPathRef toValue = (__bridge CGPathRef)preexistingEditingUnderlineGrowAnimation.toValue;
if (!CGPathEqualToPath(toValue, targetEditingUnderlineBezier.CGPath)) {
[self.editingUnderlineLayer removeAnimationForKey:self.class.editingUnderlineGrowKey];
needsEditingUnderlineGrowAnimation = YES;
self.editingUnderlineLayer.path = targetEditingUnderlineBezier.CGPath;
}
} else {
needsEditingUnderlineGrowAnimation = YES;
}
if (needsEditingUnderlineGrowAnimation) {
[self.editingUnderlineLayer addAnimation:[self pathAnimationTo:targetEditingUnderlineBezier]
forKey:self.class.editingUnderlineGrowKey];
}
if (preexistingNormalUnderlineGrowAnimation) {
[self.normalUnderlineLayer removeAnimationForKey:self.class.normalUnderlineGrowKey];
}
BOOL needsNormalUnderlineShrinkAnimation = NO;
if (preexistingNormalUnderlineShrinkAnimation) {
CGPathRef toValue = (__bridge CGPathRef)preexistingNormalUnderlineShrinkAnimation.toValue;
if (!CGPathEqualToPath(toValue, targetNormalUnderlineBezier.CGPath)) {
[self.normalUnderlineLayer removeAnimationForKey:self.class.normalUnderlineShrinkKey];
needsNormalUnderlineShrinkAnimation = YES;
self.normalUnderlineLayer.path = targetNormalUnderlineBezier.CGPath;
}
} else {
needsNormalUnderlineShrinkAnimation = YES;
}
if (needsNormalUnderlineShrinkAnimation) {
[self.normalUnderlineLayer addAnimation:[self pathAnimationTo:targetNormalUnderlineBezier]
forKey:self.class.normalUnderlineShrinkKey];
}
} else {
if (preexistingEditingUnderlineGrowAnimation) {
[self.editingUnderlineLayer removeAnimationForKey:self.class.editingUnderlineGrowKey];
}
BOOL needsEditingUnderlineShrinkAnimation = NO;
if (preexistingEditingUnderlineShrinkAnimation) {
CGPathRef toValue = (__bridge CGPathRef)preexistingEditingUnderlineShrinkAnimation.toValue;
if (!CGPathEqualToPath(toValue, targetEditingUnderlineBezier.CGPath)) {
[self.editingUnderlineLayer removeAnimationForKey:self.class.editingUnderlineShrinkKey];
needsEditingUnderlineShrinkAnimation = YES;
self.editingUnderlineLayer.path = targetEditingUnderlineBezier.CGPath;
}
} else {
needsEditingUnderlineShrinkAnimation = YES;
}
if (needsEditingUnderlineShrinkAnimation) {
[self.editingUnderlineLayer addAnimation:[self pathAnimationTo:targetEditingUnderlineBezier]
forKey:self.class.editingUnderlineShrinkKey];
}
if (preexistingNormalUnderlineShrinkAnimation) {
[self.normalUnderlineLayer removeAnimationForKey:self.class.normalUnderlineShrinkKey];
}
BOOL needsEditingUnderlineGrowAnimation = NO;
if (preexistingNormalUnderlineGrowAnimation) {
CGPathRef toValue = (__bridge CGPathRef)preexistingNormalUnderlineGrowAnimation.toValue;
if (!CGPathEqualToPath(toValue, targetNormalUnderlineBezier.CGPath)) {
[self.normalUnderlineLayer removeAnimationForKey:self.class.normalUnderlineGrowKey];
needsEditingUnderlineGrowAnimation = YES;
self.normalUnderlineLayer.path = targetNormalUnderlineBezier.CGPath;
}
} else {
needsEditingUnderlineGrowAnimation = YES;
}
if (needsEditingUnderlineGrowAnimation) {
[self.normalUnderlineLayer addAnimation:[self pathAnimationTo:targetNormalUnderlineBezier]
forKey:self.class.normalUnderlineGrowKey];
}
}
}
[CATransaction commit];
}
#pragma mark Animation
- (CABasicAnimation *)pathAnimationTo:(UIBezierPath *)path {
CABasicAnimation *animation = [self basicAnimationWithKeyPath:@"path"];
animation.toValue = (id)(path.CGPath);
return animation;
}
- (CABasicAnimation *)basicAnimationWithKeyPath:(NSString *)keyPath {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:keyPath];
animation.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.repeatCount = 0;
animation.removedOnCompletion = NO;
animation.delegate = self;
animation.fillMode = kCAFillModeForwards;
return animation;
}
- (void)animationDidStart:(CAAnimation *)anim {
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
if (![anim isKindOfClass:[CABasicAnimation class]]) {
return;
}
CABasicAnimation *animation = (CABasicAnimation *)anim;
CGPathRef toValue = (__bridge CGPathRef)animation.toValue;
CABasicAnimation *thickGrowAnimation = (CABasicAnimation *)[self.editingUnderlineLayer
animationForKey:self.class.editingUnderlineGrowKey];
CABasicAnimation *thickShrinkAnimation = (CABasicAnimation *)[self.editingUnderlineLayer
animationForKey:self.class.editingUnderlineShrinkKey];
CABasicAnimation *thinGrowAnimation = (CABasicAnimation *)[self.normalUnderlineLayer
animationForKey:self.class.normalUnderlineGrowKey];
CABasicAnimation *thinShrinkAnimation = (CABasicAnimation *)[self.normalUnderlineLayer
animationForKey:self.class.normalUnderlineShrinkKey];
CABasicAnimation *editingUnderlineAnimation = (CABasicAnimation *)[self.editingUnderlineLayer
animationForKey:self.class.editingUnderlineThicknessKey];
CABasicAnimation *normalUnderlineAnimation = (CABasicAnimation *)[self.normalUnderlineLayer
animationForKey:self.class.normalUnderlineThicknessKey];
if (flag) {
if ((animation == thickGrowAnimation) || (animation == thickShrinkAnimation)) {
self.editingUnderlineLayer.path = toValue;
}
if ((animation == thinGrowAnimation) || (animation == thinShrinkAnimation)) {
self.normalUnderlineLayer.path = toValue;
}
if (animation == editingUnderlineAnimation) {
self.editingUnderlineLayer.path = toValue;
}
if (animation == normalUnderlineAnimation) {
self.normalUnderlineLayer.path = toValue;
}
}
}
+ (NSString *)normalUnderlineShrinkKey {
return @"normalUnderlineShrinkKey";
}
+ (NSString *)normalUnderlineGrowKey {
return @"normalUnderlineGrowKey";
}
+ (NSString *)editingUnderlineShrinkKey {
return @"editingUnderlineShrinkKey";
}
+ (NSString *)editingUnderlineGrowKey {
return @"editingUnderlineGrowKey";
}
+ (NSString *)normalUnderlineThicknessKey {
return @"normalUnderlineThicknessKey";
}
+ (NSString *)editingUnderlineThicknessKey {
return @"editingUnderlineThicknessKey";
}
#pragma mark Path Drawing
- (UIBezierPath *)targetEditingUnderlineBezier {
CGFloat editingUnderlineWidth = self.isEditing ? CGRectGetWidth(self.mostRecentBounds) : 0;
CGFloat editingUnderlineThickness = self.isEditing ? self.editingUnderlineThickness : 0;
UIBezierPath *targetEditingUnderlineBezier =
[self underlinePathWithViewBounds:self.mostRecentBounds
containerHeight:self.containerHeight
underlineThickness:editingUnderlineThickness
underlineWidth:editingUnderlineWidth];
return targetEditingUnderlineBezier;
}
- (UIBezierPath *)targetNormalUnderlineBezier {
CGFloat normalUnderlineThickness = self.isEditing ? 0 : self.normalUnderlineThickness;
UIBezierPath *targetNormalUnderlineBezier =
[self underlinePathWithViewBounds:self.mostRecentBounds
containerHeight:self.containerHeight
underlineThickness:normalUnderlineThickness
underlineWidth:CGRectGetWidth(self.mostRecentBounds)];
return targetNormalUnderlineBezier;
}
- (UIBezierPath *)underlinePathWithViewBounds:(CGRect)viewBounds
containerHeight:(CGFloat)containerHeight
underlineThickness:(CGFloat)underlineThickness
underlineWidth:(CGFloat)underlineWidth {
UIBezierPath *path = [[UIBezierPath alloc] init];
CGFloat viewWidth = CGRectGetWidth(viewBounds);
CGFloat halfViewWidth = 0.5f * viewWidth;
CGFloat halfUnderlineWidth = underlineWidth * 0.5f;
CGFloat sublayerMinX = halfViewWidth - halfUnderlineWidth;
CGFloat sublayerMaxX = sublayerMinX + underlineWidth;
CGFloat sublayerMaxY = containerHeight;
CGFloat sublayerMinY = sublayerMaxY - underlineThickness;
CGPoint startingPoint = CGPointMake(sublayerMinX, sublayerMinY);
CGPoint topRightCornerPoint1 = CGPointMake(sublayerMaxX, sublayerMinY);
[path moveToPoint:startingPoint];
[path addLineToPoint:topRightCornerPoint1];
CGPoint topRightCornerPoint2 = CGPointMake(sublayerMaxX, sublayerMinY);
[path mdc_addTopRightCornerFromPoint:topRightCornerPoint1
toPoint:topRightCornerPoint2
withRadius:0];
CGPoint bottomRightCornerPoint1 = CGPointMake(sublayerMaxX, sublayerMaxY);
CGPoint bottomRightCornerPoint2 = CGPointMake(sublayerMaxX, sublayerMaxY);
[path addLineToPoint:bottomRightCornerPoint1];
[path mdc_addBottomRightCornerFromPoint:bottomRightCornerPoint1
toPoint:bottomRightCornerPoint2
withRadius:0];
CGPoint bottomLeftCornerPoint1 = CGPointMake(sublayerMinX, sublayerMaxY);
CGPoint bottomLeftCornerPoint2 = CGPointMake(sublayerMinX, sublayerMaxY);
[path addLineToPoint:bottomLeftCornerPoint1];
[path mdc_addBottomLeftCornerFromPoint:bottomLeftCornerPoint1
toPoint:bottomLeftCornerPoint2
withRadius:0];
CGPoint topLeftCornerPoint1 = CGPointMake(sublayerMinX, sublayerMinY);
CGPoint topLeftCornerPoint2 = CGPointMake(sublayerMinX, sublayerMinY);
[path addLineToPoint:topLeftCornerPoint1];
[path mdc_addTopLeftCornerFromPoint:topLeftCornerPoint1 toPoint:topLeftCornerPoint2 withRadius:0];
return path;
}
@end