| // Copyright 2017-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 "MDCLegacyInkLayer.h" |
| #import "MDCLegacyInkLayer+Private.h" |
| |
| #import <UIKit/UIKit.h> |
| |
| static inline CGPoint MDCLegacyInkLayerInterpolatePoint(CGPoint start, |
| CGPoint end, |
| CGFloat offsetPercent) { |
| CGPoint centerOffsetPoint = CGPointMake(start.x + (end.x - start.x) * offsetPercent, |
| start.y + (end.y - start.y) * offsetPercent); |
| return centerOffsetPoint; |
| } |
| |
| static inline CGFloat MDCLegacyInkLayerRadiusBounds(CGFloat maxRippleRadius, |
| CGFloat inkLayerRectHypotenuse, |
| __unused BOOL bounded) { |
| if (maxRippleRadius > 0) { |
| #ifdef MDC_BOUNDED_INK_IGNORES_MAX_RIPPLE_RADIUS |
| if (!bounded) { |
| return maxRippleRadius; |
| } else { |
| static dispatch_once_t onceToken; |
| dispatch_once(&onceToken, ^{ |
| NSLog(@"Implementation of MDCInkView with |MDCInkStyle| MDCInkStyleBounded and " |
| @"maxRippleRadius has changed.\n\n" |
| @"MDCInkStyleBounded ignores maxRippleRadius. " |
| @"Please use |MDCInkStyle| MDCInkStyleUnbounded to continue using maxRippleRadius."); |
| }); |
| return inkLayerRectHypotenuse; |
| } |
| #else |
| return maxRippleRadius; |
| #endif |
| } else { |
| return inkLayerRectHypotenuse; |
| } |
| } |
| |
| static inline CGFloat MDCLegacyInkLayerRandom() { |
| const uint32_t max_value = 10000; |
| return (CGFloat)arc4random_uniform(max_value + 1) / max_value; |
| } |
| |
| static inline CGPoint MDCLegacyInkLayerRectGetCenter(CGRect rect) { |
| return CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect)); |
| } |
| |
| static inline CGFloat MDCLegacyInkLayerRectHypotenuse(CGRect rect) { |
| return (CGFloat)hypot(CGRectGetWidth(rect), CGRectGetHeight(rect)); |
| } |
| |
| static NSString *const kInkLayerOpacity = @"opacity"; |
| static NSString *const kInkLayerPosition = @"position"; |
| static NSString *const kInkLayerScale = @"transform.scale"; |
| |
| // State tracking for ink. |
| typedef NS_ENUM(NSInteger, MDCInkRippleState) { |
| kInkRippleNone, |
| kInkRippleSpreading, |
| kInkRippleComplete, |
| kInkRippleCancelled, |
| }; |
| |
| #if defined(__IPHONE_10_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0) |
| @interface MDCLegacyInkLayerRipple () <CAAnimationDelegate> |
| @end |
| #endif |
| |
| @interface MDCLegacyInkLayerRipple () |
| |
| @property(nonatomic, assign, getter=isAnimationCleared) BOOL animationCleared; |
| @property(nonatomic, weak) id<MDCLegacyInkLayerRippleDelegate> animationDelegate; |
| @property(nonatomic, assign) BOOL bounded; |
| @property(nonatomic, weak) CALayer *inkLayer; |
| @property(nonatomic, assign) CGFloat radius; |
| @property(nonatomic, assign) CGPoint point; |
| @property(nonatomic, assign) CGRect targetFrame; |
| @property(nonatomic, assign) MDCInkRippleState rippleState; |
| @property(nonatomic, strong) UIColor *color; |
| @end |
| |
| @implementation MDCLegacyInkLayerRipple |
| |
| - (instancetype)init { |
| self = [super init]; |
| if (self) { |
| _rippleState = kInkRippleNone; |
| _animationCleared = YES; |
| } |
| return self; |
| } |
| |
| - (void)setupRipple { |
| self.fillColor = self.color.CGColor; |
| CGFloat dim = self.radius * 2; |
| self.frame = CGRectMake(0, 0, dim, dim); |
| UIBezierPath *ripplePath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, dim, dim)]; |
| self.path = ripplePath.CGPath; |
| } |
| |
| - (void)enter { |
| [self.animationDelegate animationDidStart:self]; |
| |
| _rippleState = kInkRippleSpreading; |
| [_inkLayer addSublayer:self]; |
| _animationCleared = NO; |
| } |
| |
| - (void)exit { |
| if (_rippleState != kInkRippleCancelled) { |
| _rippleState = kInkRippleComplete; |
| } |
| } |
| |
| - (CAKeyframeAnimation *)opacityAnimWithValues:(NSArray<NSNumber *> *)values |
| times:(NSArray<NSNumber *> *)times { |
| CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:kInkLayerOpacity]; |
| anim.fillMode = kCAFillModeForwards; |
| anim.keyTimes = times; |
| anim.removedOnCompletion = NO; |
| anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; |
| anim.values = values; |
| return anim; |
| } |
| |
| - (CAKeyframeAnimation *)positionAnimWithPath:(CGPathRef)path |
| duration:(CGFloat)duration |
| timingFunction:(CAMediaTimingFunction *)timingFunction { |
| CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:kInkLayerPosition]; |
| anim.duration = duration; |
| anim.fillMode = kCAFillModeForwards; |
| anim.path = path; |
| anim.removedOnCompletion = NO; |
| anim.timingFunction = timingFunction; |
| return anim; |
| } |
| |
| - (CAKeyframeAnimation *)scaleAnimWithValues:(NSArray<NSNumber *> *)values |
| times:(NSArray<NSNumber *> *)times { |
| CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:kInkLayerScale]; |
| anim.fillMode = kCAFillModeForwards; |
| anim.keyTimes = times; |
| anim.removedOnCompletion = NO; |
| anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; |
| anim.values = values; |
| return anim; |
| } |
| |
| - (CAMediaTimingFunction *)logDecelerateEasing { |
| // This bezier curve is an approximation of a log curve. |
| return |
| [[CAMediaTimingFunction alloc] initWithControlPoints:(float) |
| 0.157:(float)0.72:(float)0.386:(float)0.987]; |
| } |
| |
| - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)finished { |
| if (!self.isAnimationCleared) { |
| [self.animationDelegate animationDidStop:anim shapeLayer:self finished:finished]; |
| } |
| } |
| |
| @end |
| |
| static CGFloat const kInkLayerForegroundBoundedOpacityExitDuration = (CGFloat)0.4; |
| static CGFloat const kInkLayerForegroundBoundedPositionExitDuration = (CGFloat)0.3; |
| static CGFloat const kInkLayerForegroundBoundedRadiusExitDuration = (CGFloat)0.8; |
| static CGFloat const kInkLayerForegroundRadiusGrowthMultiplier = 350; |
| static CGFloat const kInkLayerForegroundUnboundedEnterDelay = (CGFloat)0.08; |
| static CGFloat const kInkLayerForegroundUnboundedOpacityEnterDuration = (CGFloat)0.12; |
| static CGFloat const kInkLayerForegroundWaveTouchDownAcceleration = 1024; |
| static CGFloat const kInkLayerForegroundWaveTouchUpAcceleration = 3400; |
| static NSString *const kInkLayerForegroundOpacityAnim = @"foregroundOpacityAnim"; |
| static NSString *const kInkLayerForegroundPositionAnim = @"foregroundPositionAnim"; |
| static NSString *const kInkLayerForegroundScaleAnim = @"foregroundScaleAnim"; |
| |
| @interface MDCLegacyInkLayerForegroundRipple () |
| @property(nonatomic, assign) BOOL useCustomInkCenter; |
| @property(nonatomic, assign) CGPoint customInkCenter; |
| @property(nonatomic, strong) CAKeyframeAnimation *foregroundOpacityAnim; |
| @property(nonatomic, strong) CAKeyframeAnimation *foregroundPositionAnim; |
| @property(nonatomic, strong) CAKeyframeAnimation *foregroundScaleAnim; |
| @end |
| |
| @implementation MDCLegacyInkLayerForegroundRipple |
| |
| - (void)setupRipple { |
| CGFloat random = MDCLegacyInkLayerRandom(); |
| self.radius = |
| (CGFloat)((CGFloat)0.9 + random * (CGFloat)0.1) * kInkLayerForegroundRadiusGrowthMultiplier; |
| [super setupRipple]; |
| } |
| |
| - (void)enterWithCompletion:(void (^)(void))completionBlock { |
| [super enter]; |
| |
| if (self.bounded) { |
| _foregroundOpacityAnim = [self opacityAnimWithValues:@[ @0 ] times:@[ @0 ]]; |
| _foregroundScaleAnim = [self scaleAnimWithValues:@[ @0 ] times:@[ @0 ]]; |
| } else { |
| _foregroundOpacityAnim = [self opacityAnimWithValues:@[ @0, @1 ] times:@[ @0, @1 ]]; |
| _foregroundOpacityAnim.duration = kInkLayerForegroundUnboundedOpacityEnterDuration; |
| |
| CGFloat duration = (CGFloat)sqrt(self.radius / kInkLayerForegroundWaveTouchDownAcceleration); |
| _foregroundScaleAnim = |
| [self scaleAnimWithValues:@[ @0, @1 ] |
| times:@[ @(kInkLayerForegroundUnboundedEnterDelay), @1 ]]; |
| _foregroundScaleAnim.duration = duration; |
| |
| CGFloat xOffset = self.targetFrame.origin.x - self.inkLayer.frame.origin.x; |
| CGFloat yOffset = self.targetFrame.origin.y - self.inkLayer.frame.origin.y; |
| |
| UIBezierPath *movePath = [UIBezierPath bezierPath]; |
| CGPoint startPoint = CGPointMake(self.point.x + xOffset, self.point.y + yOffset); |
| CGPoint endPoint = MDCLegacyInkLayerRectGetCenter(self.targetFrame); |
| if (self.useCustomInkCenter) { |
| endPoint = self.customInkCenter; |
| } |
| endPoint = CGPointMake(endPoint.x + xOffset, endPoint.y + yOffset); |
| [movePath moveToPoint:startPoint]; |
| [movePath addLineToPoint:endPoint]; |
| |
| CAMediaTimingFunction *linearTimingFunction = |
| [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; |
| _foregroundPositionAnim = [self positionAnimWithPath:movePath.CGPath |
| duration:duration |
| timingFunction:linearTimingFunction]; |
| _foregroundPositionAnim.keyTimes = @[ @(kInkLayerForegroundUnboundedEnterDelay), @1 ]; |
| [self addAnimation:_foregroundPositionAnim forKey:kInkLayerForegroundPositionAnim]; |
| } |
| |
| [CATransaction begin]; |
| if (completionBlock) { |
| __weak MDCLegacyInkLayerForegroundRipple *weakSelf = self; |
| [CATransaction setCompletionBlock:^{ |
| MDCLegacyInkLayerForegroundRipple *strongSelf = weakSelf; |
| if (strongSelf.rippleState != kInkRippleCancelled) { |
| completionBlock(); |
| } |
| }]; |
| } |
| [self addAnimation:_foregroundOpacityAnim forKey:kInkLayerForegroundOpacityAnim]; |
| [self addAnimation:_foregroundScaleAnim forKey:kInkLayerForegroundScaleAnim]; |
| [CATransaction commit]; |
| } |
| |
| - (void)exit:(BOOL)animated { |
| self.rippleState = kInkRippleCancelled; |
| [self exit:animated completion:nil]; |
| } |
| |
| - (void)exit:(BOOL)animated completion:(void (^)(void))completionBlock { |
| [super exit]; |
| |
| if (!animated) { |
| [self removeAllAnimations]; |
| [CATransaction begin]; |
| [CATransaction setDisableActions:YES]; |
| self.opacity = 0; |
| __weak MDCLegacyInkLayerForegroundRipple *weakSelf = self; |
| [CATransaction setCompletionBlock:^(void) { |
| MDCLegacyInkLayerForegroundRipple *strongSelf = weakSelf; |
| [strongSelf removeFromSuperlayer]; |
| [strongSelf.animationDelegate animationDidStop:nil shapeLayer:strongSelf finished:YES]; |
| }]; |
| [CATransaction commit]; |
| return; |
| } |
| |
| if (self.bounded) { |
| _foregroundOpacityAnim.values = @[ @1, @0 ]; |
| _foregroundOpacityAnim.duration = kInkLayerForegroundBoundedOpacityExitDuration; |
| |
| // Bounded ripples move slightly towards the center of the tap target. Unbounded ripples |
| // move to the center of the tap target. |
| |
| CGFloat xOffset = self.targetFrame.origin.x - self.inkLayer.frame.origin.x; |
| CGFloat yOffset = self.targetFrame.origin.y - self.inkLayer.frame.origin.y; |
| |
| CGPoint startPoint = CGPointMake(self.point.x + xOffset, self.point.y + yOffset); |
| CGPoint endPoint = MDCLegacyInkLayerRectGetCenter(self.targetFrame); |
| if (self.useCustomInkCenter) { |
| endPoint = self.customInkCenter; |
| } |
| endPoint = CGPointMake(endPoint.x + xOffset, endPoint.y + yOffset); |
| CGPoint centerOffsetPoint = |
| MDCLegacyInkLayerInterpolatePoint(startPoint, endPoint, (CGFloat)0.3); |
| UIBezierPath *movePath = [UIBezierPath bezierPath]; |
| [movePath moveToPoint:startPoint]; |
| [movePath addLineToPoint:centerOffsetPoint]; |
| |
| _foregroundPositionAnim = |
| [self positionAnimWithPath:movePath.CGPath |
| duration:kInkLayerForegroundBoundedPositionExitDuration |
| timingFunction:[self logDecelerateEasing]]; |
| _foregroundScaleAnim.values = @[ @0, @1 ]; |
| _foregroundScaleAnim.keyTimes = @[ @0, @1 ]; |
| _foregroundScaleAnim.duration = kInkLayerForegroundBoundedRadiusExitDuration; |
| } else { |
| NSNumber *opacityVal = [self.presentationLayer valueForKeyPath:kInkLayerOpacity]; |
| if (opacityVal == nil) { |
| opacityVal = [NSNumber numberWithFloat:0]; |
| } |
| CGFloat adjustedDuration = kInkLayerForegroundBoundedPositionExitDuration; |
| CGFloat normOpacityVal = opacityVal.floatValue; |
| CGFloat opacityDuration = normOpacityVal / 3; |
| _foregroundOpacityAnim.values = @[ opacityVal, @0 ]; |
| _foregroundOpacityAnim.duration = opacityDuration + adjustedDuration; |
| |
| NSNumber *scaleVal = [self.presentationLayer valueForKeyPath:kInkLayerScale]; |
| if (scaleVal == nil) { |
| scaleVal = [NSNumber numberWithFloat:0]; |
| } |
| CGFloat unboundedDuration = (CGFloat)sqrt(((1 - scaleVal.floatValue) * self.radius) / |
| (kInkLayerForegroundWaveTouchDownAcceleration + |
| kInkLayerForegroundWaveTouchUpAcceleration)); |
| _foregroundPositionAnim.duration = unboundedDuration + adjustedDuration; |
| _foregroundScaleAnim.values = @[ scaleVal, @1 ]; |
| _foregroundScaleAnim.duration = unboundedDuration + adjustedDuration; |
| } |
| |
| _foregroundOpacityAnim.keyTimes = @[ @0, @1 ]; |
| if (_foregroundOpacityAnim.duration < _foregroundScaleAnim.duration) { |
| _foregroundScaleAnim.delegate = self; |
| } else { |
| _foregroundOpacityAnim.delegate = self; |
| } |
| |
| _foregroundOpacityAnim.timingFunction = |
| [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; |
| _foregroundPositionAnim.timingFunction = [self logDecelerateEasing]; |
| _foregroundScaleAnim.timingFunction = [self logDecelerateEasing]; |
| |
| [CATransaction begin]; |
| if (completionBlock) { |
| __weak MDCLegacyInkLayerForegroundRipple *weakSelf = self; |
| [CATransaction setCompletionBlock:^{ |
| MDCLegacyInkLayerForegroundRipple *strongSelf = weakSelf; |
| if (strongSelf.rippleState != kInkRippleCancelled) { |
| completionBlock(); |
| } |
| }]; |
| } |
| [self addAnimation:_foregroundOpacityAnim forKey:kInkLayerForegroundOpacityAnim]; |
| [self addAnimation:_foregroundPositionAnim forKey:kInkLayerForegroundPositionAnim]; |
| [self addAnimation:_foregroundScaleAnim forKey:kInkLayerForegroundScaleAnim]; |
| [CATransaction commit]; |
| } |
| |
| - (void)removeAllAnimations { |
| [super removeAllAnimations]; |
| _foregroundOpacityAnim = nil; |
| _foregroundPositionAnim = nil; |
| _foregroundScaleAnim = nil; |
| self.animationCleared = YES; |
| } |
| |
| @end |
| |
| static CGFloat const kInkLayerBackgroundOpacityEnterDuration = (CGFloat)0.6; |
| static CGFloat const kInkLayerBackgroundBaseOpacityExitDuration = (CGFloat)0.48; |
| static CGFloat const kInkLayerBackgroundFastEnterDuration = (CGFloat)0.12; |
| static NSString *const kInkLayerBackgroundOpacityAnim = @"backgroundOpacityAnim"; |
| |
| @interface MDCLegacyInkLayerBackgroundRipple () |
| @property(nonatomic, strong, nullable) CAKeyframeAnimation *backgroundOpacityAnim; |
| @end |
| |
| @implementation MDCLegacyInkLayerBackgroundRipple |
| |
| - (void)enter { |
| [super enter]; |
| _backgroundOpacityAnim = [self opacityAnimWithValues:@[ @0, @1 ] times:@[ @0, @1 ]]; |
| _backgroundOpacityAnim.duration = kInkLayerBackgroundOpacityEnterDuration; |
| [self addAnimation:_backgroundOpacityAnim forKey:kInkLayerBackgroundOpacityAnim]; |
| } |
| |
| - (void)exit:(BOOL)animated { |
| [super exit]; |
| |
| if (!animated) { |
| [self removeAllAnimations]; |
| [CATransaction begin]; |
| [CATransaction setDisableActions:YES]; |
| self.opacity = 0; |
| __weak MDCLegacyInkLayerBackgroundRipple *weakSelf = self; |
| [CATransaction setCompletionBlock:^(void) { |
| MDCLegacyInkLayerBackgroundRipple *strongSelf = weakSelf; |
| [strongSelf removeFromSuperlayer]; |
| [strongSelf.animationDelegate animationDidStop:nil shapeLayer:strongSelf finished:YES]; |
| }]; |
| [CATransaction commit]; |
| return; |
| } |
| |
| NSNumber *opacityVal = [self.presentationLayer valueForKeyPath:kInkLayerOpacity]; |
| if (opacityVal == nil) { |
| opacityVal = [NSNumber numberWithFloat:0]; |
| } |
| CGFloat duration = kInkLayerBackgroundBaseOpacityExitDuration; |
| if (self.bounded) { |
| // The end (tap release) animation should continue at the opacity level of the start animation. |
| CGFloat enterDuration = (1 - opacityVal.floatValue / 1) * kInkLayerBackgroundFastEnterDuration; |
| duration += enterDuration; |
| _backgroundOpacityAnim = [self opacityAnimWithValues:@[ opacityVal, @1, @0 ] |
| times:@[ @0, @(enterDuration / duration), @1 ]]; |
| } else { |
| _backgroundOpacityAnim = [self opacityAnimWithValues:@[ opacityVal, @0 ] times:@[ @0, @1 ]]; |
| } |
| _backgroundOpacityAnim.duration = duration; |
| _backgroundOpacityAnim.delegate = self; |
| [self addAnimation:_backgroundOpacityAnim forKey:kInkLayerBackgroundOpacityAnim]; |
| } |
| |
| - (void)removeAllAnimations { |
| [super removeAllAnimations]; |
| _backgroundOpacityAnim = nil; |
| self.animationCleared = YES; |
| } |
| |
| @end |
| |
| @interface MDCLegacyInkLayer () |
| |
| /** |
| Reset the bottom-most ink applied to the layer with a completion handler to be called on completion |
| if applicable. |
| |
| @param animated Enables the ink ripple fade out animation. |
| @param completionBlock Block called after the completion of the animation. |
| */ |
| - (void)resetBottomInk:(BOOL)animated completion:(void (^)(void))completionBlock; |
| |
| /** |
| Reset the bottom-most ink applied to the layer with a completion handler to be called on completion |
| if applicable. |
| |
| @param animated Enables the ink ripple fade out animation. |
| @param point Evaporate the ink towards the point. |
| @param completionBlock Block called after the completion of the animation. |
| */ |
| - (void)resetBottomInk:(BOOL)animated |
| toPoint:(CGPoint)point |
| completion:(void (^)(void))completionBlock; |
| |
| @property(nonatomic, strong, nonnull) CAShapeLayer *compositeRipple; |
| @property(nonatomic, strong, nonnull) |
| NSMutableArray<MDCLegacyInkLayerForegroundRipple *> *foregroundRipples; |
| @property(nonatomic, strong, nonnull) |
| NSMutableArray<MDCLegacyInkLayerBackgroundRipple *> *backgroundRipples; |
| |
| @end |
| |
| @implementation MDCLegacyInkLayer |
| |
| - (instancetype)init { |
| self = [super init]; |
| if (self) { |
| self.masksToBounds = YES; |
| [self commonMDCLegacyInkLayerInit]; |
| _animating = NO; |
| } |
| return self; |
| } |
| |
| - (instancetype)initWithCoder:(NSCoder *)aDecoder { |
| self = [super initWithCoder:aDecoder]; |
| |
| if (self) { |
| _animating = NO; |
| // Discard any sublayers, which should be the composite ripple and any active ripples |
| if (self.sublayers.count > 0) { |
| NSArray<CALayer *> *sublayers = [self.sublayers copy]; |
| for (CALayer *sublayer in sublayers) { |
| [sublayer removeFromSuperlayer]; |
| } |
| } |
| [self commonMDCLegacyInkLayerInit]; |
| } |
| |
| return self; |
| } |
| |
| - (void)commonMDCLegacyInkLayerInit { |
| static UIColor *defaultInkColor; |
| static dispatch_once_t onceToken; |
| dispatch_once(&onceToken, ^{ |
| defaultInkColor = [UIColor colorWithWhite:0 alpha:(CGFloat)0.08]; |
| }); |
| |
| _bounded = YES; |
| _inkColor = defaultInkColor; |
| _compositeRipple = [CAShapeLayer layer]; |
| _foregroundRipples = [NSMutableArray array]; |
| _backgroundRipples = [NSMutableArray array]; |
| [self addSublayer:_compositeRipple]; |
| } |
| |
| - (void)layoutSublayers { |
| [super layoutSublayers]; |
| CGFloat radius = MDCLegacyInkLayerRadiusBounds( |
| _maxRippleRadius, MDCLegacyInkLayerRectHypotenuse(self.bounds) / 2, _bounded); |
| |
| CGRect rippleFrame = |
| CGRectMake(-(radius * 2 - self.bounds.size.width) / 2, |
| -(radius * 2 - self.bounds.size.height) / 2, radius * 2, radius * 2); |
| _compositeRipple.frame = rippleFrame; |
| CGRect rippleBounds = CGRectMake(0, 0, radius * 2, radius * 2); |
| CAShapeLayer *rippleMaskLayer = [CAShapeLayer layer]; |
| UIBezierPath *ripplePath = [UIBezierPath bezierPathWithOvalInRect:rippleBounds]; |
| rippleMaskLayer.path = ripplePath.CGPath; |
| _compositeRipple.mask = rippleMaskLayer; |
| } |
| |
| - (void)enterAllInks { |
| for (MDCLegacyInkLayerForegroundRipple *foregroundRipple in self.foregroundRipples) { |
| [foregroundRipple enterWithCompletion:nil]; |
| } |
| for (MDCLegacyInkLayerBackgroundRipple *backgroundRipple in self.backgroundRipples) { |
| [backgroundRipple enter]; |
| } |
| } |
| |
| - (void)resetAllInk:(BOOL)animated { |
| for (MDCLegacyInkLayerForegroundRipple *foregroundRipple in self.foregroundRipples) { |
| [foregroundRipple exit:animated]; |
| } |
| for (MDCLegacyInkLayerBackgroundRipple *backgroundRipple in self.backgroundRipples) { |
| [backgroundRipple exit:animated]; |
| } |
| } |
| |
| - (void)resetBottomInk:(BOOL)animated completion:(void (^)(void))completionBlock { |
| if (self.foregroundRipples.count > 0) { |
| [[self.foregroundRipples objectAtIndex:(self.foregroundRipples.count - 1)] |
| exit:animated |
| completion:completionBlock]; |
| } |
| if (self.backgroundRipples.count > 0) { |
| [[self.backgroundRipples objectAtIndex:(self.backgroundRipples.count - 1)] exit:animated]; |
| } |
| } |
| |
| - (void)resetBottomInk:(BOOL)animated |
| toPoint:(CGPoint)point |
| completion:(void (^)(void))completionBlock { |
| if (self.foregroundRipples.count > 0) { |
| MDCLegacyInkLayerForegroundRipple *foregroundRipple = |
| [self.foregroundRipples objectAtIndex:(self.foregroundRipples.count - 1)]; |
| foregroundRipple.point = point; |
| [foregroundRipple exit:animated completion:completionBlock]; |
| } |
| if (self.backgroundRipples.count > 0) { |
| [[self.backgroundRipples objectAtIndex:(self.backgroundRipples.count - 1)] exit:animated]; |
| } |
| } |
| |
| #pragma mark - Properties |
| |
| - (void)spreadFromPoint:(CGPoint)point completion:(void (^)(void))completionBlock { |
| // Create a mask layer before drawing the ink using the superlayer's shadowPath |
| // if it exists. This helps the FAB when it is not rectangular. |
| if (self.masksToBounds && self.superlayer.shadowPath) { |
| CAShapeLayer *mask = [CAShapeLayer layer]; |
| mask.path = self.superlayer.shadowPath; |
| mask.fillColor = [UIColor whiteColor].CGColor; |
| self.mask = mask; |
| } else { |
| self.mask = nil; |
| } |
| |
| CGFloat radius = MDCLegacyInkLayerRadiusBounds( |
| _maxRippleRadius, MDCLegacyInkLayerRectHypotenuse(self.bounds) / 2, _bounded); |
| |
| MDCLegacyInkLayerBackgroundRipple *backgroundRipple = |
| [[MDCLegacyInkLayerBackgroundRipple alloc] init]; |
| backgroundRipple.inkLayer = _compositeRipple; |
| backgroundRipple.targetFrame = self.bounds; |
| backgroundRipple.point = point; |
| backgroundRipple.color = self.inkColor; |
| backgroundRipple.radius = radius; |
| backgroundRipple.bounded = self.bounded; |
| backgroundRipple.animationDelegate = self; |
| [backgroundRipple setupRipple]; |
| |
| MDCLegacyInkLayerForegroundRipple *foregroundRipple = |
| [[MDCLegacyInkLayerForegroundRipple alloc] init]; |
| foregroundRipple.inkLayer = _compositeRipple; |
| foregroundRipple.targetFrame = self.bounds; |
| foregroundRipple.point = point; |
| foregroundRipple.color = self.inkColor; |
| foregroundRipple.radius = radius; |
| foregroundRipple.bounded = self.bounded; |
| foregroundRipple.animationDelegate = self; |
| foregroundRipple.useCustomInkCenter = self.useCustomInkCenter; |
| foregroundRipple.customInkCenter = self.customInkCenter; |
| [foregroundRipple setupRipple]; |
| |
| [backgroundRipple enter]; |
| [self.backgroundRipples addObject:backgroundRipple]; |
| [foregroundRipple enterWithCompletion:completionBlock]; |
| [self.foregroundRipples addObject:foregroundRipple]; |
| } |
| |
| - (void)evaporateWithCompletion:(void (^)(void))completionBlock { |
| [self resetBottomInk:YES completion:completionBlock]; |
| } |
| |
| - (void)evaporateToPoint:(CGPoint)point completion:(void (^)(void))completionBlock { |
| [self resetBottomInk:YES toPoint:point completion:completionBlock]; |
| } |
| |
| #pragma mark - MDCLegacyRippleInkLayerDelegate |
| |
| - (void)animationDidStart:(MDCLegacyInkLayerRipple *)layerRipple { |
| if (!self.isAnimating) { |
| self.animating = YES; |
| |
| if ([self.animationDelegate respondsToSelector:@selector(legacyInkLayerAnimationDidStart:)]) { |
| [self.animationDelegate legacyInkLayerAnimationDidStart:self]; |
| } |
| } |
| } |
| |
| - (void)animationDidStop:(__unused CAAnimation *)anim |
| shapeLayer:(MDCLegacyInkLayerRipple *)layerRipple |
| finished:(__unused BOOL)finished { |
| // Even when the ripple is "exited" without animation, we need to remove it from compositeRipple |
| [layerRipple removeFromSuperlayer]; |
| [layerRipple removeAllAnimations]; |
| |
| if ([layerRipple isKindOfClass:[MDCLegacyInkLayerForegroundRipple class]]) { |
| [self.foregroundRipples removeObject:(MDCLegacyInkLayerForegroundRipple *)layerRipple]; |
| } else if ([layerRipple isKindOfClass:[MDCLegacyInkLayerBackgroundRipple class]]) { |
| [self.backgroundRipples removeObject:(MDCLegacyInkLayerBackgroundRipple *)layerRipple]; |
| } |
| |
| // Check if all ink layer animations did finish and call animation end callback |
| if (self.isAnimating && self.foregroundRipples.count == 0 && self.backgroundRipples.count == 0) { |
| self.animating = NO; |
| if ([self.animationDelegate respondsToSelector:@selector(legacyInkLayerAnimationDidEnd:)]) { |
| [self.animationDelegate legacyInkLayerAnimationDidEnd:self]; |
| } |
| } |
| } |
| |
| @end |