| // 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 "MDCFeatureHighlightView+Private.h" |
| |
| #import <MDFTextAccessibility/MDFTextAccessibility.h> |
| #import "MDCFeatureHighlightDismissGestureRecognizer.h" |
| #import "MDCFeatureHighlightLayer.h" |
| |
| #import "MaterialAvailability.h" |
| #import "MaterialMath.h" |
| #import "MaterialTypography.h" |
| |
| static inline CGFloat CGPointDistanceToPoint(CGPoint a, CGPoint b) { |
| return hypot(a.x - b.x, a.y - b.y); |
| } |
| |
| const CGFloat kMDCFeatureHighlightMinimumInnerRadius = 44; |
| const CGFloat kMDCFeatureHighlightInnerContentPadding = 10; |
| const CGFloat kMDCFeatureHighlightInnerPadding = 20; |
| const CGFloat kMDCFeatureHighlightTextPadding = 40; |
| const CGFloat kMDCFeatureHighlightTextMaxWidth = 300; |
| const CGFloat kMDCFeatureHighlightConcentricBound = 88; |
| const CGFloat kMDCFeatureHighlightNonconcentricOffset = 20; |
| const CGFloat kMDCFeatureHighlightMaxTextHeight = 1000; |
| const CGFloat kMDCFeatureHighlightTitleBodyBaselineOffset = 32; |
| const CGFloat kMDCFeatureHighlightOuterHighlightAlpha = (CGFloat)0.96; |
| |
| const CGFloat kMDCFeatureHighlightGestureDisappearThresh = (CGFloat)0.9; |
| const CGFloat kMDCFeatureHighlightGestureAppearThresh = (CGFloat)0.95; |
| const CGFloat kMDCFeatureHighlightGestureDismissThresh = (CGFloat)0.85; |
| const CGFloat kMDCFeatureHighlightGestureAnimationDuration = (CGFloat)0.2; |
| |
| const CGFloat kMDCFeatureHighlightDismissAnimationDuration = (CGFloat)0.25; |
| |
| // Animation consts |
| const CGFloat kMDCFeatureHighlightInnerRadiusFactor = (CGFloat)1.1; |
| const CGFloat kMDCFeatureHighlightOuterRadiusFactor = (CGFloat)1.125; |
| const CGFloat kMDCFeatureHighlightPulseRadiusFactor = 2; |
| const CGFloat kMDCFeatureHighlightPulseStartAlpha = (CGFloat)0.54; |
| const CGFloat kMDCFeatureHighlightInnerRadiusBloomAmount = |
| (kMDCFeatureHighlightInnerRadiusFactor - 1) * kMDCFeatureHighlightMinimumInnerRadius; |
| const CGFloat kMDCFeatureHighlightPulseRadiusBloomAmount = |
| (kMDCFeatureHighlightPulseRadiusFactor - 1) * kMDCFeatureHighlightMinimumInnerRadius; |
| |
| static const MDCFontTextStyle kTitleTextStyle = MDCFontTextStyleTitle; |
| static const MDCFontTextStyle kBodyTextStyle = MDCFontTextStyleSubheadline; |
| |
| static inline CGPoint CGPointAddedToPoint(CGPoint a, CGPoint b) { |
| return CGPointMake(a.x + b.x, a.y + b.y); |
| } |
| |
| @implementation MDCFeatureHighlightView { |
| BOOL _forceConcentricLayout; |
| UIView *_highlightView; |
| CGPoint _highlightPoint; |
| CGFloat _innerRadius; |
| CGPoint _outerCenter; |
| CGFloat _outerRadius; |
| CGFloat _outerRadiusScale; |
| BOOL _isLayedOutAppearing; |
| MDCFeatureHighlightLayer *_outerLayer; |
| MDCFeatureHighlightLayer *_pulseLayer; |
| MDCFeatureHighlightLayer *_innerLayer; |
| MDCFeatureHighlightLayer *_displayMaskLayer; |
| UIButton *_accessibilityView; |
| |
| // This view is a hack to work around UIKit calling our animation completion blocks immediately if |
| // there is no UIKit content being animated. Since our appearance and disappearance animations are |
| // mostly CAAnimations, we need to guarantee there will be a UIKit animation occuring in order to |
| // ensure we always see the full CAAnimations before the completion blocks are called. |
| UIView *_dummyAnimatedView; |
| } |
| |
| @synthesize highlightRadius = _outerRadius; |
| @synthesize adjustsFontForContentSizeCategory = _adjustsFontForContentSizeCategory; |
| |
| - (instancetype)initWithFrame:(CGRect)frame { |
| if (self = [super initWithFrame:frame]) { |
| self.backgroundColor = [UIColor clearColor]; |
| |
| _dummyAnimatedView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 1, 1)]; |
| _dummyAnimatedView.backgroundColor = [UIColor clearColor]; |
| [self addSubview:_dummyAnimatedView]; |
| |
| _outerLayer = [[MDCFeatureHighlightLayer alloc] init]; |
| [self.layer addSublayer:_outerLayer]; |
| |
| _pulseLayer = [[MDCFeatureHighlightLayer alloc] init]; |
| [self.layer addSublayer:_pulseLayer]; |
| |
| _innerLayer = [[MDCFeatureHighlightLayer alloc] init]; |
| [self.layer addSublayer:_innerLayer]; |
| |
| _displayMaskLayer = [[MDCFeatureHighlightLayer alloc] init]; |
| _displayMaskLayer.fillColor = [UIColor whiteColor].CGColor; |
| |
| // Tiny frame just inside the bounds so that non-accessibility interactions aren't affected. |
| _accessibilityView = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 1, 1)]; |
| _accessibilityView.autoresizingMask = UIViewAutoresizingNone; |
| _accessibilityView.accessibilityLabel = @"Dismiss"; |
| // Note: The following is not strictly required, but is expected in unit tests. |
| _accessibilityView.isAccessibilityElement = YES; |
| [self addSubview:_accessibilityView]; |
| [self sendSubviewToBack:_accessibilityView]; |
| |
| _titleLabel = [[UILabel alloc] initWithFrame:CGRectZero]; |
| _titleLabel.textAlignment = NSTextAlignmentNatural; |
| _titleLabel.lineBreakMode = NSLineBreakByTruncatingTail; |
| _titleLabel.numberOfLines = 0; |
| [self addSubview:_titleLabel]; |
| |
| _bodyLabel = [[UILabel alloc] initWithFrame:CGRectZero]; |
| _bodyLabel.shadowColor = nil; |
| _bodyLabel.shadowOffset = CGSizeZero; |
| _bodyLabel.textAlignment = NSTextAlignmentNatural; |
| _bodyLabel.lineBreakMode = NSLineBreakByTruncatingTail; |
| _bodyLabel.numberOfLines = 0; |
| [self addSubview:_bodyLabel]; |
| |
| UITapGestureRecognizer *tapRecognizer = |
| [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTapView:)]; |
| tapRecognizer.delegate = self; |
| [self addGestureRecognizer:tapRecognizer]; |
| |
| MDCFeatureHighlightDismissGestureRecognizer *panRecognizer = |
| [[MDCFeatureHighlightDismissGestureRecognizer alloc] |
| initWithTarget:self |
| action:@selector(didGestureDismiss:)]; |
| panRecognizer.cancelsTouchesInView = NO; |
| [self addGestureRecognizer:panRecognizer]; |
| |
| // We want the inner and outer highlights to animate from the same origin so we start them from |
| // a concentric position. |
| _forceConcentricLayout = YES; |
| [self applyMDCFeatureHighlightViewDefaults]; |
| |
| _outerRadiusScale = 1.0; |
| } |
| return self; |
| } |
| |
| - (void)dealloc { |
| // TODO(#2651): Remove once we move to iOS8 |
| // Remove Dynamic Type contentSizeCategoryDidChangeNotification |
| [[NSNotificationCenter defaultCenter] removeObserver:self |
| name:UIContentSizeCategoryDidChangeNotification |
| object:nil]; |
| } |
| |
| - (void)applyMDCFeatureHighlightViewDefaults { |
| _outerHighlightColor = [self MDCFeatureHighlightDefaultOuterHighlightColor]; |
| _innerHighlightColor = [self MDCFeatureHighlightDefaultInnerHighlightColor]; |
| } |
| |
| - (UIColor *)MDCFeatureHighlightDefaultOuterHighlightColor { |
| return [[UIColor blueColor] colorWithAlphaComponent:kMDCFeatureHighlightOuterHighlightAlpha]; |
| } |
| |
| - (UIColor *)MDCFeatureHighlightDefaultInnerHighlightColor { |
| return [UIColor whiteColor]; |
| } |
| |
| - (void)setOuterHighlightColor:(UIColor *)outerHighlightColor { |
| if (!outerHighlightColor) { |
| outerHighlightColor = [self MDCFeatureHighlightDefaultOuterHighlightColor]; |
| } |
| _outerHighlightColor = outerHighlightColor; |
| _outerLayer.fillColor = _outerHighlightColor.CGColor; |
| } |
| |
| - (void)setTitleFont:(UIFont *)titleFont { |
| _titleFont = titleFont; |
| |
| [self updateTitleFont]; |
| } |
| |
| - (void)updateTitleFont { |
| if (!_titleFont) { |
| _titleFont = [MDCFeatureHighlightView defaultTitleFont]; |
| } |
| _titleLabel.font = _titleFont; |
| |
| if (_titleLabel.attributedText) { |
| NSMutableAttributedString *attributedString = [_titleLabel.attributedText mutableCopy]; |
| [self setFont:_titleFont forAttributedString:attributedString]; |
| _titleLabel.attributedText = attributedString; |
| } |
| |
| [self setNeedsLayout]; |
| } |
| |
| - (void)setTitleColor:(UIColor *)titleColor { |
| _titleColor = titleColor; |
| |
| _titleLabel.textColor = titleColor; |
| } |
| |
| - (void)setBodyFont:(UIFont *)bodyFont { |
| _bodyFont = bodyFont; |
| |
| [self updateBodyFont]; |
| } |
| |
| - (void)updateBodyFont { |
| if (!_bodyFont) { |
| _bodyFont = [MDCFeatureHighlightView defaultBodyFont]; |
| } |
| _bodyLabel.font = _bodyFont; |
| |
| if (_bodyLabel.attributedText) { |
| NSMutableAttributedString *attributedString = [_bodyLabel.attributedText mutableCopy]; |
| [self setFont:_bodyFont forAttributedString:attributedString]; |
| _bodyLabel.attributedText = attributedString; |
| } |
| |
| [self setNeedsLayout]; |
| } |
| |
| - (void)setBodyColor:(UIColor *)bodyColor { |
| _bodyColor = bodyColor; |
| |
| _bodyLabel.textColor = bodyColor; |
| } |
| |
| + (UIFont *)defaultBodyFont { |
| if ([MDCTypography.fontLoader isKindOfClass:[MDCSystemFontLoader class]]) { |
| return [UIFont mdc_standardFontForMaterialTextStyle:kBodyTextStyle]; |
| } |
| return [MDCTypography body1Font]; |
| } |
| |
| + (UIFont *)defaultTitleFont { |
| if ([MDCTypography.fontLoader isKindOfClass:[MDCSystemFontLoader class]]) { |
| return [UIFont mdc_standardFontForMaterialTextStyle:kTitleTextStyle]; |
| } |
| return [MDCTypography titleFont]; |
| } |
| |
| - (void)setInnerHighlightColor:(UIColor *)innerHighlightColor { |
| if (!innerHighlightColor) { |
| innerHighlightColor = [self MDCFeatureHighlightDefaultInnerHighlightColor]; |
| } |
| _innerHighlightColor = innerHighlightColor; |
| |
| _pulseLayer.fillColor = _innerHighlightColor.CGColor; |
| _innerLayer.fillColor = _innerHighlightColor.CGColor; |
| } |
| |
| - (void)layoutAppearing { |
| _isLayedOutAppearing = YES; |
| |
| // TODO: Mask the labels during the presentation and dismissal animations. |
| _titleLabel.alpha = 1; |
| _bodyLabel.alpha = 1; |
| |
| // Guarantee something changes in case the label alphas are already 1.0 |
| _dummyAnimatedView.frame = CGRectOffset(_dummyAnimatedView.frame, 1, 0); |
| } |
| |
| - (void)layoutDisappearing { |
| _isLayedOutAppearing = NO; |
| |
| _titleLabel.alpha = 0; |
| _bodyLabel.alpha = 0; |
| |
| // Guarantee something changes in case the label alphas are already 0.0 |
| _dummyAnimatedView.frame = CGRectOffset(_dummyAnimatedView.frame, 1, 0); |
| } |
| |
| - (void)setDisplayedView:(UIView *)displayedView { |
| CGSize displayedSize = displayedView.frame.size; |
| CGFloat viewRadius = |
| (CGFloat)sqrt(pow(displayedSize.width / 2, 2) + pow(displayedSize.height / 2, 2)); |
| viewRadius += kMDCFeatureHighlightInnerContentPadding; |
| _innerRadius = MAX(viewRadius, kMDCFeatureHighlightMinimumInnerRadius); |
| |
| _displayedView.layer.mask = nil; |
| [_displayedView removeFromSuperview]; |
| _displayedView = displayedView; |
| [self addSubview:_displayedView]; |
| _displayedView.layer.mask = _displayMaskLayer; |
| } |
| |
| - (NSArray *)accessibilityElements { |
| if (_displayedView) { |
| return @[ _titleLabel, _bodyLabel, _displayedView, _accessibilityView ]; |
| } |
| return @[ _titleLabel, _bodyLabel, _accessibilityView ]; |
| } |
| |
| - (void)setHighlightPoint:(CGPoint)highlightPoint { |
| _highlightPoint = highlightPoint; |
| |
| [self setNeedsLayout]; |
| } |
| |
| - (void)layoutSubviews { |
| [_innerLayer removeAllAnimations]; |
| [_outerLayer removeAllAnimations]; |
| [_pulseLayer removeAllAnimations]; |
| |
| BOOL leftHalf = _highlightPoint.x < self.frame.size.width / 2; |
| BOOL topHalf = _highlightPoint.y < self.frame.size.height / 2; |
| |
| CGFloat textWidth = MIN(self.frame.size.width - 2 * kMDCFeatureHighlightTextPadding, |
| kMDCFeatureHighlightTextMaxWidth); |
| CGSize titleSize = |
| [_titleLabel sizeThatFits:CGSizeMake(textWidth, kMDCFeatureHighlightMaxTextHeight)]; |
| CGSize detailSize = |
| [_bodyLabel sizeThatFits:CGSizeMake(textWidth, kMDCFeatureHighlightMaxTextHeight)]; |
| titleSize.width = (CGFloat)ceil(MAX(titleSize.width, detailSize.width)); |
| detailSize.width = titleSize.width; |
| |
| CGFloat textVerticalPadding = 0; |
| CGFloat textPaddingAbove = _titleLabel.font.descender; |
| CGFloat textPaddingBelow = _bodyLabel.font.ascender - textPaddingAbove; |
| if (titleSize.height > 0 && detailSize.height > 0) { |
| textVerticalPadding = kMDCFeatureHighlightTitleBodyBaselineOffset - textPaddingBelow; |
| } |
| |
| CGFloat textHeight = titleSize.height + detailSize.height + textVerticalPadding; |
| |
| if ((_highlightPoint.y <= kMDCFeatureHighlightConcentricBound) || |
| (_highlightPoint.y >= self.frame.size.height - kMDCFeatureHighlightConcentricBound)) { |
| _highlightCenter = _highlightPoint; |
| } else { |
| if (topHalf) { |
| _highlightCenter.y = _highlightPoint.y + _innerRadius + textHeight / 2; |
| } else { |
| _highlightCenter.y = _highlightPoint.y - _innerRadius - textHeight / 2; |
| } |
| if (leftHalf) { |
| _highlightCenter.x = _highlightPoint.x + kMDCFeatureHighlightNonconcentricOffset; |
| } else { |
| _highlightCenter.x = _highlightPoint.x - kMDCFeatureHighlightNonconcentricOffset; |
| } |
| } |
| |
| CGPoint outerCenter = _forceConcentricLayout ? _highlightPoint : _highlightCenter; |
| if (self.layer.animationKeys) { |
| // If our layer has an animationKeys array then we must be inside an animation (because we're |
| // resizing or rotating), so we want to use the current animation's properties for our various |
| // layers' CAAnimations. |
| CAAnimation *animation = [self.layer animationForKey:self.layer.animationKeys.firstObject]; |
| [CATransaction begin]; |
| [CATransaction setAnimationTimingFunction:animation.timingFunction]; |
| [CATransaction setAnimationDuration:animation.duration]; |
| [_innerLayer setPosition:_highlightPoint animated:YES]; |
| [_pulseLayer setPosition:_highlightPoint animated:YES]; |
| [_outerLayer setPosition:outerCenter animated:YES]; |
| [CATransaction commit]; |
| } else { |
| _innerLayer.position = _highlightPoint; |
| _pulseLayer.position = _highlightPoint; |
| _outerLayer.position = outerCenter; |
| } |
| _displayedView.center = _highlightPoint; |
| |
| CGFloat leftTextBound = kMDCFeatureHighlightTextPadding; |
| CGFloat rightTextBound = self.frame.size.width - MAX(titleSize.width, detailSize.width) - |
| kMDCFeatureHighlightTextPadding; |
| CGPoint titlePos = CGPointMake(0, 0); |
| titlePos.x = MIN(MAX(_highlightCenter.x - textWidth / 2, leftTextBound), rightTextBound); |
| if (topHalf) { |
| titlePos.y = _highlightPoint.y + kMDCFeatureHighlightInnerPadding + _innerRadius; |
| } else { |
| titlePos.y = _highlightPoint.y - kMDCFeatureHighlightInnerPadding - _innerRadius - textHeight; |
| } |
| |
| CGRect titleFrame = |
| MDCRectAlignToScale((CGRect){titlePos, titleSize}, [UIScreen mainScreen].scale); |
| _titleLabel.frame = titleFrame; |
| |
| CGFloat detailPositionY = (CGFloat)ceil(CGRectGetMaxY(titleFrame) + textVerticalPadding); |
| CGRect detailFrame = (CGRect){CGPointMake(titlePos.x, detailPositionY), detailSize}; |
| _bodyLabel.frame = detailFrame; |
| |
| // Calculating the radius required for a circle centered at _highlightCenter that fully encircles |
| // both labels. |
| CGRect textFrames = CGRectUnion(_titleLabel.frame, _bodyLabel.frame); |
| CGFloat distX = ABS(CGRectGetMidX(textFrames) - _highlightCenter.x) + textFrames.size.width / 2; |
| CGFloat distY = ABS(CGRectGetMidY(textFrames) - _highlightCenter.y) + textFrames.size.height / 2; |
| CGFloat minTextRadius = |
| (CGFloat)(sqrt(pow(distX, 2) + pow(distY, 2)) + kMDCFeatureHighlightTextPadding); |
| |
| // Calculating the radius required for a circle centered at _highlightCenter that fully encircles |
| // the inner highlight. |
| distX = ABS(_highlightCenter.x - _highlightPoint.x); |
| distY = ABS(_highlightCenter.y - _highlightPoint.y); |
| CGFloat minInnerHighlightRadius = (CGFloat)(sqrt(pow(distX, 2) + pow(distY, 2)) + _innerRadius + |
| kMDCFeatureHighlightInnerPadding); |
| |
| // Use the larger of the two radii to ensure everything is encircled. |
| _outerRadius = MAX(minTextRadius, minInnerHighlightRadius); |
| |
| // To support dynamic color |
| _pulseLayer.fillColor = _innerHighlightColor.CGColor; |
| _innerLayer.fillColor = _innerHighlightColor.CGColor; |
| _outerLayer.fillColor = _outerHighlightColor.CGColor; |
| |
| _accessibilityView.accessibilityFrame = self.bounds; |
| } |
| |
| - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection { |
| [super traitCollectionDidChange:previousTraitCollection]; |
| |
| if (self.traitCollectionDidChangeBlock) { |
| self.traitCollectionDidChangeBlock(self, previousTraitCollection); |
| } |
| } |
| |
| - (void)didTapView:(UITapGestureRecognizer *)tapGestureRecognizer { |
| BOOL hasVoiceOverFocusOnDismissView = |
| UIAccessibilityIsVoiceOverRunning() && [_accessibilityView accessibilityElementIsFocused]; |
| if (self.interactionBlock && hasVoiceOverFocusOnDismissView) { |
| // Early return when the tap happens on the _accessibilityView as its accessibilityFrame |
| // (full-screen) should not be used for the position based calculation below. |
| self.interactionBlock(NO); |
| return; |
| } |
| |
| CGPoint pos = [tapGestureRecognizer locationInView:self]; |
| CGFloat pointDist = CGPointDistanceToPoint(_highlightPoint, pos); |
| CGFloat centerDist = CGPointDistanceToPoint(_highlightCenter, pos); |
| |
| if (self.interactionBlock) { |
| if (centerDist > _outerRadius * _outerRadiusScale) { |
| // For taps outside the outer highlight, dismiss as not accepted |
| self.interactionBlock(NO); |
| } else if (pointDist < _innerRadius) { |
| // For taps inside the inner highlight, dismiss as accepted |
| self.interactionBlock(YES); |
| } |
| } |
| } |
| |
| - (void)didGestureDismiss:(MDCFeatureHighlightDismissGestureRecognizer *)dismissRecognizer { |
| CGFloat progress = dismissRecognizer.progress; |
| switch (dismissRecognizer.state) { |
| case UIGestureRecognizerStateChanged: |
| [self layoutInProgressDismissal:progress]; |
| break; |
| |
| case UIGestureRecognizerStateEnded: |
| if (progress > kMDCFeatureHighlightGestureDismissThresh) { |
| [self animateDismissalCancelled]; |
| } else { |
| if (self.interactionBlock) { |
| self.interactionBlock(NO); |
| } |
| } |
| break; |
| |
| case UIGestureRecognizerStateCancelled: |
| case UIGestureRecognizerStateFailed: |
| [self animateDismissalCancelled]; |
| break; |
| |
| case UIGestureRecognizerStatePossible: |
| break; |
| |
| case UIGestureRecognizerStateBegan: |
| break; |
| } |
| } |
| |
| - (void)layoutInProgressDismissal:(CGFloat)progress { |
| _outerRadiusScale = progress; |
| [self updateOuterHighlight]; |
| |
| // Square progress to ease-in the translation |
| CGFloat translationProgress = (1 - progress * progress); |
| CGPoint pointOffset = CGPointMake((_highlightPoint.x - _highlightCenter.x) * translationProgress, |
| (_highlightPoint.y - _highlightCenter.y) * translationProgress); |
| CGPoint center = CGPointAddedToPoint(_highlightCenter, pointOffset); |
| [_outerLayer setPosition:center animated:NO]; |
| [_outerLayer removeAllAnimations]; |
| |
| if (_isLayedOutAppearing) { |
| if (progress < kMDCFeatureHighlightGestureDisappearThresh) { |
| [UIView animateWithDuration:kMDCFeatureHighlightGestureAnimationDuration |
| animations:^{ |
| [self layoutDisappearing]; |
| }]; |
| } |
| } else if (progress > kMDCFeatureHighlightGestureAppearThresh) { |
| [UIView animateWithDuration:kMDCFeatureHighlightGestureAnimationDuration |
| animations:^{ |
| [self layoutAppearing]; |
| }]; |
| } |
| } |
| |
| - (void)animateDismissalCancelled { |
| [UIView animateWithDuration:kMDCFeatureHighlightGestureAnimationDuration |
| animations:^{ |
| [self layoutAppearing]; |
| }]; |
| |
| _outerRadiusScale = 1; |
| [CATransaction begin]; |
| [CATransaction setAnimationTimingFunction:[CAMediaTimingFunction |
| functionWithName:kCAMediaTimingFunctionEaseOut]]; |
| [CATransaction setAnimationDuration:kMDCFeatureHighlightDismissAnimationDuration]; |
| [_outerLayer setRadius:_outerRadius * _outerRadiusScale animated:YES]; |
| [_outerLayer setPosition:_highlightCenter animated:YES]; |
| [CATransaction commit]; |
| } |
| |
| - (void)animateDiscover:(NSTimeInterval)duration { |
| [_innerLayer setFillColor:[_innerHighlightColor colorWithAlphaComponent:0].CGColor]; |
| [_outerLayer setFillColor:[_outerHighlightColor colorWithAlphaComponent:0].CGColor]; |
| |
| CGPoint displayMaskCenter = |
| CGPointMake(_displayedView.frame.size.width / 2, _displayedView.frame.size.height / 2); |
| |
| [_displayMaskLayer setPosition:displayMaskCenter]; |
| [_innerLayer setPosition:_highlightPoint]; |
| [_pulseLayer setPosition:_highlightPoint]; |
| [_outerLayer setPosition:_highlightPoint]; |
| [_outerLayer setRadius:0.0 animated:NO]; |
| |
| [CATransaction begin]; |
| [CATransaction setAnimationTimingFunction:[CAMediaTimingFunction |
| functionWithName:kCAMediaTimingFunctionEaseOut]]; |
| [CATransaction setAnimationDuration:duration]; |
| [_displayMaskLayer setRadius:_innerRadius animated:YES]; |
| [_innerLayer setFillColor:[_innerHighlightColor colorWithAlphaComponent:1].CGColor animated:YES]; |
| [_innerLayer setRadius:_innerRadius animated:YES]; |
| [_outerLayer setFillColor:_outerHighlightColor.CGColor animated:YES]; |
| [_outerLayer setPosition:_highlightCenter animated:YES]; |
| [_outerLayer setRadius:_outerRadius animated:YES]; |
| [CATransaction commit]; |
| |
| _forceConcentricLayout = NO; |
| } |
| |
| - (void)animatePulse { |
| NSArray *keyTimes = @[ @0, @0.5, @1 ]; |
| __block id pulseColorStart; |
| __block id pulseColorEnd; |
| #if MDC_AVAILABLE_SDK_IOS(13_0) |
| [self.traitCollection performAsCurrentTraitCollection:^{ |
| pulseColorStart = |
| (__bridge id) |
| [self.innerHighlightColor colorWithAlphaComponent:kMDCFeatureHighlightPulseStartAlpha] |
| .CGColor; |
| pulseColorEnd = (__bridge id)[self.innerHighlightColor colorWithAlphaComponent:0].CGColor; |
| }]; |
| #else |
| pulseColorStart = |
| (__bridge id) |
| [_innerHighlightColor colorWithAlphaComponent:kMDCFeatureHighlightPulseStartAlpha] |
| .CGColor; |
| pulseColorEnd = (__bridge id)[_innerHighlightColor colorWithAlphaComponent:0].CGColor; |
| #endif // MDC_AVAILABLE_SDK_IOS(13_0) |
| |
| CGFloat radius = _innerRadius; |
| |
| [CATransaction begin]; |
| [CATransaction setAnimationDuration:1]; |
| [CATransaction setAnimationTimingFunction:[CAMediaTimingFunction |
| functionWithName:kCAMediaTimingFunctionEaseOut]]; |
| CGFloat innerBloomRadius = radius + kMDCFeatureHighlightInnerRadiusBloomAmount; |
| CGFloat pulseBloomRadius = radius + kMDCFeatureHighlightPulseRadiusBloomAmount; |
| NSArray *innerKeyframes = @[ @(radius), @(innerBloomRadius), @(radius) ]; |
| [_innerLayer animateRadiusOverKeyframes:innerKeyframes keyTimes:keyTimes]; |
| NSArray *pulseKeyframes = @[ @(radius), @(radius), @(pulseBloomRadius) ]; |
| [_pulseLayer animateRadiusOverKeyframes:pulseKeyframes keyTimes:keyTimes]; |
| [_pulseLayer animateFillColorOverKeyframes:@[ pulseColorStart, pulseColorStart, pulseColorEnd ] |
| keyTimes:keyTimes]; |
| [CATransaction commit]; |
| } |
| |
| - (void)animateAccepted:(NSTimeInterval)duration { |
| CGPoint displayMaskCenter = |
| CGPointMake(_displayedView.frame.size.width / 2, _displayedView.frame.size.height / 2); |
| |
| [CATransaction begin]; |
| [CATransaction setAnimationTimingFunction:[CAMediaTimingFunction |
| functionWithName:kCAMediaTimingFunctionEaseOut]]; |
| [CATransaction setAnimationDuration:duration]; |
| [_displayMaskLayer setPosition:displayMaskCenter animated:YES]; |
| [_displayMaskLayer setRadius:0.0 animated:YES]; |
| [_innerLayer setPosition:_highlightPoint animated:YES]; |
| [_innerLayer setRadius:0.0 animated:YES]; |
| [_outerLayer setFillColor:[_outerHighlightColor colorWithAlphaComponent:0].CGColor animated:YES]; |
| [_outerLayer setPosition:_highlightCenter animated:YES]; |
| [_outerLayer setRadius:kMDCFeatureHighlightOuterRadiusFactor * _outerRadius animated:YES]; |
| [CATransaction commit]; |
| |
| _forceConcentricLayout = YES; |
| } |
| |
| - (void)animateRejected:(NSTimeInterval)duration { |
| CGPoint displayMaskCenter = |
| CGPointMake(_displayedView.frame.size.width / 2, _displayedView.frame.size.height / 2); |
| |
| [CATransaction begin]; |
| [CATransaction setAnimationTimingFunction:[CAMediaTimingFunction |
| functionWithName:kCAMediaTimingFunctionEaseOut]]; |
| [CATransaction setAnimationDuration:duration]; |
| [_displayMaskLayer setPosition:displayMaskCenter animated:YES]; |
| [_displayMaskLayer setRadius:0 animated:YES]; |
| [_innerLayer setPosition:_highlightPoint animated:YES]; |
| [_innerLayer setRadius:0 animated:YES]; |
| [_outerLayer setFillColor:[_outerHighlightColor colorWithAlphaComponent:0].CGColor animated:YES]; |
| [_outerLayer setPosition:_highlightPoint animated:YES]; |
| [_outerLayer setRadius:0 animated:YES]; |
| [CATransaction commit]; |
| |
| _forceConcentricLayout = NO; |
| } |
| |
| - (void)updateOuterHighlight { |
| CGFloat scaledRadius = _outerRadius * _outerRadiusScale; |
| if (self.layer.animationKeys) { |
| CAAnimation *animation = [self.layer animationForKey:self.layer.animationKeys.firstObject]; |
| [CATransaction begin]; |
| [CATransaction setAnimationTimingFunction:animation.timingFunction]; |
| [CATransaction setAnimationDuration:animation.duration]; |
| [_outerLayer setRadius:scaledRadius animated:YES]; |
| [CATransaction commit]; |
| } else { |
| [_outerLayer setRadius:scaledRadius animated:NO]; |
| } |
| } |
| |
| - (UIButton *)accessibilityDismissView { |
| return _accessibilityView; |
| } |
| |
| #pragma mark - Dynamic Type Support |
| - (void)setAdjustsFontForContentSizeCategory:(BOOL)adjustsFontForContentSizeCategory { |
| _adjustsFontForContentSizeCategory = adjustsFontForContentSizeCategory; |
| self.titleLabel.adjustsFontForContentSizeCategory = adjustsFontForContentSizeCategory; |
| self.bodyLabel.adjustsFontForContentSizeCategory = adjustsFontForContentSizeCategory; |
| } |
| |
| // Handles UIContentSizeCategoryDidChangeNotifications |
| - (void)contentSizeCategoryDidChange:(__unused NSNotification *)notification { |
| [self updateTitleFont]; |
| [self updateBodyFont]; |
| } |
| |
| - (void)setFont:(UIFont *)font forAttributedString:(NSMutableAttributedString *)attributedString { |
| [attributedString beginEditing]; |
| NSRange range = NSMakeRange(0, attributedString.length); |
| [attributedString removeAttribute:NSFontAttributeName range:range]; |
| [attributedString addAttribute:NSFontAttributeName value:font range:range]; |
| [attributedString endEditing]; |
| } |
| |
| #pragma mark - UIGestureRecognizerDelegate (Tap) |
| |
| - (BOOL)gestureRecognizer:(__unused UIGestureRecognizer *)gestureRecognizer |
| shouldRecognizeSimultaneouslyWithGestureRecognizer: |
| (__unused UIGestureRecognizer *)otherGestureRecognizer { |
| return YES; |
| } |
| |
| #pragma mark - UIAccessibility |
| |
| - (void)setAccessibilityHint:(NSString *)accessibilityHint { |
| _accessibilityView.accessibilityHint = accessibilityHint; |
| } |
| |
| - (NSString *)accessibilityHint { |
| return _accessibilityView.accessibilityHint; |
| } |
| |
| @end |