blob: 326471ca60f4ff4c6e59d3f47ecf503fa764c730 [file] [log] [blame] [edit]
// 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