| /* |
| 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 = 0.26f; |
| static const float kAmbientShadowOpacity = 0.08f; |
| |
| static NSString *const MDCShadowLayerElevationKey = @"MDCShadowLayerElevationKey"; |
| static NSString *const MDCShadowLayerShadowMaskEnabledKey = @"MDCShadowLayerShadowMaskEnabledKey"; |
| |
| @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.0f; |
| emptyShadowMetrics->_bottomShadowRadius = (CGFloat)0.0; |
| emptyShadowMetrics->_bottomShadowOffset = CGSizeMake(0.0, 0.0); |
| emptyShadowMetrics->_bottomShadowOpacity = 0.0f; |
| }); |
| |
| return emptyShadowMetrics; |
| } |
| |
| + (CGFloat)ambientShadowBlur:(CGFloat)points { |
| CGFloat blur = 0.889544f * points - 0.003701f; |
| return blur; |
| } |
| |
| + (CGFloat)keyShadowBlur:(CGFloat)points { |
| CGFloat blur = 0.666920f * points - 0.001648f; |
| return blur; |
| } |
| |
| + (CGFloat)keyShadowYOff:(CGFloat)points { |
| CGFloat yOff = 1.23118f * points - 0.03933f; |
| return yOff; |
| } |
| |
| @end |
| |
| @interface MDCShadowLayer () |
| |
| @property(nonatomic, strong) CAShapeLayer *topShadow; |
| @property(nonatomic, strong) CAShapeLayer *bottomShadow; |
| |
| @end |
| |
| @implementation MDCShadowLayer |
| |
| - (instancetype)init { |
| self = [super init]; |
| if (self) { |
| _elevation = 0; |
| _shadowMaskEnabled = YES; |
| |
| [self commonMDCShadowLayerInit]; |
| } |
| return self; |
| } |
| |
| - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { |
| self = [super initWithCoder:aDecoder]; |
| if (self) { |
| if ([aDecoder containsValueForKey:MDCShadowLayerElevationKey]) { |
| _elevation = (CGFloat)[aDecoder decodeDoubleForKey:MDCShadowLayerElevationKey]; |
| } |
| if ([aDecoder containsValueForKey:MDCShadowLayerShadowMaskEnabledKey]) { |
| _shadowMaskEnabled = [aDecoder decodeBoolForKey:MDCShadowLayerShadowMaskEnabledKey]; |
| } |
| |
| [self commonMDCShadowLayerInit]; |
| } |
| return self; |
| } |
| |
| /** |
| commonMDCShadowLayerInit creates additional layers based on the values of _elevation and |
| _shadowMaskEnabled. |
| */ |
| - (void)commonMDCShadowLayerInit { |
| _bottomShadow = [CAShapeLayer layer]; |
| _bottomShadow.backgroundColor = [UIColor clearColor].CGColor; |
| _bottomShadow.shadowColor = [UIColor blackColor].CGColor; |
| [self addSublayer:_bottomShadow]; |
| |
| _topShadow = [CAShapeLayer layer]; |
| _topShadow.backgroundColor = [UIColor clearColor].CGColor; |
| _topShadow.shadowColor = [UIColor blackColor].CGColor; |
| [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; |
| |
| // TODO(#1021): We shouldn't be calling property accessors in an init method. |
| if (_shadowMaskEnabled) { |
| _topShadow.mask = [self shadowLayerMaskForLayer:_topShadow]; |
| _bottomShadow.mask = [self shadowLayerMaskForLayer:_bottomShadow]; |
| } |
| } |
| |
| // TODO(#993): Implement missing initWithLayer: |
| |
| - (void)encodeWithCoder:(NSCoder *)aCoder { |
| [super encodeWithCoder:aCoder]; |
| [aCoder encodeDouble:_elevation forKey:MDCShadowLayerElevationKey]; |
| [aCoder encodeBool:_shadowMaskEnabled forKey:MDCShadowLayerShadowMaskEnabledKey]; |
| // Additional state is calculated at deserialization time based on _elevation and |
| // _shadowMaskEnabled so we don't need to store them. |
| } |
| |
| - (void)layoutSublayers { |
| [super layoutSublayers]; |
| [self commonLayoutSublayers]; |
| } |
| |
| - (void)setBounds:(CGRect)bounds { |
| BOOL sizeChanged = !CGSizeEqualToSize(self.bounds.size, bounds.size); |
| [super setBounds:bounds]; |
| if (sizeChanged) { |
| // Invalidate our shadow paths. |
| _bottomShadow.shadowPath = nil; |
| _topShadow.shadowPath = nil; |
| [self setNeedsLayout]; |
| } |
| } |
| |
| #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)setShadowPath:(CGPathRef)shadowPath { |
| super.shadowPath = shadowPath; |
| _topShadow.shadowPath = shadowPath; |
| _bottomShadow.shadowPath = shadowPath; |
| if (_shadowMaskEnabled) { |
| _topShadow.mask = [self shadowLayerMaskForLayer:_topShadow]; |
| _bottomShadow.mask = [self shadowLayerMaskForLayer:_bottomShadow]; |
| } |
| } |
| |
| - (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) { |
| _topShadow.mask = [self shadowLayerMaskForLayer:_topShadow]; |
| _bottomShadow.mask = [self shadowLayerMaskForLayer:_bottomShadow]; |
| } 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. |
| - (CAShapeLayer *)shadowLayerMaskForLayer:(CALayer *)layer { |
| CAShapeLayer *maskLayer = [CAShapeLayer layer]; |
| |
| CGSize shadowSpread = [MDCShadowLayer shadowSpreadForElevation:kShadowElevationDialog]; |
| CGRect bounds = layer.bounds; |
| CGRect maskRect = CGRectInset(bounds, -shadowSpread.width * 2, -shadowSpread.height * 2); |
| |
| UIBezierPath *path = [UIBezierPath bezierPathWithRect:maskRect]; |
| 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(bounds), CGRectGetMidY(bounds)); |
| maskLayer.bounds = maskRect; |
| maskLayer.path = path.CGPath; |
| maskLayer.fillRule = kCAFillRuleEvenOdd; |
| maskLayer.fillColor = [UIColor blackColor].CGColor; |
| return maskLayer; |
| } |
| |
| - (void)setElevation:(CGFloat)elevation { |
| _elevation = elevation; |
| MDCShadowMetrics *shadowMetrics = [MDCShadowMetrics metricsWithElevation:elevation]; |
| [self setMetrics:shadowMetrics]; |
| } |
| |
| - (void)setMetrics:(MDCShadowMetrics *)shadowMetrics { |
| CABasicAnimation *topOffsetAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOffset"]; |
| topOffsetAnimation.fromValue = nil; |
| topOffsetAnimation.toValue = [NSValue valueWithCGSize:shadowMetrics.topShadowOffset]; |
| |
| CABasicAnimation *bottomOffsetAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOffset"]; |
| bottomOffsetAnimation.fromValue = nil; |
| bottomOffsetAnimation.toValue = [NSValue valueWithCGSize:shadowMetrics.bottomShadowOffset]; |
| |
| CABasicAnimation *topRadiusAnimation = [CABasicAnimation animationWithKeyPath:@"shadowRadius"]; |
| topRadiusAnimation.fromValue = nil; |
| topRadiusAnimation.toValue = @(shadowMetrics.topShadowRadius); |
| |
| CABasicAnimation *bottomRadiusAnimation = [CABasicAnimation animationWithKeyPath:@"shadowRadius"]; |
| bottomRadiusAnimation.fromValue = nil; |
| bottomRadiusAnimation.toValue = @(shadowMetrics.bottomShadowRadius); |
| |
| CABasicAnimation *topOpacityAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"]; |
| topOpacityAnimation.fromValue = nil; |
| topOpacityAnimation.toValue = @(shadowMetrics.topShadowOpacity); |
| |
| CABasicAnimation *bottomOpacityAnimation = |
| [CABasicAnimation animationWithKeyPath:@"shadowOpacity"]; |
| bottomOpacityAnimation.fromValue = nil; |
| bottomOpacityAnimation.toValue = @(shadowMetrics.bottomShadowOpacity); |
| |
| // Group all animations together. |
| CAAnimationGroup *topAnimations = [CAAnimationGroup animation]; |
| topAnimations.animations = @[ topOffsetAnimation, topRadiusAnimation, topOpacityAnimation ]; |
| |
| CAAnimationGroup *bottomAnimations = [CAAnimationGroup animation]; |
| bottomAnimations.animations = |
| @[ bottomOffsetAnimation, bottomRadiusAnimation, bottomOpacityAnimation ]; |
| |
| [_topShadow removeAllAnimations]; |
| [_bottomShadow removeAllAnimations]; |
| [_topShadow addAnimation:topAnimations forKey:nil]; |
| [_bottomShadow addAnimation:bottomAnimations forKey:nil]; |
| |
| // Set the final animation value in to the model layer. |
| _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 - 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) { |
| _bottomShadow.mask = [self shadowLayerMaskForLayer:_bottomShadow]; |
| _topShadow.mask = [self shadowLayerMaskForLayer:_topShadow]; |
| } |
| // 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) { |
| if (self.shadowPath) { |
| _bottomShadow.shadowPath = self.shadowPath; |
| } else { |
| _bottomShadow.shadowPath = [self defaultShadowPath].CGPath; |
| } |
| } |
| if (!_topShadow.shadowPath) { |
| if (self.shadowPath) { |
| _topShadow.shadowPath = self.shadowPath; |
| } else { |
| _topShadow.shadowPath = [self defaultShadowPath].CGPath; |
| } |
| } |
| } |
| |
| @end |