blob: f8f5089847e1c47098cb466e4eafaba7c0eeb92d [file] [log] [blame] [edit]
// 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 "MDCPageControlTrackLayer.h"
static const NSTimeInterval kPageControlAnimationDuration = 0.2;
static const NSInteger kPageControlKeyframeCount = 2;
static NSString *const kPageControlAnimationKeyDraw = @"drawTrack";
@implementation MDCPageControlTrackLayer {
CGFloat _radius;
CGPoint _startPoint, _endPoint, _midPoint;
BOOL _isAnimating;
}
- (instancetype)initWithRadius:(CGFloat)radius {
self = [super init];
if (self) {
_trackHidden = YES;
_radius = radius;
self.cornerRadius = radius;
}
return self;
}
- (void)setTrackColor:(UIColor *)trackColor {
_trackColor = trackColor;
self.fillColor = trackColor.CGColor;
self.backgroundColor = trackColor.CGColor;
}
#pragma mark - Draw/Extend Track
- (void)drawTrackFromStartPoint:(CGPoint)startPoint toEndPoint:(CGPoint)endPoint {
if (_isAnimating || !_trackHidden || [self isPointZero:startPoint] ||
[self isPointZero:endPoint]) {
return;
}
// First reset track frame.
[self resetTrackFrame];
_isAnimating = YES;
_startPoint = startPoint;
_endPoint = endPoint;
_midPoint = [self midPointFromPoint:startPoint toPoint:endPoint];
[self resetHidden:NO];
[CATransaction begin];
[CATransaction setCompletionBlock:^{
// After drawn, remove animation and update track frame.
[self removeAnimationForKey:kPageControlAnimationKeyDraw];
[self updateTrackFrameWithAnimation:NO completion:nil];
self->_trackHidden = NO;
self->_isAnimating = NO;
}];
// Get animation keyframes.
NSMutableArray<UIBezierPath *> *values = [NSMutableArray array];
for (NSInteger i = 0; i < kPageControlKeyframeCount; i++) {
[values addObject:(id)[self pathAtKeyframe:i]];
}
// Add animation path.
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"path"];
animation.duration = kPageControlAnimationDuration;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
animation.values = values;
[self addAnimation:animation forKey:kPageControlAnimationKeyDraw];
[CATransaction commit];
}
- (void)extendTrackFromStartPoint:(CGPoint)startPoint toEndPoint:(CGPoint)endPoint {
if (_trackHidden || [self isPointZero:startPoint] || [self isPointZero:endPoint]) {
return;
}
// Extend track to encompass minimum startPoint and maximum endPoint.
_startPoint = (startPoint.x < _startPoint.x) ? startPoint : _startPoint;
_endPoint = (endPoint.x > _endPoint.x) ? endPoint : _endPoint;
[self updateTrackFrameWithAnimation:YES completion:nil];
}
- (void)drawAndExtendTrackFromStartPoint:(CGPoint)startPoint
toEndPoint:(CGPoint)endPoint
completion:(void (^)(void))completion {
_trackHidden = NO;
if ([self isPointZero:_startPoint]) {
// If no previous start point, first set frame without animation.
_startPoint = startPoint;
_endPoint = endPoint;
[self updateTrackFrameWithAnimation:NO
completion:^{
[self updateTrackFrameWithAnimation:YES
completion:^{
if (completion) {
completion();
}
}];
}];
} else {
// Previous startPoint exists, therefore animate to new start and end points.
_startPoint = startPoint;
_endPoint = endPoint;
[self updateTrackFrameWithAnimation:YES
completion:^{
if (completion) {
completion();
}
}];
}
}
- (void)updateTrackFrameWithAnimation:(BOOL)animated completion:(void (^)(void))completion {
// Set track frame without implicit animation.
[self resetHidden:NO];
[CATransaction begin];
[CATransaction setDisableActions:!animated];
[CATransaction setCompletionBlock:^{
if (completion) {
completion();
}
}];
self.frame = CGRectMake(_startPoint.x - _radius, _startPoint.y - _radius,
_endPoint.x - _startPoint.x + (_radius * 2), _radius * 2);
[CATransaction commit];
}
- (void)resetTrackFrame {
// Reset track frame without implicit animation.
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.frame = CGRectZero;
[CATransaction commit];
}
- (void)resetHidden:(BOOL)hidden {
// Reset hidden without implicit animation.
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.hidden = hidden;
[CATransaction commit];
}
#pragma mark - Remove Track
- (void)removeTrackTowardsPoint:(CGPoint)point completion:(void (^)(void))completion {
// Animate the track removal towards a single point.
_startPoint = point;
_endPoint = point;
[self updateTrackFrameWithAnimation:YES
completion:^{
[self reset];
if (completion) {
completion();
}
}];
}
- (void)resetAtPoint:(CGPoint)point {
// Resets the track at single point without animation.
_startPoint = point;
_endPoint = point;
[self updateTrackFrameWithAnimation:NO
completion:^{
[self reset];
}];
}
- (void)reset {
// Reset track frame without implicit animation.
[CATransaction begin];
[CATransaction setDisableActions:YES];
[self removeAllAnimations];
_isAnimating = NO;
_trackHidden = YES;
[self resetHidden:YES];
[CATransaction commit];
}
#pragma mark - Private
- (CGPoint)midPointFromPoint:(CGPoint)fromPoint toPoint:(CGPoint)toPoint {
// Returns midpoint between two points.
return CGPointMake((fromPoint.x + toPoint.x) / 2, (fromPoint.y + toPoint.y) / 2);
}
- (CGPathRef)pathAtKeyframe:(NSInteger)keyframe {
// Generates bezier path keyframes that can be animated forward and in reverse.
CGFloat r = _radius;
CGFloat d = _radius * 2;
UIBezierPath *bezierPath = UIBezierPath.bezierPath;
if (keyframe == 0) {
// Create circles at start and end points.
[self addRoundedEndpontToBezierPath:bezierPath atPoint:_startPoint];
[self addRoundedEndpontToBezierPath:bezierPath atPoint:_endPoint];
// Create an arc from top of startpoint circle to midpoint.
[bezierPath moveToPoint:[self pointOnCircleWithRadius:r angleInDegrees:300 origin:_startPoint]];
[bezierPath addQuadCurveToPoint:_midPoint controlPoint:CGPointMake(_midPoint.x - r / 2, r)];
// Create an arc from midpoint to top of endpoint circle.
[bezierPath addQuadCurveToPoint:[self pointOnCircleWithRadius:r
angleInDegrees:240
origin:_endPoint]
controlPoint:CGPointMake(_midPoint.x + r / 2, r)];
// Create a line from top of endpoint circle to bottom of endpoint circle.
[bezierPath addLineToPoint:[self pointOnCircleWithRadius:r
angleInDegrees:120
origin:_endPoint]];
// Create an arc from bottom of endpoint circle to midpoint.
[bezierPath addQuadCurveToPoint:_midPoint controlPoint:CGPointMake(_midPoint.x + r / 2, r)];
// Create an arc from midpoint to bottom of startpoint circle.
[bezierPath addQuadCurveToPoint:[self pointOnCircleWithRadius:r
angleInDegrees:60
origin:_startPoint]
controlPoint:CGPointMake(_midPoint.x - r / 2, r)];
// Create line from bottom of startpoint circle to top of startpoint circle.
[bezierPath addLineToPoint:[self pointOnCircleWithRadius:r
angleInDegrees:300
origin:_startPoint]];
// Close path.
[bezierPath closePath];
} else if (keyframe == 1) {
// Creates rectangular path from startpoint to endpoint with rounded ends.
// Requires same number of paths as previous keyframe to animate properly.
[self addRoundedEndpontToBezierPath:bezierPath atPoint:_startPoint];
[self addRoundedEndpontToBezierPath:bezierPath atPoint:_endPoint];
[bezierPath moveToPoint:CGPointMake(_startPoint.x, 0)];
[bezierPath addLineToPoint:CGPointMake(_midPoint.x, 0)];
[bezierPath addLineToPoint:CGPointMake(_endPoint.x, 0)];
[bezierPath addLineToPoint:CGPointMake(_endPoint.x, d)];
[bezierPath addLineToPoint:CGPointMake(_midPoint.x, d)];
[bezierPath addLineToPoint:CGPointMake(_startPoint.x, d)];
[bezierPath closePath];
}
return bezierPath.CGPath;
}
- (void)addRoundedEndpontToBezierPath:(UIBezierPath *)bezierPath atPoint:(CGPoint)point {
// Creates a closed circle at designated point.
[bezierPath moveToPoint:CGPointMake(point.x, _radius * 2)];
[bezierPath addArcWithCenter:point
radius:_radius
startAngle:0
endAngle:[self degreesToRadians:360]
clockwise:YES];
}
- (CGPoint)pointOnCircleWithRadius:(CGFloat)radius
angleInDegrees:(CGFloat)angleInDegrees
origin:(CGPoint)origin {
// Returns a point along a circles edge at given angle.
CGFloat locationX = (CGFloat)(radius * cos([self degreesToRadians:angleInDegrees])) + origin.x;
CGFloat locationY = (CGFloat)(radius * sin([self degreesToRadians:angleInDegrees])) + origin.y;
return CGPointMake(locationX, locationY);
}
- (CGFloat)degreesToRadians:(CGFloat)degrees {
return degrees * (CGFloat)M_PI / 180;
}
- (BOOL)isPointZero:(CGPoint)point {
return CGPointEqualToPoint(point, CGPointZero);
}
@end