| // 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 |