blob: 49e7f62f553e1ac640465f1fda1d49581efb10f2 [file] [log] [blame] [edit]
// 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