blob: befc7bbbbb96229a04e58bd41f1f78e3085a3cb3 [file] [log] [blame] [edit]
// Copyright 2016-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 "MDCProgressView.h"
#import <CoreGraphics/CoreGraphics.h>
#import <QuartzCore/QuartzCore.h>
#import <UIKit/UIKit.h>
#include <tgmath.h>
#import "MDCPalettes.h"
#import "MDCProgressGradientView.h"
#import "MDCProgressLayerView.h"
#import "MaterialProgressViewStrings.h"
#import "MaterialProgressViewStrings_table.h"
#import "MDCMath.h"
#import <MDFInternationalization/MDFRTL.h>
NS_ASSUME_NONNULL_BEGIN
static inline UIColor *MDCProgressViewDefaultTintColor(void) {
return MDCPalette.bluePalette.tint500;
}
// The ratio by which to desaturate the progress tint color to obtain the default track tint color.
static const CGFloat MDCProgressViewTrackColorDesaturation = (CGFloat)0.3;
static const NSTimeInterval MDCProgressViewAnimationDuration = 0.25;
// Used by the NTC Determinate branch.
static const CGFloat MDCProgressViewGapWidth = 4.0;
// This is the total duration from 0 to 1. A proportional amount of this duration is calculated
// based on the difference between two progress intervals. Used by the NTC Determinate branch.
static const CGFloat MDCProgressViewDeterminateDuration = 1.6;
static NSString *const kProgressViewLayerAnimationKey = @"kProgressViewLayerAnimation";
static NSString *const kProgressViewGapAnimationKey = @"kProgressViewGapAnimation";
// The Bundle for string resources.
static NSString *const kBundle = @"MaterialProgressView.bundle";
@interface MDCProgressView () <CAAnimationDelegate>
// `progressView` and `indeterminateProgressView` are both used to create animations for
// Indeterminate mode. `progressView` is used for the first animation in the sequence, and
// `indeterminateProgressView` is used for the second animation.
// When `progressLayerView` is enabled, it is the only view used for Indeterminate animation.
@property(nonatomic, strong) MDCProgressGradientView *progressView;
@property(nonatomic, strong) MDCProgressGradientView *indeterminateProgressView;
@property(nonatomic, strong) MDCProgressLayerView *progressLayerView;
@property(nonatomic, strong) UIView *trackView;
@property(nonatomic) BOOL animatingHide;
// These properties are used for the NTC Determinate branch of the Progress view.
// The NTC Determinate branch is gated by the `enableDeterminateStopMark` property.
// In this branch, `progressView`, `indeterminateProgressView`, and `progressLayerView` are hidden
// when in Determinate mode.
@property(nonatomic, strong) UIView *determinateProgressView;
@property(nonatomic, strong) UIView *determinateStopMark;
@property(nonatomic, strong, nullable) CAShapeLayer *determinateProgressBarLayer;
@property(nonatomic, strong, nullable) CAShapeLayer *determinateProgressViewGapLayer;
@property(nonatomic, copy, nullable) void (^userCompletion)(BOOL finished);
// `queuedProgress` represents a unary queue that stores the most recent value from `setProgress`.
// In Determinate mode, when a new `progress` value is set while the Determinate progress view is
// animating, this value is used for a follow-up animation.
// This is a nullable NSNumber instead of a CGFloat so that its unset state can be differentiated
// from a value of 0.
@property(nonatomic, strong, nullable) NSNumber *queuedProgress;
// A UIProgressView to return the same format for the accessibility value. For example, when
// progress is 0.497, it reports "fifty per cent".
@property(nonatomic, readonly) UIProgressView *accessibilityProgressView;
@end
@implementation MDCProgressView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self commonMDCProgressViewInit];
}
return self;
}
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self commonMDCProgressViewInit];
}
return self;
}
- (void)commonMDCProgressViewInit {
self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
self.backgroundColor = [UIColor clearColor];
self.clipsToBounds = YES;
self.isAccessibilityElement = YES;
_mode = MDCProgressViewModeDeterminate;
_animating = NO;
_backwardProgressAnimationMode = MDCProgressViewBackwardAnimationModeReset;
_trackView = [[UIView alloc] initWithFrame:self.frame];
_trackView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
[self addSubview:_trackView];
_progressView = [[MDCProgressGradientView alloc] initWithFrame:CGRectZero];
[self addSubview:_progressView];
_indeterminateProgressView = [[MDCProgressGradientView alloc] initWithFrame:CGRectZero];
_indeterminateProgressView.hidden = YES;
[self addSubview:_indeterminateProgressView];
_progressLayerView = [[MDCProgressLayerView alloc] initWithFrame:CGRectZero];
_progressLayerView.hidden = YES;
[self addSubview:_progressLayerView];
_progressView.colors = @[ MDCProgressViewDefaultTintColor(), MDCProgressViewDefaultTintColor() ];
_indeterminateProgressView.colors =
@[ MDCProgressViewDefaultTintColor(), MDCProgressViewDefaultTintColor() ];
_trackView.backgroundColor =
[[self class] defaultTrackTintColorForProgressTintColor:MDCProgressViewDefaultTintColor()];
}
- (void)willMoveToSuperview:(nullable UIView *)superview {
[super willMoveToSuperview:superview];
[NSObject cancelPreviousPerformRequestsWithTarget:self];
}
- (void)layoutSubviews {
[super layoutSubviews];
// NTC Determinate Branch
if (_enableDeterminateStopMark && _mode == MDCProgressViewModeDeterminate) {
// Set an initial width and height so that the stopmark can be drawn.
if (_determinateProgressView == nil) {
_determinateProgressView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 4, 4)];
_determinateProgressView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
_determinateProgressView.backgroundColor = _trackTintColor;
_determinateProgressView.translatesAutoresizingMaskIntoConstraints = NO;
_determinateProgressView.clipsToBounds = YES;
if (self.effectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft) {
_determinateProgressView.transform = CGAffineTransformMakeScale(-1, 1);
}
[self addSubview:_determinateProgressView];
[NSLayoutConstraint activateConstraints:@[
[_determinateProgressView.heightAnchor constraintEqualToAnchor:self.heightAnchor],
[_determinateProgressView.widthAnchor constraintEqualToAnchor:self.widthAnchor],
[_determinateProgressView.centerXAnchor constraintEqualToAnchor:self.centerXAnchor],
[_determinateProgressView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor]
]];
}
if (_determinateProgressBarLayer == nil) {
_determinateProgressBarLayer = [self makeDeterminateProgressBarLayer];
if (self.cornerRadius > 0) {
_determinateProgressBarLayer.lineCap = kCALineCapButt;
_determinateProgressView.layer.cornerRadius = _cornerRadius;
_determinateProgressBarLayer.cornerRadius = _cornerRadius;
}
[_determinateProgressView.layer addSublayer:_determinateProgressBarLayer];
}
if (_determinateProgressViewGapLayer == nil) {
[self configureDeterminateProgressGapLayer];
}
// StopMark must be configured after DeterminateProgressBar.
// StopMark is DeterminateProgressBar's subview, and is constrained against it.
if (_determinateStopMark == nil) {
_determinateStopMark = [self makeCircularStopMarkView];
[_determinateStopMark.layer addSublayer:[self makeCircularStopMarkLayer]];
_determinateStopMark.hidden = (_mode == MDCProgressViewModeIndeterminate);
}
// Hide determinate progress view when in indeterminate mode.
_determinateProgressView.hidden = NO;
// Hide non-determinate progress views when in determinate mode.
_progressView.hidden = YES;
_indeterminateProgressView.hidden = YES;
_progressLayerView.hidden = YES;
if (!self.animatingHide) {
_determinateProgressView.frame = self.bounds;
}
} else {
_determinateProgressView.hidden = YES;
// Don't update the views when the hide animation is in progress.
if (!self.animatingHide) {
[self updateProgressView];
[self updateIndeterminateProgressView];
[self updateTrackView];
[self updateProgressLayerView];
}
}
}
- (void)traitCollectionDidChange:(nullable UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
if (self.progressTintColor) {
self.progressView.colors = @[ self.progressTintColor, self.progressTintColor ];
self.indeterminateProgressView.colors = @[ self.progressTintColor, self.progressTintColor ];
}
if (self.traitCollectionDidChangeBlock) {
self.traitCollectionDidChangeBlock(self, previousTraitCollection);
}
// Reconfigure layers when trait collection changes in order to get the correct color.
if ([self.traitCollection
hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
if (_enableDeterminateStopMark && _mode == MDCProgressViewModeDeterminate) {
if (_determinateProgressBarLayer) {
[_determinateProgressBarLayer removeFromSuperlayer];
_determinateProgressBarLayer = nil;
_determinateProgressBarLayer = [self makeDeterminateProgressBarLayer];
[_determinateProgressView.layer addSublayer:_determinateProgressBarLayer];
}
if (_determinateProgressViewGapLayer) {
[_determinateProgressViewGapLayer removeFromSuperlayer];
_determinateProgressViewGapLayer = nil;
[self configureDeterminateProgressGapLayer];
}
if (_determinateStopMark) {
_determinateStopMark.layer.sublayers = nil;
[_determinateStopMark.layer addSublayer:[self makeCircularStopMarkLayer]];
}
}
}
}
- (void)setProgressTintColor:(nullable UIColor *)progressTintColor {
_progressTintColor = progressTintColor;
_progressTintColors = nil;
if (progressTintColor != nil) {
self.progressView.colors = @[ progressTintColor, progressTintColor ];
self.indeterminateProgressView.colors = @[ progressTintColor, progressTintColor ];
_determinateProgressBarLayer.fillColor = progressTintColor.CGColor;
} else {
self.progressView.colors = nil;
self.indeterminateProgressView.colors = nil;
_determinateProgressBarLayer.fillColor = nil;
}
}
- (void)setProgressTintColors:(nullable NSArray<UIColor *> *)progressTintColors {
_progressTintColors = [progressTintColors copy];
self.progressView.colors = _progressTintColors;
self.indeterminateProgressView.colors = _progressTintColors;
self.progressLayerView.frame = self.bounds;
self.progressLayerView.colors = _progressTintColors;
// progressTintColors is not used in NTC Determinate branch.
// Since some clients set the tint color with this setter, use the first value if there is one
// for Determinate mode.
if (_enableDeterminateStopMark) {
if (_mode == MDCProgressViewModeDeterminate && progressTintColors.count > 0) {
_progressTintColor = progressTintColors[0];
_determinateProgressBarLayer.fillColor = _progressTintColor.CGColor;
}
} else {
_progressTintColor = nil;
}
}
- (void)setMode:(MDCProgressViewMode)mode {
if (_mode == mode) {
return;
}
_mode = mode;
self.indeterminateProgressView.hidden = (mode == MDCProgressViewModeDeterminate);
self.determinateStopMark.hidden =
(_mode == MDCProgressViewModeIndeterminate || !_enableDeterminateStopMark);
}
- (void)setCornerRadius:(CGFloat)cornerRadius {
_cornerRadius = cornerRadius;
_progressView.layer.cornerRadius = cornerRadius;
_indeterminateProgressView.layer.cornerRadius = cornerRadius;
_trackView.layer.cornerRadius = cornerRadius;
_determinateProgressView.layer.cornerRadius = cornerRadius;
_determinateProgressBarLayer.cornerRadius = cornerRadius;
BOOL hasNonZeroCornerRadius = !MDCCGFloatIsExactlyZero(cornerRadius);
_progressView.clipsToBounds = hasNonZeroCornerRadius;
_indeterminateProgressView.clipsToBounds = hasNonZeroCornerRadius;
_trackView.clipsToBounds = hasNonZeroCornerRadius;
_determinateProgressView.clipsToBounds = hasNonZeroCornerRadius;
}
- (void)setEnableDeterminateStopMark:(BOOL)enableDeterminateStopMark {
_enableDeterminateStopMark = enableDeterminateStopMark;
_determinateStopMark.hidden =
(_mode == MDCProgressViewModeIndeterminate || !enableDeterminateStopMark);
[self setNeedsLayout];
}
- (void)setProgress:(float)progress {
if (progress > 1) {
progress = 1;
}
if (progress < 0) {
progress = 0;
}
if (!_enableDeterminateStopMark) {
_progress = progress;
}
// Indeterminate mode ignores the progress property.
if (_mode == MDCProgressViewModeIndeterminate) {
return;
}
if (_enableDeterminateStopMark) {
[self setProgress:progress animated:NO completion:nil];
}
[self accessibilityValueDidChange];
[self setNeedsLayout];
}
- (void)setProgress:(float)progress
animated:(BOOL)animated
completion:(void (^__nullable)(BOOL finished))userCompletion {
// Indeterminate progress animation branch, with an early return.
// Indeterminate animation is handled in `startAnimatingBar`.
if (_mode == MDCProgressViewModeIndeterminate) {
self.progress = progress;
if (userCompletion) {
userCompletion(NO);
}
return;
}
// NTC Determinate Branch - setProgress Animation
if (_enableDeterminateStopMark && _mode == MDCProgressViewModeDeterminate) {
// Completion block is called in `animationDidStop`.
_userCompletion = userCompletion;
if (animated) {
if ([_determinateProgressBarLayer animationForKey:kProgressViewLayerAnimationKey]) {
_queuedProgress = @(progress);
return;
}
[self configureDeterminateAnimationsForProgress:progress];
} else {
_queuedProgress = nil;
UIBezierPath *toPathForProgressLayer = [self makeToPathForBarWithProgress:progress];
UIBezierPath *toPathForProgressGapLayer = [self makeToPathForGapWithProgress:progress];
[CATransaction begin];
[CATransaction setDisableActions:YES];
// Use `setDisableActions` to disable implicit animations.
_determinateProgressBarLayer.path = toPathForProgressLayer.CGPath;
_determinateProgressViewGapLayer.path = toPathForProgressGapLayer.CGPath;
[CATransaction commit];
}
_progress = progress;
} else {
if (progress < self.progress &&
self.backwardProgressAnimationMode == MDCProgressViewBackwardAnimationModeReset) {
self.progress = 0;
[self updateProgressView];
}
self.progress = progress;
[UIView animateWithDuration:animated ? [[self class] animationDuration] : 0
delay:0
options:[[self class] animationOptions]
animations:^{
[self updateProgressView];
}
completion:userCompletion];
}
}
- (void)setHidden:(BOOL)hidden
animated:(BOOL)animated
completion:(void (^__nullable)(BOOL finished))userCompletion {
if (hidden == self.hidden) {
if (userCompletion) {
userCompletion(YES);
}
return;
}
void (^animations)(void);
if (hidden) {
self.animatingHide = YES;
animations = ^{
CGFloat y = CGRectGetHeight(self.bounds);
CGRect trackViewFrame = self.trackView.frame;
trackViewFrame.origin.y = y;
trackViewFrame.size.height = 0;
self.trackView.frame = trackViewFrame;
CGRect progressViewFrame = self.progressView.frame;
progressViewFrame.origin.y = y;
progressViewFrame.size.height = 0;
self.progressView.frame = progressViewFrame;
};
} else {
self.hidden = NO;
animations = ^{
self.trackView.frame = self.bounds;
CGRect progressViewFrame = self.progressView.frame;
progressViewFrame.origin.y = 0;
progressViewFrame.size.height = CGRectGetHeight(self.bounds);
self.progressView.frame = progressViewFrame;
};
}
[UIView animateWithDuration:animated ? [[self class] animationDuration] : 0
delay:0
options:[[self class] animationOptions]
animations:animations
completion:^(BOOL finished) {
if (hidden) {
self.animatingHide = NO;
self.hidden = YES;
}
if (userCompletion) {
userCompletion(finished);
}
}];
}
#pragma mark Accessibility
- (UIProgressView *)accessibilityProgressView {
// Accessibility values are determined by querying a UIProgressView set to the same value as our
// MDCProgressView.
static UIProgressView *accessibilityProgressView;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
accessibilityProgressView = [[UIProgressView alloc] init];
});
return accessibilityProgressView;
}
- (nullable NSString *)accessibilityValue {
self.accessibilityProgressView.progress = self.progress;
return self.accessibilityProgressView.accessibilityValue;
}
- (void)accessibilityValueDidChange {
// Store a strong reference to self until the end of the method. Indeed,
// a previous -performSelector:withObject:afterDelay: might be the last thing
// to retain self, so calling +cancelPreviousPerformRequestsWithTarget: might
// deallocate self.
MDCProgressView *strongSelf = self;
// Cancel unprocessed announcements and replace them with the most up-to-date
// value. That way, they don't overlap and don't spam the user.
[NSObject cancelPreviousPerformRequestsWithTarget:strongSelf
selector:@selector(announceAccessibilityValueChange)
object:nil];
// Schedule a new announcement.
[strongSelf performSelector:@selector(announceAccessibilityValueChange)
withObject:nil
afterDelay:1];
}
- (void)announceAccessibilityValueChange {
if ([self accessibilityElementIsFocused]) {
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification,
[self accessibilityValue]);
}
}
- (nullable NSString *)accessibilityLabel {
return self.accessibilityProgressView.accessibilityLabel ?: [self defaultAccessibilityLabel];
}
- (NSString *)defaultAccessibilityLabel {
MaterialProgressViewStringId keyIndex = kStr_MaterialProgressViewAccessibilityLabel;
NSString *key = kMaterialProgressViewStringTable[keyIndex];
return NSLocalizedStringFromTableInBundle(key, kMaterialProgressViewStringsTableName,
[[self class] bundle], @"Progress View");
}
- (void)startAnimating {
[self startAnimatingBar];
_animating = YES;
[self setNeedsLayout];
}
- (void)stopAnimating {
_animating = NO;
[self.progressView.shapeLayer removeAllAnimations];
[self.indeterminateProgressView.shapeLayer removeAllAnimations];
[_progressLayerView stopAnimating];
[self setNeedsLayout];
}
#pragma mark - Resource Bundle
+ (NSBundle *)bundle {
static NSBundle *bundle = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
bundle = [NSBundle bundleWithPath:[self bundlePathWithName:kBundle]];
});
return bundle;
}
+ (NSString *)bundlePathWithName:(NSString *)bundleName {
// In iOS 8+, we could be included by way of a dynamic framework, and our resource bundles may
// not be in the main .app bundle, but rather in a nested framework, so figure out where we live
// and use that as the search location.
NSBundle *bundle = [NSBundle bundleForClass:[MDCProgressView class]];
NSString *resourcePath = [(nil == bundle ? [NSBundle mainBundle] : bundle) resourcePath];
return [resourcePath stringByAppendingPathComponent:bundleName];
}
#pragma mark Private
+ (NSTimeInterval)animationDuration {
return MDCProgressViewAnimationDuration;
}
+ (UIViewAnimationOptions)animationOptions {
// Since the animation is fake, using a linear interpolation avoids the speeding up and slowing
// down that repeated easing in and out causes.
return UIViewAnimationOptionCurveLinear;
}
+ (UIColor *)defaultTrackTintColorForProgressTintColor:(UIColor *)progressTintColor {
CGFloat hue, saturation, brightness, alpha;
if ([progressTintColor getHue:&hue saturation:&saturation brightness:&brightness alpha:&alpha]) {
CGFloat newSaturation = MIN(saturation * MDCProgressViewTrackColorDesaturation, 1);
return [UIColor colorWithHue:hue saturation:newSaturation brightness:brightness alpha:alpha];
}
return [UIColor clearColor];
}
- (void)updateProgressView {
CGRect progressFrame = self.bounds;
if (_mode == MDCProgressViewModeDeterminate) {
// Update progressView with the current progress value.
#if defined(TARGET_OS_VISION) && TARGET_OS_VISION
// For code review, use the review queue listed in go/material-visionos-review.
UITraitCollection *current = [UITraitCollection currentTraitCollection];
CGFloat scale = current ? [current displayScale] : 1.0;
if (scale <= 0) {
scale = 1.0;
}
#else
CGFloat scale = self.window.screen.scale > 0 ? self.window.screen.scale : 1;
#endif
CGFloat pointWidth = self.progress * CGRectGetWidth(self.bounds);
CGFloat pixelAlignedWidth = round(pointWidth * scale) / scale;
progressFrame = CGRectMake(0, 0, pixelAlignedWidth, CGRectGetHeight(self.bounds));
} else {
if (!self.animating) {
progressFrame = CGRectZero;
}
}
if (self.effectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft) {
progressFrame = MDFRectFlippedHorizontally(progressFrame, CGRectGetWidth(self.bounds));
}
self.progressView.frame = progressFrame;
}
- (void)updateIndeterminateProgressView {
self.indeterminateProgressView.frame = self.animating ? self.bounds : CGRectZero;
}
- (void)updateTrackView {
const CGSize size = self.bounds.size;
self.trackView.frame = self.hidden ? CGRectMake(0.0, size.height, size.width, 0.0) : self.bounds;
}
- (BOOL)isIndeterminateAndMultichromatic {
BOOL isIndeterminate = (_mode == MDCProgressViewModeIndeterminate);
BOOL isMultichromatic = (_progressTintColor == nil && _progressTintColors != nil);
return isIndeterminate && isMultichromatic;
}
- (void)updateProgressLayerView {
self.progressLayerView.frame = self.animating ? self.bounds : CGRectZero;
}
- (void)startAnimatingBar {
// Use the new MDCProgressLayerView only for indeterminate multichromatic.
// Determinate Monochromatic, Determinate Multichromatic, and Indeterminate Monochromatic
// will continue to use MDCProgressGradientView.
// NTC Determinate branch is handled in `setProgress`, which uses `determinateProgressView`.
// The code path for indeterminate animations uses `progressView` and `indeterminateProgressView`,
// which are MDCProgressGradientViews.
// Note that this code path is for the Indeterminate animation, but it uses both `_progressView`
// and `_indeterminateProgressView` to create the Indeterminate animations.
if (_mode == MDCProgressViewModeDeterminate && _enableDeterminateStopMark) {
return;
}
if ([self isIndeterminateAndMultichromatic]) {
_progressView.hidden = YES;
[_progressView.shapeLayer removeAllAnimations];
_indeterminateProgressView.hidden = YES;
[self.indeterminateProgressView.shapeLayer removeAllAnimations];
_progressLayerView.frame = self.bounds;
_progressLayerView.hidden = NO;
[_progressLayerView startAnimating];
return;
} else {
_progressLayerView.hidden = YES;
[_progressLayerView stopAnimating];
_indeterminateProgressView.hidden = NO;
_progressView.hidden = NO;
}
// If the bar isn't indeterminate or the bar is already animating, don't add the animation again.
if (_mode == MDCProgressViewModeDeterminate || _animating) {
return;
}
[self.progressView.shapeLayer removeAllAnimations];
[self.indeterminateProgressView.shapeLayer removeAllAnimations];
// The numeric values used here conform to https://material.io/components/progress-indicators.
CABasicAnimation *progressViewHead = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
progressViewHead.fromValue = @0;
progressViewHead.toValue = @1;
progressViewHead.duration = 0.75;
progressViewHead.timingFunction =
[[CAMediaTimingFunction alloc] initWithControlPoints:0.20f:0.00f:0.80f:1.00f];
progressViewHead.fillMode = kCAFillModeBackwards;
CABasicAnimation *progressViewTail = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
progressViewTail.beginTime = 0.333;
progressViewTail.fromValue = @0;
progressViewTail.toValue = @1;
progressViewTail.duration = 0.85;
progressViewTail.timingFunction =
[[CAMediaTimingFunction alloc] initWithControlPoints:0.40f:0.00f:1.00f:1.00f];
progressViewTail.fillMode = kCAFillModeForwards;
CAAnimationGroup *progressViewAnimationGroup = [[CAAnimationGroup alloc] init];
progressViewAnimationGroup.animations = @[ progressViewHead, progressViewTail ];
progressViewAnimationGroup.duration = 1.8;
progressViewAnimationGroup.removedOnCompletion = NO;
progressViewAnimationGroup.repeatCount = HUGE_VALF;
[self.progressView.shapeLayer addAnimation:progressViewAnimationGroup
forKey:@"kProgressViewAnimation"];
CABasicAnimation *indeterminateProgressViewHead =
[CABasicAnimation animationWithKeyPath:@"strokeEnd"];
indeterminateProgressViewHead.fromValue = @0;
indeterminateProgressViewHead.toValue = @1;
indeterminateProgressViewHead.duration = 0.567;
indeterminateProgressViewHead.beginTime = 1;
indeterminateProgressViewHead.timingFunction =
[[CAMediaTimingFunction alloc] initWithControlPoints:0.00f:0.00f:0.65f:1.00f];
indeterminateProgressViewHead.fillMode = kCAFillModeBackwards;
CABasicAnimation *indeterminateProgressViewTail =
[CABasicAnimation animationWithKeyPath:@"strokeStart"];
indeterminateProgressViewTail.beginTime = 1.267;
indeterminateProgressViewTail.fromValue = @0;
indeterminateProgressViewTail.toValue = @1;
indeterminateProgressViewTail.duration = 0.533;
indeterminateProgressViewTail.timingFunction =
[[CAMediaTimingFunction alloc] initWithControlPoints:0.10f:0.00f:0.45f:1.00f];
indeterminateProgressViewTail.fillMode = kCAFillModeBackwards;
CAAnimationGroup *indeterminateProgressViewAnimationGroup = [[CAAnimationGroup alloc] init];
indeterminateProgressViewAnimationGroup.animations =
@[ indeterminateProgressViewHead, indeterminateProgressViewTail ];
indeterminateProgressViewAnimationGroup.duration = 1.8;
indeterminateProgressViewAnimationGroup.removedOnCompletion = NO;
indeterminateProgressViewAnimationGroup.repeatCount = HUGE_VALF;
[self.indeterminateProgressView.shapeLayer addAnimation:indeterminateProgressViewAnimationGroup
forKey:@"kIndeterminateProgressViewAnimation"];
}
#pragma mark - NTC Determinate Branch
- (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)flag {
if (!_enableDeterminateStopMark) {
return;
}
// `animationDidStop` is necessary because, without a delegate call, we can only tell when a
// CAAnimation has completed successfully. `animationDidStop` notifies when an animation is
// stopped early or fails.
if (_userCompletion) {
_userCompletion(flag);
_userCompletion = nil;
}
if (_queuedProgress != nil) {
[self configureDeterminateAnimationsForProgress:_queuedProgress.floatValue];
_progress = _queuedProgress.floatValue;
_queuedProgress = nil;
}
}
- (void)setTrackTintColor:(nullable UIColor *)trackTintColor {
_trackTintColor = trackTintColor;
_trackView.backgroundColor = trackTintColor;
_determinateProgressView.backgroundColor = trackTintColor;
}
- (void)setGapColor:(nullable UIColor *)gapColor {
_gapColor = gapColor;
_determinateProgressViewGapLayer.fillColor = gapColor.CGColor;
}
- (UIView *)makeCircularStopMarkView {
UIView *determinateStopMark = [[UIView alloc]
initWithFrame:CGRectMake(0, 0, self.bounds.size.height, self.bounds.size.height)];
determinateStopMark.translatesAutoresizingMaskIntoConstraints = NO;
[_determinateProgressView addSubview:determinateStopMark];
NSLayoutConstraint *xAnchor =
(self.effectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft
? [_determinateProgressView.leadingAnchor
constraintEqualToAnchor:determinateStopMark.leadingAnchor]
: [_determinateProgressView.trailingAnchor
constraintEqualToAnchor:determinateStopMark.trailingAnchor]);
[NSLayoutConstraint activateConstraints:@[
[determinateStopMark.heightAnchor constraintEqualToConstant:self.bounds.size.height],
[determinateStopMark.widthAnchor constraintEqualToConstant:self.bounds.size.height],
xAnchor,
[determinateStopMark.centerYAnchor
constraintEqualToAnchor:_determinateProgressView.centerYAnchor],
]];
return determinateStopMark;
}
- (CAShapeLayer *)makeCircularStopMarkLayer {
CGFloat circleDimension = self.bounds.size.height;
CAShapeLayer *circleLayer = [[CAShapeLayer alloc] init];
circleLayer.frame = CGRectMake(0, 0, circleDimension, circleDimension);
circleLayer.path = [UIBezierPath bezierPathWithOvalInRect:circleLayer.bounds].CGPath;
circleLayer.fillColor = self.progressTintColor.CGColor;
return circleLayer;
}
- (CAShapeLayer *)makeDeterminateProgressBarLayer {
CAShapeLayer *determinateProgressBarLayer = [CAShapeLayer layer];
determinateProgressBarLayer.cornerRadius = _cornerRadius;
determinateProgressBarLayer.lineCap = kCALineCapButt;
determinateProgressBarLayer.frame = CGRectMake(0, 0, 0, self.bounds.size.height);
determinateProgressBarLayer.fillColor = _progressTintColor.CGColor;
determinateProgressBarLayer.path = [self makeToPathForBarWithProgress:_progress].CGPath;
return determinateProgressBarLayer;
}
- (UIBezierPath *)makeToPathForGapWithProgress:(CGFloat)progress {
CGRect rect = CGRectMake(self.bounds.size.width * progress + progress, 0, MDCProgressViewGapWidth,
self.bounds.size.height);
CGFloat x = (progress == 0) ? rect.origin.x - MDCProgressViewGapWidth - 3 : rect.origin.x - 3;
CGFloat y = rect.origin.y - 1;
// TODO(b/358368816): Fix the bug where the gap does not take into account changes in height.
// Draws a square shape with concave sides that looks like this:
// ---
// ) (
// ---
// The sides are to account for rounded corners.
// This shape is used for the progress gap.
// It is based on a bar height of 4, and a corner radius of 2.
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(x + 8, y)];
[path addLineToPoint:CGPointMake(x, y)];
[path addLineToPoint:CGPointMake(x, y + 1)];
[path addCurveToPoint:CGPointMake(x + 2, y + 3)
controlPoint1:CGPointMake(x + 1, y + 1)
controlPoint2:CGPointMake(x + 2, y + 2)];
[path addCurveToPoint:CGPointMake(x, y + 5)
controlPoint1:CGPointMake(x + 2, y + 4)
controlPoint2:CGPointMake(x + 1, y + 5)];
[path addLineToPoint:CGPointMake(x, y + 6)];
[path addLineToPoint:CGPointMake(x + 8, y + 6)];
[path addLineToPoint:CGPointMake(x + 8, y + 5)];
[path addCurveToPoint:CGPointMake(x + 6, y + 3)
controlPoint1:CGPointMake(x + 7, y + 5)
controlPoint2:CGPointMake(x + 6, y + 4)];
[path addCurveToPoint:CGPointMake(x + 8, y + 1)
controlPoint1:CGPointMake(x + 6, y + 2)
controlPoint2:CGPointMake(x + 7, y + 1)];
[path addLineToPoint:CGPointMake(x + 8, y)];
[path closePath];
return path;
}
- (UIBezierPath *)makeToPathForBarWithProgress:(CGFloat)progress {
CGFloat cornerRadiusOffset = (_cornerRadius > 0 ? _cornerRadius / 2 : 0);
CGSize size = self.bounds.size;
UIBezierPath *path;
CGRect pathRect = CGRectMake(0, 0, size.width * progress - cornerRadiusOffset, size.height);
if (_cornerRadius > 0) {
path = [UIBezierPath bezierPathWithRoundedRect:pathRect cornerRadius:_cornerRadius];
} else {
path = [UIBezierPath bezierPathWithRect:pathRect];
}
return path;
}
- (void)configureDeterminateProgressGapLayer {
CAShapeLayer *determinateProgressViewGapLayer = [CAShapeLayer layer];
determinateProgressViewGapLayer.frame = CGRectMake(0, 0, 0, self.bounds.size.height);
determinateProgressViewGapLayer.fillColor = _gapColor.CGColor;
[_determinateProgressView.layer addSublayer:determinateProgressViewGapLayer];
UIBezierPath *path = [self makeToPathForGapWithProgress:_progress];
determinateProgressViewGapLayer.path = path.CGPath;
_determinateProgressViewGapLayer = determinateProgressViewGapLayer;
}
- (CGFloat)calculateProportionalDeterminateAnimationDurationFrom:(CGFloat)fromProgressPercent
to:(CGFloat)toProgressPercent {
CGFloat difference = ABS(toProgressPercent - fromProgressPercent);
CGFloat duration = difference * MDCProgressViewDeterminateDuration;
return duration;
}
- (void)configureDeterminateAnimationsForProgress:(CGFloat)progress {
UIBezierPath *toPathForProgressLayer = [self makeToPathForBarWithProgress:progress];
UIBezierPath *toPathForProgressGapLayer = [self makeToPathForGapWithProgress:progress];
CGPathRef originalProgressBarPath = _determinateProgressBarLayer.path;
NSValue *originalProgressBarPathValue = [NSValue valueWithPointer:originalProgressBarPath];
CGPathRef originalProgressGapPath = _determinateProgressViewGapLayer.path;
NSValue *originalProgressGapPathValue = [NSValue valueWithPointer:originalProgressGapPath];
CGFloat duration = [self calculateProportionalDeterminateAnimationDurationFrom:_progress
to:progress];
_determinateProgressBarLayer.path = toPathForProgressLayer.CGPath;
CABasicAnimation *progressBarLayerPathAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
progressBarLayerPathAnimation.duration = duration;
progressBarLayerPathAnimation.delegate = self;
progressBarLayerPathAnimation.fromValue = (id)([originalProgressBarPathValue pointerValue]);
progressBarLayerPathAnimation.fillMode = kCAFillModeBoth;
progressBarLayerPathAnimation.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[_determinateProgressBarLayer addAnimation:progressBarLayerPathAnimation
forKey:kProgressViewLayerAnimationKey];
_determinateProgressViewGapLayer.path = toPathForProgressGapLayer.CGPath;
CABasicAnimation *progressGapLayerPathAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
// Add a slight duration increase to account for track bar overhang near animation end.
progressGapLayerPathAnimation.duration = duration + 0.01;
progressGapLayerPathAnimation.fromValue = (id)([originalProgressGapPathValue pointerValue]);
progressGapLayerPathAnimation.fillMode = kCAFillModeBoth;
progressGapLayerPathAnimation.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[_determinateProgressViewGapLayer addAnimation:progressGapLayerPathAnimation
forKey:kProgressViewGapAnimationKey];
}
@end
NS_ASSUME_NONNULL_END