| // Copyright 2022-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 "MDCProgressLayerView.h" |
| |
| #import <CoreGraphics/CoreGraphics.h> |
| |
| NS_ASSUME_NONNULL_BEGIN |
| |
| /** The duration of each individual layer animation. */ |
| static const CGFloat kProgressIntervalDuration = 0.667; |
| |
| /** The delay between each individual layer animation. */ |
| static const CGFloat kProgressIntervalDelay = 0.333; |
| |
| @interface MDCProgressLayerView () <CAAnimationDelegate> |
| |
| @property(nonatomic, readwrite) BOOL isAnimating; |
| @property(nonatomic, readwrite) NSUInteger sequenceCounter; |
| @property(nonatomic, readwrite) NSMutableArray<CALayer *> *animatableLayers; |
| @end |
| |
| @implementation MDCProgressLayerView |
| |
| #pragma mark - init |
| |
| - (instancetype)initWithFrame:(CGRect)frame { |
| self = [super initWithFrame:frame]; |
| if (self) { |
| [self commonMDCMulticoloredProgressViewInit]; |
| } |
| return self; |
| } |
| |
| - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { |
| self = [super initWithCoder:aDecoder]; |
| if (self) { |
| [self commonMDCMulticoloredProgressViewInit]; |
| } |
| return self; |
| } |
| |
| - (void)commonMDCMulticoloredProgressViewInit { |
| _animatableLayers = [[NSMutableArray alloc] init]; |
| |
| self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin; |
| self.layer.backgroundColor = UIColor.clearColor.CGColor; |
| self.clipsToBounds = YES; |
| } |
| |
| #pragma mark - Public |
| |
| - (void)startAnimating { |
| if (_isAnimating) { |
| [self stopAnimating]; |
| return; |
| } |
| |
| // Animate the first layer, if there is one. |
| // The first layer animation will begin a chain of subsequent repeating animations. |
| if (_animatableLayers.firstObject != nil) { |
| [self buildAnimationGroupForLayer:_animatableLayers[0] delay:0]; |
| _isAnimating = YES; |
| } |
| } |
| |
| - (void)stopAnimating { |
| if (!_isAnimating) { |
| return; |
| } |
| |
| [self resetAllAnimations]; |
| _isAnimating = NO; |
| } |
| |
| - (void)createSublayers { |
| [self resetAllAnimations]; |
| |
| [_colors enumerateObjectsUsingBlock:^(UIColor *color, NSUInteger index, BOOL *_Nonnull stop) { |
| CALayer *layer = [[CALayer alloc] init]; |
| |
| // The anchor point determines the direction of the progress indicator animations. |
| // (0, 1) animates left to right. |
| // (1, 0) animates right to left. |
| layer.anchorPoint = CGPointMake(0, 1); |
| |
| layer.frame = CGRectMake(self.layer.bounds.origin.x, self.layer.bounds.origin.y, 0, |
| self.layer.bounds.size.height); |
| |
| NSString *layerName = [NSString stringWithFormat:@"kColoredProgressLayer%lu", index]; |
| layer.name = layerName; |
| layer.backgroundColor = color.CGColor; |
| |
| [_animatableLayers addObject:layer]; |
| [self.layer addSublayer:layer]; |
| }]; |
| } |
| |
| - (void)configureSublayerFrames { |
| for (CALayer *sublayer in self.layer.sublayers) { |
| sublayer.frame = CGRectMake(self.layer.bounds.origin.x, self.layer.bounds.origin.y, |
| sublayer.bounds.size.width, self.layer.bounds.size.height); |
| } |
| } |
| |
| #pragma mark - layoutSubviews |
| |
| - (void)layoutSubviews { |
| [super layoutSubviews]; |
| |
| [self configureSublayerFrames]; |
| } |
| |
| #pragma mark - Setters |
| |
| - (void)setColors:(nullable NSArray<UIColor *> *)colors { |
| _colors = colors; |
| [self createSublayers]; |
| } |
| |
| #pragma mark - Private |
| |
| - (void)resetAllAnimations { |
| for (CALayer *layer in [self.layer.sublayers copy]) { |
| [layer removeFromSuperlayer]; |
| } |
| |
| _sequenceCounter = 0; |
| _animatableLayers = [[NSMutableArray alloc] init]; |
| } |
| |
| - (void)buildAnimationGroupForLayer:(CALayer *)animatableLayer delay:(CGFloat)delay { |
| [CATransaction begin]; |
| |
| CABasicAnimation *growAnimation = [CABasicAnimation animationWithKeyPath:@"bounds.size.width"]; |
| growAnimation.timingFunction = [[CAMediaTimingFunction alloc] initWithControlPoints: |
| 0.40:0.0:0.20:1.0]; |
| |
| growAnimation.duration = kProgressIntervalDuration; |
| |
| CGFloat beginTime = CACurrentMediaTime() + delay; |
| growAnimation.beginTime = beginTime; |
| |
| growAnimation.fromValue = 0; |
| growAnimation.toValue = @(self.bounds.size.width); |
| |
| growAnimation.removedOnCompletion = NO; |
| growAnimation.fillMode = kCAFillModeForwards; |
| |
| growAnimation.delegate = self; |
| |
| [animatableLayer addAnimation:growAnimation forKey:@"kGrowAnimation"]; |
| |
| // An animation group is used to provide a delay between each layer's animation. |
| // The delay is the total duration of the sequence. |
| CAAnimationGroup *animationGroup = [[CAAnimationGroup alloc] init]; |
| animationGroup.animations = @[ growAnimation ]; |
| animationGroup.duration = kProgressIntervalDuration * _colors.count; |
| [animatableLayer addAnimation:animationGroup forKey:nil]; |
| |
| [CATransaction commit]; |
| } |
| |
| - (void)bringSublayerToFront:(CALayer *)sublayer { |
| [sublayer removeFromSuperlayer]; |
| [self.layer addSublayer:sublayer]; |
| } |
| |
| /** |
| * Determines the next layer in a sequence following the given layer. |
| * The next layer is identified based on the given layer's position. |
| * The given layer is brought to the front, and an animation group is prepared for the next layer. |
| */ |
| - (void)processLayer:(nullable CALayer *)givenLayer { |
| if (_animatableLayers.firstObject == nil) { |
| return; |
| } |
| |
| if (givenLayer == nil || !_isAnimating) { |
| return; |
| } |
| |
| _sequenceCounter = (_sequenceCounter + 1) % _colors.count; |
| |
| [self bringSublayerToFront:givenLayer]; |
| |
| [self buildAnimationGroupForLayer:_animatableLayers[_sequenceCounter] |
| delay:kProgressIntervalDelay]; |
| } |
| |
| #pragma mark - CAAnimationDelegate |
| |
| - (void)animationDidStart:(CAAnimation *)animation { |
| if (_sequenceCounter < _animatableLayers.count) { |
| [self processLayer:_animatableLayers[_sequenceCounter]]; |
| } |
| } |
| |
| @end |
| |
| NS_ASSUME_NONNULL_END |