blob: 4ab3b5ee5d14ac3b72f83e3a1a84fd26f9577ad4 [file] [log] [blame]
// Copyright 2015-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 "MDCInkLayer.h"
#import "MDCInkLayerDelegate.h"
#import <CoreGraphics/CoreGraphics.h>
static const CGFloat MDCInkLayerCommonDuration = (CGFloat)0.083;
static const CGFloat MDCInkLayerEndFadeOutDuration = (CGFloat)0.15;
static const CGFloat MDCInkLayerStartScalePositionDuration = (CGFloat)0.333;
static const CGFloat MDCInkLayerStartFadeHalfDuration = (CGFloat)0.167;
static const CGFloat MDCInkLayerStartFadeHalfBeginTimeFadeOutDuration = (CGFloat)0.25;
static const CGFloat MDCInkLayerScaleStartMin = (CGFloat)0.2;
static const CGFloat MDCInkLayerScaleStartMax = (CGFloat)0.6;
static const CGFloat MDCInkLayerScaleDivisor = 300;
static NSString *const MDCInkLayerOpacityString = @"opacity";
static NSString *const MDCInkLayerPositionString = @"position";
static NSString *const MDCInkLayerScaleString = @"transform.scale";
@implementation MDCInkLayer
- (instancetype)init {
self = [super init];
if (self) {
_inkColor = [UIColor colorWithWhite:0 alpha:(CGFloat)0.08];
}
return self;
}
- (instancetype)initWithLayer:(id)layer {
self = [super initWithLayer:layer];
if (self) {
_endAnimationDelay = 0;
_finalRadius = 0;
_initialRadius = 0;
_inkColor = [UIColor colorWithWhite:0 alpha:(CGFloat)0.08];
_startAnimationActive = NO;
if ([layer isKindOfClass:[MDCInkLayer class]]) {
MDCInkLayer *inkLayer = (MDCInkLayer *)layer;
_endAnimationDelay = inkLayer.endAnimationDelay;
_finalRadius = inkLayer.finalRadius;
_initialRadius = inkLayer.initialRadius;
_maxRippleRadius = inkLayer.maxRippleRadius;
_inkColor = inkLayer.inkColor;
_startAnimationActive = NO;
}
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
_inkColor = [UIColor colorWithWhite:0 alpha:(CGFloat)0.08];
}
return self;
}
- (void)setNeedsLayout {
[super setNeedsLayout];
[self setRadiiWithRect:self.bounds];
}
- (void)setRadiiWithRect:(CGRect)rect {
self.initialRadius =
(CGFloat)(hypot(CGRectGetHeight(rect), CGRectGetWidth(rect)) / 2 * (CGFloat)0.6);
self.finalRadius = (CGFloat)(hypot(CGRectGetHeight(rect), CGRectGetWidth(rect)) / 2 + 10);
}
- (void)startAnimationAtPoint:(CGPoint)point {
[self startInkAtPoint:point animated:YES];
}
- (void)startInkAtPoint:(CGPoint)point animated:(BOOL)animated {
CGFloat radius = self.finalRadius;
if (self.maxRippleRadius > 0) {
radius = self.maxRippleRadius;
}
CGRect ovalRect = CGRectMake(CGRectGetWidth(self.bounds) / 2 - radius,
CGRectGetHeight(self.bounds) / 2 - radius, radius * 2, radius * 2);
UIBezierPath *circlePath = [UIBezierPath bezierPathWithOvalInRect:ovalRect];
self.path = circlePath.CGPath;
self.fillColor = self.inkColor.CGColor;
if (!animated) {
self.opacity = 1;
self.position = CGPointMake(CGRectGetWidth(self.bounds) / 2, CGRectGetHeight(self.bounds) / 2);
} else {
self.opacity = 0;
self.position = point;
_startAnimationActive = YES;
CAMediaTimingFunction *materialTimingFunction =
[[CAMediaTimingFunction alloc] initWithControlPoints:(float) 0.4:0:(float)0.2:1];
CGFloat scaleStart =
MIN(CGRectGetWidth(self.bounds), CGRectGetHeight(self.bounds)) / MDCInkLayerScaleDivisor;
if (scaleStart < MDCInkLayerScaleStartMin) {
scaleStart = MDCInkLayerScaleStartMin;
} else if (scaleStart > MDCInkLayerScaleStartMax) {
scaleStart = MDCInkLayerScaleStartMax;
}
CABasicAnimation *scaleAnim = [[CABasicAnimation alloc] init];
scaleAnim.keyPath = MDCInkLayerScaleString;
scaleAnim.fromValue = @(scaleStart);
scaleAnim.toValue = @1;
scaleAnim.duration = MDCInkLayerStartScalePositionDuration;
scaleAnim.beginTime = MDCInkLayerCommonDuration;
scaleAnim.timingFunction = materialTimingFunction;
scaleAnim.fillMode = kCAFillModeForwards;
scaleAnim.removedOnCompletion = NO;
UIBezierPath *centerPath = [UIBezierPath bezierPath];
CGPoint startPoint = point;
CGPoint endPoint =
CGPointMake(CGRectGetWidth(self.bounds) / 2, CGRectGetHeight(self.bounds) / 2);
[centerPath moveToPoint:startPoint];
[centerPath addLineToPoint:endPoint];
[centerPath closePath];
CAKeyframeAnimation *positionAnim = [[CAKeyframeAnimation alloc] init];
positionAnim.keyPath = MDCInkLayerPositionString;
positionAnim.path = centerPath.CGPath;
positionAnim.keyTimes = @[ @0, @1 ];
positionAnim.values = @[ @0, @1 ];
positionAnim.duration = MDCInkLayerStartScalePositionDuration;
positionAnim.beginTime = MDCInkLayerCommonDuration;
positionAnim.timingFunction = materialTimingFunction;
positionAnim.fillMode = kCAFillModeForwards;
positionAnim.removedOnCompletion = NO;
CABasicAnimation *fadeInAnim = [[CABasicAnimation alloc] init];
fadeInAnim.keyPath = MDCInkLayerOpacityString;
fadeInAnim.fromValue = @0;
fadeInAnim.toValue = @1;
fadeInAnim.duration = MDCInkLayerCommonDuration;
fadeInAnim.beginTime = MDCInkLayerCommonDuration;
fadeInAnim.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
fadeInAnim.fillMode = kCAFillModeForwards;
fadeInAnim.removedOnCompletion = NO;
[CATransaction begin];
CAAnimationGroup *animGroup = [[CAAnimationGroup alloc] init];
animGroup.animations = @[ scaleAnim, positionAnim, fadeInAnim ];
animGroup.duration = MDCInkLayerStartScalePositionDuration;
animGroup.fillMode = kCAFillModeForwards;
animGroup.removedOnCompletion = NO;
[CATransaction setCompletionBlock:^{
self->_startAnimationActive = NO;
if ([self.animationDelegate respondsToSelector:@selector(inkLayerStartAnimationDidFinish:)]) {
[self.animationDelegate inkLayerStartAnimationDidFinish:self];
}
}];
[self addAnimation:animGroup forKey:nil];
[CATransaction commit];
}
if ([self.animationDelegate respondsToSelector:@selector(inkLayerAnimationDidStart:)]) {
[self.animationDelegate inkLayerAnimationDidStart:self];
}
}
- (void)changeAnimationAtPoint:(CGPoint)point {
CGFloat animationDelay = 0;
if (self.startAnimationActive) {
animationDelay =
MDCInkLayerStartFadeHalfBeginTimeFadeOutDuration + MDCInkLayerStartFadeHalfDuration;
}
BOOL viewContainsPoint = CGRectContainsPoint(self.bounds, point) ? YES : NO;
CGFloat currOpacity = self.presentationLayer.opacity;
CGFloat updatedOpacity = 0;
if (viewContainsPoint) {
updatedOpacity = 1;
}
CABasicAnimation *changeAnim = [[CABasicAnimation alloc] init];
changeAnim.keyPath = MDCInkLayerOpacityString;
changeAnim.fromValue = @(currOpacity);
changeAnim.toValue = @(updatedOpacity);
changeAnim.duration = MDCInkLayerCommonDuration;
changeAnim.beginTime = [self convertTime:(CACurrentMediaTime() + animationDelay) fromLayer:nil];
changeAnim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
changeAnim.fillMode = kCAFillModeForwards;
changeAnim.removedOnCompletion = NO;
[self addAnimation:changeAnim forKey:nil];
}
- (void)endAnimationAtPoint:(CGPoint)point {
[self endInkAtPoint:point animated:YES];
}
- (void)endInkAtPoint:(CGPoint)point animated:(BOOL)animated {
if (self.startAnimationActive) {
self.endAnimationDelay = MDCInkLayerStartFadeHalfBeginTimeFadeOutDuration;
}
CGFloat opacity = 1;
BOOL viewContainsPoint = CGRectContainsPoint(self.bounds, point) ? YES : NO;
if (!viewContainsPoint) {
opacity = 0;
}
if (!animated) {
self.opacity = 0;
if ([self.animationDelegate respondsToSelector:@selector(inkLayerAnimationDidEnd:)]) {
[self.animationDelegate inkLayerAnimationDidEnd:self];
}
[self removeFromSuperlayer];
} else {
[CATransaction begin];
CABasicAnimation *fadeOutAnim = [[CABasicAnimation alloc] init];
fadeOutAnim.keyPath = MDCInkLayerOpacityString;
fadeOutAnim.fromValue = @(opacity);
fadeOutAnim.toValue = @0;
fadeOutAnim.duration = MDCInkLayerEndFadeOutDuration;
fadeOutAnim.beginTime = [self convertTime:(CACurrentMediaTime() + self.endAnimationDelay)
fromLayer:nil];
fadeOutAnim.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
fadeOutAnim.fillMode = kCAFillModeForwards;
fadeOutAnim.removedOnCompletion = NO;
[CATransaction setCompletionBlock:^{
if ([self.animationDelegate respondsToSelector:@selector(inkLayerAnimationDidEnd:)]) {
[self.animationDelegate inkLayerAnimationDidEnd:self];
}
[self removeFromSuperlayer];
}];
[self addAnimation:fadeOutAnim forKey:nil];
[CATransaction commit];
}
}
@end