blob: 0ebae41d374e0a2fdd2232c729d39f4d56ae602b [file] [log] [blame] [edit]
// Copyright 2019-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 "MDCRippleLayer.h"
#import <CoreGraphics/CoreGraphics.h>
#import "MDCRippleLayerDelegate.h"
static const CGFloat kExpandRippleBeyondSurface = 10;
static const CGFloat kRippleStartingScale = (CGFloat)0.6;
static const CGFloat kRippleTouchDownDuration = (CGFloat)0.225;
static const CGFloat kRippleTouchUpDuration = (CGFloat)0.15;
static const CGFloat kRippleFadeInDuration = (CGFloat)0.075;
static const CGFloat kRippleFadeOutDuration = (CGFloat)0.075;
static const CGFloat kRippleFadeOutDelay = (CGFloat)0.15;
static NSString *const kRippleLayerOpacityString = @"opacity";
static NSString *const kRippleLayerPositionString = @"position";
static NSString *const kRippleLayerScaleString = @"transform.scale";
static CGFloat GetInitialRippleRadius(CGRect rect) {
return MAX(CGRectGetWidth(rect), CGRectGetHeight(rect)) * kRippleStartingScale / 2.f;
}
static CGFloat GetFinalRippleRadius(CGRect rect) {
return (CGFloat)(hypot(CGRectGetMidX(rect), CGRectGetMidY(rect)) + kExpandRippleBeyondSurface);
}
@implementation MDCRippleLayer
- (void)setNeedsLayout {
[super setNeedsLayout];
[self calculateRadiusAndSetPath];
self.position = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
}
- (void)calculateRadiusAndSetPath {
[self setPathFromRadii:[self calculateRadius]];
}
- (void)setPathFromRadii:(CGFloat)radius {
CGRect ovalRect = CGRectMake(CGRectGetMidX(self.bounds) - radius,
CGRectGetMidY(self.bounds) - radius, radius * 2, radius * 2);
UIBezierPath *circlePath = [UIBezierPath bezierPathWithOvalInRect:ovalRect];
self.path = circlePath.CGPath;
}
- (CGFloat)calculateRadius {
return self.maximumRadius > 0 ? self.maximumRadius : GetFinalRippleRadius(self.bounds);
}
- (void)startRippleAtPoint:(CGPoint)point
animated:(BOOL)animated
completion:(MDCRippleCompletionBlock)completion {
[self.rippleLayerDelegate rippleLayerTouchDownAnimationDidBegin:self];
CGFloat finalRadius = [self calculateRadius];
[self setPathFromRadii:finalRadius];
self.opacity = 1;
self.position = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
if (!animated) {
if (completion) {
completion();
}
[self.rippleLayerDelegate rippleLayerTouchDownAnimationDidEnd:self];
} else {
_startAnimationActive = YES;
CGFloat startingScale = GetInitialRippleRadius(self.bounds) / finalRadius;
CABasicAnimation *scaleAnim = [[CABasicAnimation alloc] init];
scaleAnim.keyPath = kRippleLayerScaleString;
scaleAnim.fromValue = @(startingScale);
scaleAnim.toValue = @1;
scaleAnim.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
UIBezierPath *centerPath = [UIBezierPath bezierPath];
CGPoint startPoint = point;
CGPoint endPoint = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
[centerPath moveToPoint:startPoint];
[centerPath addLineToPoint:endPoint];
[centerPath closePath];
CAKeyframeAnimation *positionAnim = [[CAKeyframeAnimation alloc] init];
positionAnim.keyPath = kRippleLayerPositionString;
positionAnim.path = centerPath.CGPath;
positionAnim.keyTimes = @[ @0, @1 ];
positionAnim.values = @[ @0, @1 ];
positionAnim.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
CABasicAnimation *fadeInAnim = [[CABasicAnimation alloc] init];
fadeInAnim.keyPath = kRippleLayerOpacityString;
fadeInAnim.fromValue = @0;
fadeInAnim.toValue = @1;
fadeInAnim.duration = kRippleFadeInDuration;
fadeInAnim.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[CATransaction begin];
CAAnimationGroup *animGroup = [[CAAnimationGroup alloc] init];
animGroup.animations = @[ scaleAnim, positionAnim, fadeInAnim ];
animGroup.duration = kRippleTouchDownDuration;
[CATransaction setCompletionBlock:^{
self->_startAnimationActive = NO;
if (completion) {
completion();
}
[self.rippleLayerDelegate rippleLayerTouchDownAnimationDidEnd:self];
}];
[self addAnimation:animGroup forKey:nil];
_rippleTouchDownStartTime = CACurrentMediaTime();
[CATransaction commit];
}
}
- (void)fadeInRippleAnimated:(BOOL)animated completion:(MDCRippleCompletionBlock)completion {
[CATransaction begin];
CABasicAnimation *fadeInAnim = [[CABasicAnimation alloc] init];
fadeInAnim.keyPath = kRippleLayerOpacityString;
fadeInAnim.fromValue = @0;
fadeInAnim.toValue = @1;
fadeInAnim.duration = animated ? kRippleFadeInDuration : 0;
fadeInAnim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
fadeInAnim.fillMode = kCAFillModeForwards;
fadeInAnim.removedOnCompletion = NO;
[CATransaction setCompletionBlock:^{
if (completion) {
completion();
}
}];
[self addAnimation:fadeInAnim forKey:nil];
[CATransaction commit];
}
- (void)fadeOutRippleAnimated:(BOOL)animated completion:(MDCRippleCompletionBlock)completion {
[CATransaction begin];
CABasicAnimation *fadeInAnim = [[CABasicAnimation alloc] init];
fadeInAnim.keyPath = kRippleLayerOpacityString;
fadeInAnim.fromValue = @1;
fadeInAnim.toValue = @0;
fadeInAnim.duration = animated ? kRippleFadeOutDuration : 0;
fadeInAnim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
fadeInAnim.fillMode = kCAFillModeForwards;
fadeInAnim.removedOnCompletion = NO;
[CATransaction setCompletionBlock:^{
if (completion) {
completion();
}
}];
[self addAnimation:fadeInAnim forKey:nil];
[CATransaction commit];
}
- (void)endRippleAnimated:(BOOL)animated completion:(MDCRippleCompletionBlock)completion {
CGFloat delay = 0;
if (self.startAnimationActive) {
delay = kRippleFadeOutDelay;
}
[self.rippleLayerDelegate rippleLayerTouchUpAnimationDidBegin:self];
[CATransaction begin];
CABasicAnimation *fadeOutAnim = [[CABasicAnimation alloc] init];
fadeOutAnim.keyPath = kRippleLayerOpacityString;
fadeOutAnim.fromValue = @1;
fadeOutAnim.toValue = @0;
fadeOutAnim.duration = animated ? kRippleTouchUpDuration : 0;
fadeOutAnim.beginTime = [self convertTime:_rippleTouchDownStartTime + delay fromLayer:nil];
fadeOutAnim.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
fadeOutAnim.fillMode = kCAFillModeForwards;
fadeOutAnim.removedOnCompletion = NO;
[CATransaction setCompletionBlock:^{
if (completion) {
completion();
}
[self.rippleLayerDelegate rippleLayerTouchUpAnimationDidEnd:self];
[self removeFromSuperlayer];
}];
[self addAnimation:fadeOutAnim forKey:nil];
[CATransaction commit];
}
@end