// 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 #import "MaterialActivityIndicator.h" #import "MaterialButtons+ButtonThemer.h" #import "MaterialButtons.h" static const CGFloat kActivityIndicatorExampleArrowHeadSize = 5; static const CGFloat kActivityIndicatorExampleStrokeWidth = 2; static const NSTimeInterval kActivityIndicatorExampleAnimationDuration = 2.0 / 3.0; @interface ActivityIndicatorTransitionExampleViewController : UIViewController @property(nonatomic, strong) MDCSemanticColorScheme *colorScheme; @property(nonatomic, strong) MDCTypographyScheme *typographyScheme; @end @implementation ActivityIndicatorTransitionExampleViewController { MDCActivityIndicator *_activityIndicator; MDCButton *_button; CALayer *_rotationContainer; CALayer *_refreshArrowContainer; CAShapeLayer *_refreshArrowPoint; CAShapeLayer *_refreshStrokeLayer; } - (id)init { self = [super init]; if (self) { self.colorScheme = [[MDCSemanticColorScheme alloc] initWithDefaults:MDCColorSchemeDefaultsMaterial201804]; self.typographyScheme = [[MDCTypographyScheme alloc] init]; } return self; } - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = self.colorScheme.backgroundColor; _activityIndicator = [[MDCActivityIndicator alloc] initWithFrame:CGRectZero]; [_activityIndicator sizeToFit]; _activityIndicator.center = CGPointMake(self.view.bounds.size.width / 2, 130); _activityIndicator.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; _activityIndicator.delegate = self; [self.view addSubview:_activityIndicator]; _button = [[MDCButton alloc] init]; MDCButtonScheme *buttonScheme = [[MDCButtonScheme alloc] init]; buttonScheme.colorScheme = self.colorScheme; buttonScheme.typographyScheme = self.typographyScheme; [MDCContainedButtonThemer applyScheme:buttonScheme toButton:_button]; [_button addTarget:self action:@selector(startRefreshing) forControlEvents:UIControlEventTouchUpInside]; [_button setTitle:@"Refresh" forState:UIControlStateNormal]; [_button sizeToFit]; _button.center = CGPointMake(self.view.bounds.size.width / 2, 200); _button.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; [self.view addSubview:_button]; // Layers used in the custom transition animation. _rotationContainer = [CALayer layer]; [_activityIndicator.layer addSublayer:_rotationContainer]; _refreshArrowContainer = [CALayer layer]; [_rotationContainer addSublayer:_refreshArrowContainer]; CGMutablePathRef refreshArrowPath = CGPathCreateMutable(); CGPathMoveToPoint(refreshArrowPath, NULL, 0, -kActivityIndicatorExampleArrowHeadSize); CGPathAddLineToPoint(refreshArrowPath, NULL, kActivityIndicatorExampleArrowHeadSize, 0); CGPathAddLineToPoint(refreshArrowPath, NULL, 0, kActivityIndicatorExampleArrowHeadSize); CGPathCloseSubpath(refreshArrowPath); _refreshArrowPoint = [CAShapeLayer layer]; _refreshArrowPoint.anchorPoint = CGPointMake(0.5, 1); _refreshArrowPoint.path = refreshArrowPath; [_refreshArrowContainer addSublayer:_refreshArrowPoint]; CGPathRelease(refreshArrowPath); _refreshStrokeLayer = [CAShapeLayer layer]; _refreshStrokeLayer.lineWidth = kActivityIndicatorExampleStrokeWidth; _refreshStrokeLayer.fillColor = [UIColor clearColor].CGColor; _refreshStrokeLayer.strokeColor = [UIColor blackColor].CGColor; _refreshStrokeLayer.strokeStart = 0; _refreshStrokeLayer.strokeEnd = (CGFloat)0.8; [_rotationContainer addSublayer:_refreshStrokeLayer]; _rotationContainer.transform = CATransform3DMakeRotation((CGFloat)M_PI * (CGFloat)0.65, 0, 0, 1); _refreshArrowContainer.transform = CATransform3DMakeRotation((CGFloat)1.6 * (float)M_PI, 0, 0, 1); [CATransaction commit]; } - (void)viewWillLayoutSubviews { [super viewWillLayoutSubviews]; [CATransaction begin]; [CATransaction setDisableActions:YES]; CGRect bounds = _activityIndicator.bounds; _rotationContainer.bounds = bounds; _rotationContainer.position = CGPointMake(bounds.size.width / 2, bounds.size.height / 2); _refreshStrokeLayer.bounds = _rotationContainer.bounds; _refreshStrokeLayer.position = _rotationContainer.position; _refreshArrowContainer.bounds = _rotationContainer.bounds; _refreshArrowContainer.position = _rotationContainer.position; _refreshArrowPoint.position = CGPointMake(bounds.size.width / 2, kActivityIndicatorExampleStrokeWidth / 2); CGFloat offsetRadius = _activityIndicator.radius - kActivityIndicatorExampleStrokeWidth / 2; UIBezierPath *strokePath = [UIBezierPath bezierPathWithArcCenter:_refreshStrokeLayer.position radius:offsetRadius startAngle:-1 * (CGFloat)M_PI_2 endAngle:3 * (CGFloat)M_PI_2 clockwise:YES]; _refreshStrokeLayer.path = strokePath.CGPath; } - (void)startRefreshing { _button.enabled = NO; MDCActivityIndicatorTransition *transition = [[MDCActivityIndicatorTransition alloc] initWithAnimation:^(CGFloat start, CGFloat end) { [self addFromRefreshIconAnimationsToActivityIndicatorWithStrokeStart:start strokeEnd:end]; }]; transition.duration = kActivityIndicatorExampleAnimationDuration; transition.completion = ^{ [CATransaction begin]; [CATransaction setDisableActions:YES]; self->_rotationContainer.hidden = YES; [self->_refreshArrowContainer removeAllAnimations]; [self->_refreshArrowPoint removeAllAnimations]; [CATransaction commit]; dispatch_time_t stopTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)); dispatch_after(stopTime, dispatch_get_main_queue(), ^{ [self stopRefreshing]; }); }; [_activityIndicator startAnimatingWithTransition:transition cycleStartIndex:1]; } - (void)stopRefreshing { [_refreshStrokeLayer removeAllAnimations]; [_rotationContainer removeAllAnimations]; MDCActivityIndicatorTransition *transition = [[MDCActivityIndicatorTransition alloc] initWithAnimation:^(CGFloat start, CGFloat end) { [self addToRefreshIconAnimationsFromActivityIndicatorWithStrokeStart:start strokeEnd:end]; }]; transition.duration = kActivityIndicatorExampleAnimationDuration; transition.completion = ^{ self->_button.enabled = YES; UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, self->_button); }; [_activityIndicator stopAnimatingWithTransition:transition]; } #pragma mark - Private - (void)addFromRefreshIconAnimationsToActivityIndicatorWithStrokeStart:(CGFloat)strokeStart strokeEnd:(CGFloat)strokeEnd { // Outer rotation CABasicAnimation *outerRotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"]; outerRotationAnimation.fromValue = @((CGFloat)M_PI * (CGFloat)0.65); outerRotationAnimation.toValue = @(strokeEnd * 2 * M_PI); outerRotationAnimation.fillMode = kCAFillModeForwards; outerRotationAnimation.removedOnCompletion = NO; [_rotationContainer addAnimation:outerRotationAnimation forKey:@"transform.rotation.z"]; CGFloat difference = strokeEnd - strokeStart; // Stroke start CABasicAnimation *strokeStartAnimation = [CABasicAnimation animationWithKeyPath:@"strokeStart"]; strokeStartAnimation.fromValue = @(0); // Ensure the stroke never disappears by never hitting stroke end's toValue. strokeStartAnimation.toValue = @(1 - difference); strokeStartAnimation.fillMode = kCAFillModeBoth; strokeStartAnimation.removedOnCompletion = NO; [_refreshStrokeLayer addAnimation:strokeStartAnimation forKey:@"strokeStart"]; // Stroke end CABasicAnimation *strokeEndAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"]; strokeEndAnimation.fromValue = @(_refreshStrokeLayer.strokeEnd); strokeEndAnimation.toValue = @(1); strokeEndAnimation.fillMode = kCAFillModeBoth; strokeEndAnimation.removedOnCompletion = NO; [_refreshStrokeLayer addAnimation:strokeEndAnimation forKey:@"strokeEnd"]; // Refresh arrow rotation and scale CABasicAnimation *refreshArrowRotation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"]; refreshArrowRotation.fromValue = @(M_PI * (CGFloat)1.6); refreshArrowRotation.toValue = @(M_PI * 2); refreshArrowRotation.fillMode = kCAFillModeForwards; refreshArrowRotation.removedOnCompletion = NO; [_refreshArrowContainer addAnimation:refreshArrowRotation forKey:@"transform.rotation.z"]; CABasicAnimation *arrowPointScaleAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale"]; arrowPointScaleAnimation.fromValue = @(1); arrowPointScaleAnimation.toValue = @(0); arrowPointScaleAnimation.fillMode = kCAFillModeForwards; arrowPointScaleAnimation.removedOnCompletion = NO; [_refreshArrowPoint addAnimation:arrowPointScaleAnimation forKey:@"transform.scale"]; } - (void)addToRefreshIconAnimationsFromActivityIndicatorWithStrokeStart:(CGFloat)strokeStart strokeEnd:(CGFloat)strokeEnd { // Adjust stroke position to offset outer rotation angle and ensure stroke position is in range // [0,1] for smooth animation strokeStart -= (CGFloat)0.325; strokeStart = strokeStart < 0 ? strokeStart + 1 : strokeStart; strokeEnd -= (CGFloat)0.325; strokeEnd = strokeEnd < 0 ? strokeEnd + 1 : strokeEnd; _rotationContainer.hidden = NO; // Stroke start CABasicAnimation *strokeStartAnimation = [CABasicAnimation animationWithKeyPath:@"strokeStart"]; strokeStartAnimation.fromValue = @(strokeStart); strokeStartAnimation.toValue = @(0); strokeStartAnimation.fillMode = kCAFillModeBoth; strokeStartAnimation.removedOnCompletion = NO; [_refreshStrokeLayer addAnimation:strokeStartAnimation forKey:@"strokeStart"]; // Stroke end CABasicAnimation *strokeEndAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"]; strokeEndAnimation.fromValue = @(strokeEnd); strokeEndAnimation.toValue = @((CGFloat)0.8); strokeEndAnimation.fillMode = kCAFillModeBoth; strokeEndAnimation.removedOnCompletion = NO; [_refreshStrokeLayer addAnimation:strokeEndAnimation forKey:@"strokeEnd"]; // Refresh arrow rotation and scale CABasicAnimation *refreshArrowRotation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"]; refreshArrowRotation.fromValue = @(strokeStart * 2 * M_PI); refreshArrowRotation.toValue = @((CGFloat)1.6 * M_PI); refreshArrowRotation.fillMode = kCAFillModeForwards; refreshArrowRotation.removedOnCompletion = NO; [_refreshArrowContainer addAnimation:refreshArrowRotation forKey:@"transform.rotation.z"]; CABasicAnimation *arrowPointScaleAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale"]; arrowPointScaleAnimation.fromValue = @(0); arrowPointScaleAnimation.toValue = @(1); arrowPointScaleAnimation.fillMode = kCAFillModeForwards; arrowPointScaleAnimation.removedOnCompletion = NO; [_refreshArrowPoint addAnimation:arrowPointScaleAnimation forKey:@"transform.scale"]; } #pragma mark - Catalog by Convention + (NSDictionary *)catalogMetadata { return @{ @"breadcrumbs" : @[ @"Activity Indicator", @"Activity Indicator Transition" ], @"primaryDemo" : @NO, @"presentable" : @YES }; } @end