| // 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 "MDCShadowLayer.h" |
| |
| static const CGFloat kShadowElevationDialog = 24.0; |
| static const float kKeyShadowOpacity = (float)0.26; |
| static const float kAmbientShadowOpacity = (float)0.08; |
| |
| @interface MDCPendingAnimation : NSObject <CAAction> |
| @property(nonatomic, weak) CALayer *animationSourceLayer; |
| @property(nonatomic, strong) NSString *keyPath; |
| @property(nonatomic, strong) id fromValue; |
| @property(nonatomic, strong) id toValue; |
| @end |
| |
| @implementation MDCShadowMetrics |
| |
| + (MDCShadowMetrics *)metricsWithElevation:(CGFloat)elevation { |
| if (0.0 < elevation) { |
| return [[MDCShadowMetrics alloc] initWithElevation:elevation]; |
| } else { |
| return [MDCShadowMetrics emptyShadowMetrics]; |
| } |
| } |
| |
| - (MDCShadowMetrics *)initWithElevation:(CGFloat)elevation { |
| self = [super init]; |
| if (self) { |
| _topShadowRadius = [MDCShadowMetrics ambientShadowBlur:elevation]; |
| _topShadowOffset = CGSizeMake(0.0, 0.0); |
| _topShadowOpacity = kAmbientShadowOpacity; |
| _bottomShadowRadius = [MDCShadowMetrics keyShadowBlur:elevation]; |
| _bottomShadowOffset = CGSizeMake(0.0, [MDCShadowMetrics keyShadowYOff:elevation]); |
| _bottomShadowOpacity = kKeyShadowOpacity; |
| } |
| return self; |
| } |
| |
| + (MDCShadowMetrics *)emptyShadowMetrics { |
| static MDCShadowMetrics *emptyShadowMetrics; |
| static dispatch_once_t once; |
| dispatch_once(&once, ^{ |
| emptyShadowMetrics = [[MDCShadowMetrics alloc] init]; |
| emptyShadowMetrics->_topShadowRadius = (CGFloat)0.0; |
| emptyShadowMetrics->_topShadowOffset = CGSizeMake(0.0, 0.0); |
| emptyShadowMetrics->_topShadowOpacity = 0; |
| emptyShadowMetrics->_bottomShadowRadius = (CGFloat)0.0; |
| emptyShadowMetrics->_bottomShadowOffset = CGSizeMake(0.0, 0.0); |
| emptyShadowMetrics->_bottomShadowOpacity = 0; |
| }); |
| |
| return emptyShadowMetrics; |
| } |
| |
| + (CGFloat)ambientShadowBlur:(CGFloat)points { |
| CGFloat blur = (CGFloat)0.889544 * points - (CGFloat)0.003701; |
| return blur; |
| } |
| |
| + (CGFloat)keyShadowBlur:(CGFloat)points { |
| CGFloat blur = (CGFloat)0.666920 * points - (CGFloat)0.001648; |
| return blur; |
| } |
| |
| + (CGFloat)keyShadowYOff:(CGFloat)points { |
| CGFloat yOff = (CGFloat)1.23118 * points - (CGFloat)0.03933; |
| return yOff; |
| } |
| |
| @end |
| |
| @interface MDCShadowLayer () |
| |
| @property(nonatomic, strong) CAShapeLayer *topShadow; |
| @property(nonatomic, strong) CAShapeLayer *bottomShadow; |
| @property(nonatomic, strong) CAShapeLayer *topShadowMask; |
| @property(nonatomic, strong) CAShapeLayer *bottomShadowMask; |
| |
| @end |
| |
| @implementation MDCShadowLayer { |
| BOOL _shadowPathIsInvalid; |
| } |
| |
| - (instancetype)init { |
| self = [super init]; |
| if (self) { |
| _elevation = 0; |
| _shadowMaskEnabled = YES; |
| _shadowPathIsInvalid = YES; |
| |
| [self commonMDCShadowLayerInit]; |
| } |
| return self; |
| } |
| |
| - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { |
| self = [super initWithCoder:aDecoder]; |
| if (self) { |
| [self commonMDCShadowLayerInit]; |
| } |
| return self; |
| } |
| |
| - (instancetype)initWithLayer:(id)layer { |
| if (self = [super initWithLayer:layer]) { |
| if ([layer isKindOfClass:[MDCShadowLayer class]]) { |
| MDCShadowLayer *otherLayer = (MDCShadowLayer *)layer; |
| _elevation = otherLayer.elevation; |
| _shadowMaskEnabled = otherLayer.isShadowMaskEnabled; |
| _bottomShadow = [[CAShapeLayer alloc] initWithLayer:otherLayer.bottomShadow]; |
| _topShadow = [[CAShapeLayer alloc] initWithLayer:otherLayer.topShadow]; |
| _topShadowMask = [[CAShapeLayer alloc] initWithLayer:otherLayer.topShadowMask]; |
| _bottomShadowMask = [[CAShapeLayer alloc] initWithLayer:otherLayer.bottomShadowMask]; |
| [self commonMDCShadowLayerInit]; |
| } |
| } |
| return self; |
| } |
| |
| /** |
| commonMDCShadowLayerInit creates additional layers based on the values of _elevation and |
| _shadowMaskEnabled. |
| */ |
| - (void)commonMDCShadowLayerInit { |
| if (!_bottomShadow) { |
| _bottomShadow = [CAShapeLayer layer]; |
| _bottomShadow.backgroundColor = [UIColor clearColor].CGColor; |
| _bottomShadow.shadowColor = [UIColor blackColor].CGColor; |
| _bottomShadow.delegate = self; |
| [self addSublayer:_bottomShadow]; |
| } |
| |
| if (!_topShadow) { |
| _topShadow = [CAShapeLayer layer]; |
| _topShadow.backgroundColor = [UIColor clearColor].CGColor; |
| _topShadow.shadowColor = [UIColor blackColor].CGColor; |
| _topShadow.delegate = self; |
| [self addSublayer:_topShadow]; |
| } |
| |
| // Setup shadow layer state based off _elevation and _shadowMaskEnabled |
| MDCShadowMetrics *shadowMetrics = [MDCShadowMetrics metricsWithElevation:_elevation]; |
| _topShadow.shadowOffset = shadowMetrics.topShadowOffset; |
| _topShadow.shadowRadius = shadowMetrics.topShadowRadius; |
| _topShadow.shadowOpacity = shadowMetrics.topShadowOpacity; |
| _bottomShadow.shadowOffset = shadowMetrics.bottomShadowOffset; |
| _bottomShadow.shadowRadius = shadowMetrics.bottomShadowRadius; |
| _bottomShadow.shadowOpacity = shadowMetrics.bottomShadowOpacity; |
| |
| if (!_topShadowMask) { |
| _topShadowMask = [CAShapeLayer layer]; |
| _topShadowMask.delegate = self; |
| } |
| if (!_bottomShadowMask) { |
| _bottomShadowMask = [CAShapeLayer layer]; |
| _bottomShadowMask.delegate = self; |
| } |
| |
| // TODO(#1021): We shouldn't be calling property accessors in an init method. |
| if (_shadowMaskEnabled) { |
| [self configureShadowLayerMaskForLayer:_topShadowMask]; |
| [self configureShadowLayerMaskForLayer:_bottomShadowMask]; |
| _topShadow.mask = _topShadowMask; |
| _bottomShadow.mask = _bottomShadowMask; |
| } |
| } |
| |
| - (void)layoutSublayers { |
| [super layoutSublayers]; |
| |
| [self prepareShadowPath]; |
| [self commonLayoutSublayers]; |
| } |
| |
| - (void)setBounds:(CGRect)bounds { |
| BOOL sizeChanged = !CGSizeEqualToSize(self.bounds.size, bounds.size); |
| [super setBounds:bounds]; |
| if (sizeChanged) { |
| _shadowPathIsInvalid = YES; |
| [self setNeedsLayout]; |
| } |
| } |
| |
| - (void)prepareShadowPath { |
| // This method is meant to be overriden by its subclasses. |
| } |
| |
| #pragma mark - CALayer change monitoring. |
| |
| /** Returns a shadowPath based on the layer properties. */ |
| - (UIBezierPath *)defaultShadowPath { |
| CGFloat cornerRadius = self.cornerRadius; |
| if (0.0 < cornerRadius) { |
| return [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:cornerRadius]; |
| } |
| return [UIBezierPath bezierPathWithRect:self.bounds]; |
| } |
| |
| - (void)setCornerRadius:(CGFloat)cornerRadius { |
| super.cornerRadius = cornerRadius; |
| |
| _topShadow.cornerRadius = cornerRadius; |
| _bottomShadow.cornerRadius = cornerRadius; |
| if (_shadowMaskEnabled) { |
| [self configureShadowLayerMaskForLayer:_topShadowMask]; |
| [self configureShadowLayerMaskForLayer:_bottomShadowMask]; |
| _topShadow.mask = _topShadowMask; |
| _bottomShadow.mask = _bottomShadowMask; |
| } |
| } |
| |
| - (void)setShadowPath:(CGPathRef)shadowPath { |
| super.shadowPath = shadowPath; |
| _topShadow.shadowPath = shadowPath; |
| _bottomShadow.shadowPath = shadowPath; |
| if (_shadowMaskEnabled) { |
| [self configureShadowLayerMaskForLayer:_topShadowMask]; |
| [self configureShadowLayerMaskForLayer:_bottomShadowMask]; |
| } |
| } |
| |
| - (void)setShadowColor:(CGColorRef)shadowColor { |
| super.shadowColor = shadowColor; |
| _topShadow.shadowColor = shadowColor; |
| _bottomShadow.shadowColor = shadowColor; |
| } |
| |
| #pragma mark - shouldRasterize forwarding |
| |
| - (void)setShouldRasterize:(BOOL)shouldRasterize { |
| [super setShouldRasterize:shouldRasterize]; |
| _topShadow.shouldRasterize = shouldRasterize; |
| _bottomShadow.shouldRasterize = shouldRasterize; |
| } |
| |
| #pragma mark - Shadow Spread |
| |
| // Returns how far aware the shadow is spread from the edge of the layer. |
| + (CGSize)shadowSpreadForElevation:(CGFloat)elevation { |
| MDCShadowMetrics *metrics = [MDCShadowMetrics metricsWithElevation:elevation]; |
| |
| CGSize shadowSpread = CGSizeZero; |
| shadowSpread.width = MAX(metrics.topShadowRadius, metrics.bottomShadowRadius) + |
| MAX(metrics.topShadowOffset.width, metrics.bottomShadowOffset.width); |
| shadowSpread.height = MAX(metrics.topShadowRadius, metrics.bottomShadowRadius) + |
| MAX(metrics.topShadowOffset.height, metrics.bottomShadowOffset.height); |
| |
| return shadowSpread; |
| } |
| |
| #pragma mark - Pseudo Shadow Masks |
| |
| - (void)setShadowMaskEnabled:(BOOL)shadowMaskEnabled { |
| _shadowMaskEnabled = shadowMaskEnabled; |
| if (_shadowMaskEnabled) { |
| [self configureShadowLayerMaskForLayer:_topShadowMask]; |
| [self configureShadowLayerMaskForLayer:_bottomShadowMask]; |
| _topShadow.mask = _topShadowMask; |
| _bottomShadow.mask = _bottomShadowMask; |
| } else { |
| _topShadow.mask = nil; |
| _bottomShadow.mask = nil; |
| } |
| } |
| |
| // Creates a layer mask that has a hole cut inside so that the original contents |
| // of the view is no obscured by the shadow the top/bottom pseudo shadow layers |
| // cast. |
| - (void)configureShadowLayerMaskForLayer:(CAShapeLayer *)maskLayer { |
| UIBezierPath *path = [self outerMaskPath]; |
| UIBezierPath *innerPath = nil; |
| if (self.shadowPath != nil) { |
| innerPath = [UIBezierPath bezierPathWithCGPath:(_Nonnull CGPathRef)self.shadowPath]; |
| } else if (self.cornerRadius > 0) { |
| innerPath = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:self.cornerRadius]; |
| } else { |
| innerPath = [UIBezierPath bezierPathWithRect:self.bounds]; |
| } |
| [path appendPath:innerPath]; |
| [path setUsesEvenOddFillRule:YES]; |
| |
| maskLayer.position = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); |
| maskLayer.bounds = [self maskRect]; |
| maskLayer.path = path.CGPath; |
| maskLayer.fillRule = kCAFillRuleEvenOdd; |
| maskLayer.fillColor = [UIColor blackColor].CGColor; |
| } |
| |
| - (CGRect)maskRect { |
| CGSize shadowSpread = [MDCShadowLayer shadowSpreadForElevation:kShadowElevationDialog]; |
| CGRect bounds = self.bounds; |
| return CGRectInset(bounds, -shadowSpread.width * 2, -shadowSpread.height * 2); |
| } |
| |
| - (UIBezierPath *)outerMaskPath { |
| return [UIBezierPath bezierPathWithRect:[self maskRect]]; |
| } |
| |
| - (void)setElevation:(CGFloat)elevation { |
| _elevation = elevation; |
| |
| MDCShadowMetrics *shadowMetrics = [MDCShadowMetrics metricsWithElevation:elevation]; |
| |
| _topShadow.shadowOffset = shadowMetrics.topShadowOffset; |
| _topShadow.shadowRadius = shadowMetrics.topShadowRadius; |
| _topShadow.shadowOpacity = shadowMetrics.topShadowOpacity; |
| _bottomShadow.shadowOffset = shadowMetrics.bottomShadowOffset; |
| _bottomShadow.shadowRadius = shadowMetrics.bottomShadowRadius; |
| _bottomShadow.shadowOpacity = shadowMetrics.bottomShadowOpacity; |
| } |
| |
| #pragma mark - CALayerDelegate |
| |
| - (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event { |
| if ([event isEqualToString:@"path"] || [event isEqualToString:@"shadowPath"]) { |
| // We have to create a pending animation because if we are inside a UIKit animation block we |
| // won't know any properties of the animation block until it is commited. |
| MDCPendingAnimation *pendingAnim = [[MDCPendingAnimation alloc] init]; |
| pendingAnim.animationSourceLayer = self; |
| pendingAnim.fromValue = [layer.presentationLayer valueForKey:event]; |
| pendingAnim.toValue = nil; |
| pendingAnim.keyPath = event; |
| |
| return pendingAnim; |
| } |
| return nil; |
| } |
| |
| #pragma mark - Private |
| |
| - (void)commonLayoutSublayers { |
| CGRect bounds = self.bounds; |
| |
| _bottomShadow.position = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds)); |
| _bottomShadow.bounds = bounds; |
| _topShadow.position = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds)); |
| _topShadow.bounds = bounds; |
| |
| if (_shadowMaskEnabled) { |
| [self configureShadowLayerMaskForLayer:_topShadowMask]; |
| [self configureShadowLayerMaskForLayer:_bottomShadowMask]; |
| } |
| // Enforce shadowPaths because otherwise no shadows can be drawn. If a shadowPath |
| // is already set, use that, otherwise fallback to just a regular rect because path. |
| if (!_bottomShadow.shadowPath || _shadowPathIsInvalid) { |
| if (self.shadowPath) { |
| _bottomShadow.shadowPath = self.shadowPath; |
| } else { |
| _bottomShadow.shadowPath = [self defaultShadowPath].CGPath; |
| } |
| } |
| if (!_topShadow.shadowPath || _shadowPathIsInvalid) { |
| if (self.shadowPath) { |
| _topShadow.shadowPath = self.shadowPath; |
| } else { |
| _topShadow.shadowPath = [self defaultShadowPath].CGPath; |
| } |
| } |
| _shadowPathIsInvalid = NO; |
| } |
| |
| - (void)animateCornerRadius:(CGFloat)cornerRadius |
| withTimingFunction:(CAMediaTimingFunction *)timingFunction |
| duration:(NSTimeInterval)duration { |
| [CATransaction begin]; |
| [CATransaction setDisableActions:YES]; |
| CGFloat currentCornerRadius = (self.cornerRadius <= 0) ? (CGFloat)0.001 : self.cornerRadius; |
| CGFloat newCornerRadius = (cornerRadius <= 0) ? (CGFloat)0.001 : cornerRadius; |
| // Create the paths |
| UIBezierPath *currentLayerPath = [UIBezierPath bezierPathWithRoundedRect:self.bounds |
| cornerRadius:currentCornerRadius]; |
| UIBezierPath *newLayerPath = [UIBezierPath bezierPathWithRoundedRect:self.bounds |
| cornerRadius:newCornerRadius]; |
| |
| UIBezierPath *currentMaskPath = [self outerMaskPath]; |
| [currentMaskPath appendPath:currentLayerPath]; |
| currentMaskPath.usesEvenOddFillRule = YES; |
| |
| UIBezierPath *newMaskPath = [self outerMaskPath]; |
| [newMaskPath appendPath:newLayerPath]; |
| newMaskPath.usesEvenOddFillRule = YES; |
| |
| // Animate the top layers |
| NSString *shadowPathKey = @"shadowPath"; |
| CABasicAnimation *topLayerAnimation = [CABasicAnimation animationWithKeyPath:shadowPathKey]; |
| topLayerAnimation.fromValue = (__bridge id)currentLayerPath.CGPath; |
| topLayerAnimation.toValue = (__bridge id)newLayerPath.CGPath; |
| topLayerAnimation.duration = duration; |
| topLayerAnimation.timingFunction = timingFunction; |
| self.topShadow.shadowPath = newLayerPath.CGPath; |
| [self.topShadow addAnimation:topLayerAnimation forKey:shadowPathKey]; |
| CABasicAnimation *bottomLayerAnimation = [CABasicAnimation animationWithKeyPath:shadowPathKey]; |
| bottomLayerAnimation.fromValue = (__bridge id)currentLayerPath.CGPath; |
| bottomLayerAnimation.toValue = (__bridge id)newLayerPath.CGPath; |
| bottomLayerAnimation.duration = duration; |
| bottomLayerAnimation.timingFunction = timingFunction; |
| self.bottomShadow.shadowPath = newLayerPath.CGPath; |
| [self.bottomShadow addAnimation:bottomLayerAnimation forKey:shadowPathKey]; |
| |
| // Animate the masks |
| if (self.shadowMaskEnabled) { |
| NSString *pathKey = @"path"; |
| CABasicAnimation *topMaskLayerAnimation = [CABasicAnimation animationWithKeyPath:pathKey]; |
| topMaskLayerAnimation.fromValue = (__bridge id)currentMaskPath.CGPath; |
| topMaskLayerAnimation.toValue = (__bridge id)newMaskPath.CGPath; |
| topMaskLayerAnimation.duration = duration; |
| topMaskLayerAnimation.timingFunction = timingFunction; |
| self.topShadowMask.path = newMaskPath.CGPath; |
| [self.topShadowMask addAnimation:topMaskLayerAnimation forKey:pathKey]; |
| CABasicAnimation *bottomMaskLayerAnimation = [CABasicAnimation animationWithKeyPath:pathKey]; |
| bottomMaskLayerAnimation.fromValue = (__bridge id)currentMaskPath.CGPath; |
| bottomMaskLayerAnimation.toValue = (__bridge id)newMaskPath.CGPath; |
| bottomMaskLayerAnimation.duration = duration; |
| bottomMaskLayerAnimation.timingFunction = timingFunction; |
| self.bottomShadowMask.path = newMaskPath.CGPath; |
| [self.bottomShadowMask addAnimation:bottomMaskLayerAnimation forKey:pathKey]; |
| } |
| |
| // Animate the corner radius |
| CABasicAnimation *cornerRadiusAnimation = [CABasicAnimation animationWithKeyPath:@"cornerRadius"]; |
| cornerRadiusAnimation.fromValue = @((CGFloat)currentCornerRadius); |
| cornerRadiusAnimation.toValue = @((CGFloat)newCornerRadius); |
| cornerRadiusAnimation.duration = duration; |
| cornerRadiusAnimation.timingFunction = timingFunction; |
| self.cornerRadius = cornerRadius; |
| [self addAnimation:cornerRadiusAnimation forKey:@"cornerRadius"]; |
| [CATransaction commit]; |
| } |
| |
| @end |
| |
| @implementation MDCPendingAnimation |
| |
| - (void)runActionForKey:(NSString *)event object:(id)anObject arguments:(NSDictionary *)dict { |
| if ([anObject isKindOfClass:[CAShapeLayer class]]) { |
| CAShapeLayer *layer = (CAShapeLayer *)anObject; |
| |
| // In order to synchronize our animation with UIKit animations we have to fetch the resizing |
| // animation created by UIKit and copy the configuration to our custom animation. |
| CAAnimation *boundsAction = [self.animationSourceLayer animationForKey:@"bounds.size"]; |
| if (!boundsAction) { |
| // Headless layers will animate bounds directly instead of decomposing |
| // bounds.size/bounds.position. A headless layer is a CALayer without a delegate (usually |
| // would be a UIView). |
| boundsAction = [self.animationSourceLayer animationForKey:@"bounds"]; |
| } |
| if ([boundsAction isKindOfClass:[CABasicAnimation class]]) { |
| CABasicAnimation *animation = (CABasicAnimation *)[boundsAction copy]; |
| animation.keyPath = self.keyPath; |
| animation.fromValue = self.fromValue; |
| animation.toValue = self.toValue; |
| |
| [layer addAnimation:animation forKey:event]; |
| } |
| } |
| } |
| |
| @end |