| // Copyright 2018-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 "MDCCard.h" |
| |
| #import "MaterialElevation.h" |
| #import "MaterialInk.h" |
| #import "MaterialRipple.h" |
| #import "MaterialShadowElevations.h" |
| #import "MaterialShadowLayer.h" |
| #import "MaterialShapes.h" |
| #import "MaterialMath.h" |
| |
| static const CGFloat MDCCardShadowElevationNormal = 1; |
| static const CGFloat MDCCardShadowElevationHighlighted = 8; |
| static const CGFloat MDCCardCornerRadiusDefault = 4; |
| static const BOOL MDCCardIsInteractableDefault = YES; |
| |
| @interface MDCCard () |
| @property(nonatomic, readonly, strong) MDCShapedShadowLayer *layer; |
| @end |
| |
| @implementation MDCCard { |
| NSMutableDictionary<NSNumber *, NSNumber *> *_shadowElevations; |
| NSMutableDictionary<NSNumber *, UIColor *> *_shadowColors; |
| NSMutableDictionary<NSNumber *, NSNumber *> *_borderWidths; |
| NSMutableDictionary<NSNumber *, UIColor *> *_borderColors; |
| UIColor *_backgroundColor; |
| CGPoint _lastTouch; |
| } |
| |
| @dynamic layer; |
| @synthesize mdc_overrideBaseElevation = _mdc_overrideBaseElevation; |
| @synthesize mdc_elevationDidChangeBlock = _mdc_elevationDidChangeBlock; |
| |
| + (Class)layerClass { |
| return [MDCShapedShadowLayer class]; |
| } |
| |
| - (instancetype)initWithCoder:(NSCoder *)coder { |
| self = [super initWithCoder:coder]; |
| if (self) { |
| [self commonMDCCardInit]; |
| } |
| return self; |
| } |
| |
| - (instancetype)initWithFrame:(CGRect)frame { |
| self = [super initWithFrame:frame]; |
| if (self) { |
| [self commonMDCCardInit]; |
| } |
| return self; |
| } |
| |
| - (void)commonMDCCardInit { |
| self.layer.cornerRadius = MDCCardCornerRadiusDefault; |
| _interactable = MDCCardIsInteractableDefault; |
| _mdc_overrideBaseElevation = -1; |
| |
| if (_inkView == nil) { |
| #pragma clang diagnostic push |
| #pragma clang diagnostic ignored "-Wdeprecated-declarations" |
| _inkView = [[MDCInkView alloc] initWithFrame:self.bounds]; |
| #pragma clang diagnostic pop |
| _inkView.autoresizingMask = |
| (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); |
| _inkView.usesLegacyInkRipple = NO; |
| _inkView.layer.zPosition = FLT_MAX; |
| [self addSubview:_inkView]; |
| } |
| |
| if (_shadowElevations == nil) { |
| _shadowElevations = [NSMutableDictionary dictionary]; |
| _shadowElevations[@(UIControlStateNormal)] = @(MDCCardShadowElevationNormal); |
| _shadowElevations[@(UIControlStateHighlighted)] = @(MDCCardShadowElevationHighlighted); |
| } |
| |
| if (_shadowColors == nil) { |
| _shadowColors = [NSMutableDictionary dictionary]; |
| _shadowColors[@(UIControlStateNormal)] = UIColor.blackColor; |
| } |
| |
| if (_borderColors == nil) { |
| _borderColors = [NSMutableDictionary dictionary]; |
| } |
| |
| if (_borderWidths == nil) { |
| _borderWidths = [NSMutableDictionary dictionary]; |
| } |
| |
| if (_backgroundColor == nil) { |
| _backgroundColor = UIColor.whiteColor; |
| } |
| |
| [self updateShadowElevation]; |
| [self updateShadowColor]; |
| [self updateBorderWidth]; |
| [self updateBorderColor]; |
| [self updateBackgroundColor]; |
| } |
| |
| - (void)layoutSubviews { |
| [super layoutSubviews]; |
| |
| if (!self.layer.shapeGenerator) { |
| self.layer.shadowPath = [self boundingPath].CGPath; |
| } |
| |
| [self updateShadowColor]; |
| [self updateBackgroundColor]; |
| [self updateBorderColor]; |
| } |
| |
| - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection { |
| [super traitCollectionDidChange:previousTraitCollection]; |
| |
| if (self.traitCollectionDidChangeBlock) { |
| self.traitCollectionDidChangeBlock(self, previousTraitCollection); |
| } |
| } |
| |
| - (void)setCornerRadius:(CGFloat)cornerRadius { |
| self.layer.cornerRadius = cornerRadius; |
| [self setNeedsLayout]; |
| } |
| |
| - (CGFloat)cornerRadius { |
| return self.layer.cornerRadius; |
| } |
| |
| - (UIBezierPath *)boundingPath { |
| CGFloat cornerRadius = self.cornerRadius; |
| return [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:cornerRadius]; |
| } |
| |
| - (MDCShadowElevation)shadowElevationForState:(UIControlState)state { |
| NSNumber *elevation = _shadowElevations[@(state)]; |
| if (state != UIControlStateNormal && elevation == nil) { |
| elevation = _shadowElevations[@(UIControlStateNormal)]; |
| } |
| if (elevation != nil) { |
| return (CGFloat)[elevation doubleValue]; |
| } |
| return 0; |
| } |
| |
| - (void)setShadowElevation:(MDCShadowElevation)shadowElevation forState:(UIControlState)state { |
| _shadowElevations[@(state)] = @(shadowElevation); |
| |
| [self updateShadowElevation]; |
| } |
| |
| - (void)updateShadowElevation { |
| CGFloat elevation = [self shadowElevationForState:self.state]; |
| if (!MDCCGFloatEqual(((MDCShadowLayer *)self.layer).elevation, elevation)) { |
| if (!self.layer.shapeGenerator) { |
| self.layer.shadowPath = [self boundingPath].CGPath; |
| } |
| [(MDCShadowLayer *)self.layer setElevation:elevation]; |
| [self mdc_elevationDidChange]; |
| } |
| } |
| |
| - (void)setBorderWidth:(CGFloat)borderWidth forState:(UIControlState)state { |
| _borderWidths[@(state)] = @(borderWidth); |
| |
| [self updateBorderWidth]; |
| } |
| |
| - (void)updateBorderWidth { |
| CGFloat borderWidth = [self borderWidthForState:self.state]; |
| self.layer.shapedBorderWidth = borderWidth; |
| } |
| |
| - (CGFloat)borderWidthForState:(UIControlState)state { |
| NSNumber *borderWidth = _borderWidths[@(state)]; |
| if (state != UIControlStateNormal && borderWidth == nil) { |
| borderWidth = _borderWidths[@(UIControlStateNormal)]; |
| } |
| if (borderWidth != nil) { |
| return (CGFloat)[borderWidth doubleValue]; |
| } |
| return 0; |
| } |
| |
| - (void)setBorderColor:(UIColor *)borderColor forState:(UIControlState)state { |
| _borderColors[@(state)] = borderColor; |
| |
| [self updateBorderColor]; |
| } |
| |
| - (void)updateBorderColor { |
| UIColor *borderColor = [self borderColorForState:self.state]; |
| self.layer.shapedBorderColor = borderColor; |
| } |
| |
| - (UIColor *)borderColorForState:(UIControlState)state { |
| UIColor *borderColor = _borderColors[@(state)]; |
| if (state != UIControlStateNormal && borderColor == nil) { |
| borderColor = _borderColors[@(UIControlStateNormal)]; |
| } |
| return borderColor; |
| } |
| |
| - (void)setShadowColor:(UIColor *)shadowColor forState:(UIControlState)state { |
| _shadowColors[@(state)] = shadowColor; |
| |
| [self updateShadowColor]; |
| } |
| |
| - (void)updateShadowColor { |
| CGColorRef shadowColor = [self shadowColorForState:self.state].CGColor; |
| self.layer.shadowColor = shadowColor; |
| } |
| |
| - (UIColor *)shadowColorForState:(UIControlState)state { |
| UIColor *shadowColor = _shadowColors[@(state)]; |
| if (state != UIControlStateNormal && shadowColor == nil) { |
| shadowColor = _shadowColors[@(UIControlStateNormal)]; |
| } |
| if (shadowColor != nil) { |
| return shadowColor; |
| } |
| return [UIColor blackColor]; |
| } |
| |
| - (void)setHighlighted:(BOOL)highlighted { |
| // Original logic for changing the state to highlighted. |
| if (self.rippleView == nil) { |
| if (highlighted && !self.highlighted) { |
| [self.inkView startTouchBeganAnimationAtPoint:_lastTouch completion:nil]; |
| } else if (!highlighted && self.highlighted) { |
| [self.inkView startTouchEndedAnimationAtPoint:_lastTouch completion:nil]; |
| } |
| } |
| [super setHighlighted:highlighted]; |
| // Updated logic using Ripple for changing the state to highlighted. |
| if (self.rippleView) { |
| self.rippleView.rippleHighlighted = highlighted; |
| } |
| |
| [self updateShadowElevation]; |
| [self updateBorderColor]; |
| [self updateBorderWidth]; |
| [self updateShadowColor]; |
| } |
| |
| - (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event { |
| BOOL beginTracking = [super beginTrackingWithTouch:touch withEvent:event]; |
| CGPoint location = [touch locationInView:self]; |
| _lastTouch = location; |
| return beginTracking; |
| } |
| |
| - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { |
| UIView *result = [super hitTest:point withEvent:event]; |
| if (!_interactable && result == self) { |
| return nil; |
| } |
| if (self.layer.shapeGenerator) { |
| if (!CGPathContainsPoint(self.layer.shapeLayer.path, nil, point, true)) { |
| return nil; |
| } |
| } |
| return result; |
| } |
| |
| - (void)setShapeGenerator:(id<MDCShapeGenerating>)shapeGenerator { |
| if (shapeGenerator) { |
| self.layer.shadowPath = nil; |
| } else { |
| self.layer.shadowPath = [self boundingPath].CGPath; |
| } |
| |
| self.layer.shapeGenerator = shapeGenerator; |
| self.layer.shadowMaskEnabled = NO; |
| [self updateBackgroundColor]; |
| // Original logic for configuring Ink prior to the Ripple integration. |
| if (self.rippleView == nil) { |
| [self updateInkForShape]; |
| } |
| } |
| |
| - (id<MDCShapeGenerating>)shapeGenerator { |
| return self.layer.shapeGenerator; |
| } |
| |
| - (void)updateInkForShape { |
| CGRect boundingBox = CGPathGetBoundingBox(self.layer.shapeLayer.path); |
| self.inkView.maxRippleRadius = |
| (CGFloat)(hypot(CGRectGetHeight(boundingBox), CGRectGetWidth(boundingBox)) / 2 + 10); |
| self.inkView.layer.masksToBounds = NO; |
| } |
| |
| - (void)setBackgroundColor:(UIColor *)backgroundColor { |
| _backgroundColor = backgroundColor; |
| [self updateBackgroundColor]; |
| } |
| |
| - (UIColor *)backgroundColor { |
| return _backgroundColor; |
| } |
| |
| - (void)updateBackgroundColor { |
| self.layer.shapedBackgroundColor = _backgroundColor; |
| } |
| |
| - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { |
| if (self.rippleView) { |
| [self.rippleView touchesBegan:touches withEvent:event]; |
| } |
| [super touchesBegan:touches withEvent:event]; |
| } |
| |
| - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { |
| // The ripple invocation must come before touchesMoved of the super, otherwise the setHighlighted |
| // of the UIControl will be triggered before the ripple identifies that the highlighted was |
| // trigerred from a long press entering the view and shouldn't invoke a ripple. |
| if (self.rippleView) { |
| [self.rippleView touchesMoved:touches withEvent:event]; |
| } |
| [super touchesMoved:touches withEvent:event]; |
| } |
| |
| - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { |
| if (self.rippleView) { |
| [self.rippleView touchesEnded:touches withEvent:event]; |
| } |
| [super touchesEnded:touches withEvent:event]; |
| } |
| |
| - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { |
| if (self.rippleView) { |
| [self.rippleView touchesCancelled:touches withEvent:event]; |
| } |
| [super touchesCancelled:touches withEvent:event]; |
| } |
| |
| - (void)setEnableRippleBehavior:(BOOL)enableRippleBehavior { |
| if (enableRippleBehavior == _enableRippleBehavior) { |
| return; |
| } |
| _enableRippleBehavior = enableRippleBehavior; |
| if (enableRippleBehavior) { |
| if (_rippleView == nil) { |
| _rippleView = [[MDCStatefulRippleView alloc] initWithFrame:self.bounds]; |
| _rippleView.layer.zPosition = FLT_MAX; |
| [self addSubview:_rippleView]; |
| } |
| if (_inkView) { |
| [_inkView removeFromSuperview]; |
| _inkView = nil; |
| } |
| } else { |
| if (_rippleView) { |
| [_rippleView removeFromSuperview]; |
| _rippleView = nil; |
| } |
| [self addSubview:_inkView]; |
| } |
| } |
| |
| - (CGFloat)mdc_currentElevation { |
| return [self shadowElevationForState:self.state]; |
| } |
| |
| @end |