You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
378 lines
11 KiB
378 lines
11 KiB
//
|
|
// M13ProgressViewMetro.m
|
|
// M13ProgressSuite
|
|
//
|
|
// Created by Brandon McQuilkin on 3/8/14.
|
|
// Copyright (c) 2014 Brandon McQuilkin. All rights reserved.
|
|
//
|
|
|
|
#import "M13ProgressViewMetro.h"
|
|
#import "M13ProgressViewMetroDotPolygon.h"
|
|
|
|
@interface M13ProgressViewMetro ()
|
|
|
|
/**The start progress for the progress animation.*/
|
|
@property (nonatomic, assign) CGFloat animationFromValue;
|
|
/**The end progress for the progress animation.*/
|
|
@property (nonatomic, assign) CGFloat animationToValue;
|
|
/**The start time interval for the animaiton.*/
|
|
@property (nonatomic, assign) CFTimeInterval animationStartTime;
|
|
/**Link to the display to keep animations in sync.*/
|
|
@property (nonatomic, strong) CADisplayLink *displayLink;
|
|
|
|
@end
|
|
|
|
@implementation M13ProgressViewMetroDot
|
|
{
|
|
M13ProgressViewAction currentAction;
|
|
}
|
|
|
|
#pragma mark Initalization
|
|
|
|
- (id)init
|
|
{
|
|
self = [super init];
|
|
if (self) {
|
|
[self setup];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (id)initWithCoder:(NSCoder *)aDecoder
|
|
{
|
|
self = [super initWithCoder:aDecoder];
|
|
if (self) {
|
|
[self setup];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (id)initWithLayer:(id)layer
|
|
{
|
|
self = [super initWithLayer:layer];
|
|
if (self) {
|
|
[self setup];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
+ (id)layer
|
|
{
|
|
return [[M13ProgressViewMetroDot alloc] init];
|
|
}
|
|
|
|
- (void)setup
|
|
{
|
|
_highlighted = NO;
|
|
currentAction = M13ProgressViewActionNone;
|
|
_primaryColor = [UIColor colorWithRed:0 green:122/255.0 blue:1.0 alpha:1.0];
|
|
_secondaryColor = [UIColor colorWithRed:181/255.0 green:182/255.0 blue:183/255.0 alpha:1.0];
|
|
_successColor = [UIColor colorWithRed:63.0f/255.0f green:226.0f/255.0f blue:80.0f/255.0f alpha:1];
|
|
_failureColor = [UIColor colorWithRed:249.0f/255.0f green:37.0f/255.0f blue:0 alpha:1];
|
|
}
|
|
|
|
#pragma mark Setters
|
|
|
|
- (void)setHighlighted:(BOOL)highlighted
|
|
{
|
|
_highlighted = highlighted;
|
|
[self setNeedsDisplay];
|
|
}
|
|
|
|
- (void)setSuccessColor:(UIColor *)successColor
|
|
{
|
|
_successColor = successColor;
|
|
[self setNeedsDisplay];
|
|
}
|
|
|
|
- (void)setFailureColor:(UIColor *)failureColor
|
|
{
|
|
_failureColor = failureColor;
|
|
[self setNeedsDisplay];
|
|
}
|
|
|
|
- (void)setPrimaryColor:(UIColor *)primaryColor
|
|
{
|
|
_primaryColor = primaryColor;
|
|
[self setNeedsDisplay];
|
|
}
|
|
|
|
- (void)setSecondaryColor:(UIColor *)secondaryColor
|
|
{
|
|
_secondaryColor = secondaryColor;
|
|
[self setNeedsDisplay];
|
|
}
|
|
|
|
- (void)performAction:(M13ProgressViewAction)action animated:(BOOL)animated
|
|
{
|
|
currentAction = action;
|
|
[self setNeedsDisplay];
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation M13ProgressViewMetro
|
|
{
|
|
BOOL animating;
|
|
NSUInteger currentNumberOfDots;
|
|
NSTimer *animationTimer;
|
|
UIBezierPath *animationPath;
|
|
NSMutableArray *dots;
|
|
|
|
CGFloat circleSize;
|
|
}
|
|
|
|
- (id)init
|
|
{
|
|
self = [super init];
|
|
if (self) {
|
|
[self setup];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (id)initWithFrame:(CGRect)frame
|
|
{
|
|
self = [super initWithFrame:frame];
|
|
if (self) {
|
|
[self setup];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (id)initWithCoder:(NSCoder *)aDecoder
|
|
{
|
|
self = [super initWithCoder:aDecoder];
|
|
if (self) {
|
|
[self setup];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)setup
|
|
{
|
|
//Set defaults
|
|
self.clipsToBounds = YES;
|
|
animating = NO;
|
|
_numberOfDots = 6;
|
|
_dotSize = CGSizeMake(20, 20);
|
|
self.animationDuration = 1.5;
|
|
self.animationShape = M13ProgressViewMetroAnimationShapeEllipse;
|
|
self.primaryColor = [UIColor colorWithRed:0 green:122/255.0 blue:1.0 alpha:1.0];
|
|
self.secondaryColor = [UIColor colorWithRed:181/255.0 green:182/255.0 blue:183/255.0 alpha:1.0];
|
|
_successColor = [UIColor colorWithRed:63.0f/255.0f green:226.0f/255.0f blue:80.0f/255.0f alpha:1];
|
|
_failureColor = [UIColor colorWithRed:249.0f/255.0f green:37.0f/255.0f blue:0 alpha:1];
|
|
_metroDot = [[M13ProgressViewMetroDotPolygon alloc] init];
|
|
}
|
|
|
|
#pragma mark Properties
|
|
|
|
- (void)setFrame:(CGRect)frame
|
|
{
|
|
[super setFrame:frame];
|
|
if (animating) {
|
|
[self stopAnimating];
|
|
[self beginAnimating];
|
|
}
|
|
}
|
|
|
|
- (BOOL)isAnimating
|
|
{
|
|
return animating;
|
|
}
|
|
|
|
- (void)setAnimationShape:(M13ProgressViewMetroAnimationShape)animationShape
|
|
{
|
|
_animationShape = animationShape;
|
|
if (_animationShape == M13ProgressViewMetroAnimationShapeEllipse) {
|
|
//Inset the size of the dot
|
|
CGFloat radius = MIN((self.bounds.size.width - _dotSize.width) / 2, (self.bounds.size.height - _dotSize.height) / 2);
|
|
//Create the path
|
|
animationPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 2) radius:radius startAngle:M_PI_2 endAngle:(-M_PI_2 * 3) clockwise:YES];
|
|
} else if (_animationShape == M13ProgressViewMetroAnimationShapeRectangle) {
|
|
//Inset the size of the dot
|
|
CGRect pathRect = CGRectInset(self.bounds, _dotSize.width, _dotSize.height);
|
|
//Create the path
|
|
animationPath = [UIBezierPath bezierPath];
|
|
[animationPath moveToPoint:CGPointMake((pathRect.size.width / 2) + pathRect.origin.x, pathRect.size.height + pathRect.origin.y)];
|
|
[animationPath addLineToPoint:CGPointMake(pathRect.origin.x, pathRect.size.height + pathRect.origin.y)];
|
|
[animationPath addLineToPoint:CGPointMake(pathRect.origin.x, pathRect.origin.y)];
|
|
[animationPath addLineToPoint:CGPointMake(pathRect.origin.x + pathRect.size.width, pathRect.origin.y)];
|
|
[animationPath addLineToPoint:CGPointMake(pathRect.origin.x + pathRect.size.width, pathRect.origin.y + pathRect.size.height)];
|
|
[animationPath moveToPoint:CGPointMake((pathRect.size.width / 2) + pathRect.origin.x, pathRect.size.height + pathRect.origin.y)];
|
|
} else if (animationShape == M13ProgressViewMetroAnimationShapeLine) {
|
|
//Create the path
|
|
animationPath = [UIBezierPath bezierPath];
|
|
[animationPath moveToPoint:CGPointMake(-_dotSize.width, self.bounds.size.height / 2)];
|
|
[animationPath addLineToPoint:CGPointMake(self.bounds.size.width + _dotSize.width, self.bounds.size.height / 2)];
|
|
}
|
|
[self stopAnimating];
|
|
[self beginAnimating];
|
|
}
|
|
|
|
- (void)setNumberOfDots:(NSUInteger)numberOfDots
|
|
{
|
|
_numberOfDots = numberOfDots;
|
|
[self invalidateIntrinsicContentSize];
|
|
[self stopAnimating];
|
|
[self beginAnimating];
|
|
}
|
|
|
|
- (void)setDotSize:(CGSize)dotSize
|
|
{
|
|
_dotSize = dotSize;
|
|
[self invalidateIntrinsicContentSize];
|
|
[self stopAnimating];
|
|
[self beginAnimating];
|
|
}
|
|
|
|
- (void)performAction:(M13ProgressViewAction)action animated:(BOOL)animated
|
|
{
|
|
for (M13ProgressViewMetroDot *dot in dots) {
|
|
[dot performAction:action animated:animated];
|
|
}
|
|
}
|
|
|
|
- (void)setProgress:(CGFloat)progress animated:(BOOL)animated
|
|
{
|
|
if (animated == NO) {
|
|
if (_displayLink) {
|
|
//Kill running animations
|
|
[_displayLink invalidate];
|
|
_displayLink = nil;
|
|
}
|
|
[super setProgress:progress animated:NO];
|
|
[self showProgress];
|
|
} else {
|
|
_animationStartTime = CACurrentMediaTime();
|
|
_animationFromValue = self.progress;
|
|
_animationToValue = progress;
|
|
if (!_displayLink) {
|
|
//Create and setup the display link
|
|
[self.displayLink invalidate];
|
|
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(animateProgress:)];
|
|
[self.displayLink addToRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes];
|
|
} /*else {
|
|
//Reuse the current display link
|
|
}*/
|
|
}
|
|
}
|
|
|
|
- (void)animateProgress:(CADisplayLink *)displayLink
|
|
{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
CGFloat dt = (displayLink.timestamp - self.animationStartTime) / self.animationDuration;
|
|
if (dt >= 1.0) {
|
|
//Order is important! Otherwise concurrency will cause errors, because setProgress: will detect an animation in progress and try to stop it by itself. Once over one, set to actual progress amount. Animation is over.
|
|
[self.displayLink invalidate];
|
|
self.displayLink = nil;
|
|
[super setProgress:self.animationToValue animated:NO];
|
|
[self showProgress];
|
|
return;
|
|
}
|
|
|
|
//Set progress
|
|
[super setProgress:self.animationFromValue + dt * (self.animationToValue - self.animationFromValue) animated:YES];
|
|
[self showProgress];
|
|
|
|
});
|
|
}
|
|
|
|
- (void)showProgress
|
|
{
|
|
static int pastIndexToHighlightTo = 0;
|
|
int indexToHighlightTo = (int)ceilf(_numberOfDots * (float)self.progress);
|
|
//Only perform the animation if necessary.
|
|
if (pastIndexToHighlightTo != indexToHighlightTo) {
|
|
for (int i = 0; i < _numberOfDots; i++) {
|
|
M13ProgressViewMetroDot *dot = dots[i];
|
|
if (i <= indexToHighlightTo && self.progress != 0) {
|
|
dot.highlighted = YES;
|
|
} else {
|
|
dot.highlighted = NO;
|
|
}
|
|
}
|
|
pastIndexToHighlightTo = indexToHighlightTo;
|
|
}
|
|
}
|
|
|
|
- (CGSize)intrinsicContentSize
|
|
{
|
|
//No real constraint on size.
|
|
if (_animationShape == M13ProgressViewMetroAnimationShapeEllipse || _animationShape == M13ProgressViewMetroAnimationShapeRectangle) {
|
|
return CGSizeMake(3 * _dotSize.width, 3 * _dotSize.height);
|
|
} else {
|
|
return CGSizeMake(_dotSize.width * _numberOfDots, _dotSize.height);
|
|
}
|
|
}
|
|
|
|
#pragma mark Animation
|
|
|
|
-(void)beginAnimating
|
|
{
|
|
if (!animating){
|
|
|
|
animating = YES;
|
|
|
|
currentNumberOfDots = 0;
|
|
dots = [[NSMutableArray alloc] init];
|
|
|
|
//add circles
|
|
animationTimer = [NSTimer scheduledTimerWithTimeInterval: 0.20 target: self
|
|
selector: @selector(createCircle) userInfo: nil repeats: YES];
|
|
}
|
|
}
|
|
|
|
-(void)createCircle
|
|
{
|
|
if (currentNumberOfDots<_numberOfDots){
|
|
|
|
currentNumberOfDots ++;
|
|
CGRect f;
|
|
if (_animationShape != M13ProgressViewMetroAnimationShapeLine) {
|
|
f = CGRectMake((self.frame.size.width - _dotSize.width) / 2 - 1, self.frame.size.height - _dotSize.height - 1, _dotSize.width, _dotSize.height);
|
|
} else {
|
|
f = CGRectMake(- _dotSize.width, self.bounds.size.height / 2, _dotSize.width, _dotSize.height);
|
|
}
|
|
M13ProgressViewMetroDot *dotLayer = [_metroDot copy];
|
|
dotLayer.frame = f;
|
|
[self.layer addSublayer:dotLayer];
|
|
[dots addObject:dotLayer];
|
|
|
|
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
|
|
animation.duration = self.animationDuration;
|
|
animation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.15f :0.60f :0.85f :0.4f];
|
|
[animation setCalculationMode:kCAAnimationPaced];
|
|
animation.path = animationPath.CGPath;
|
|
animation.repeatCount = HUGE_VALF;
|
|
|
|
if (_animationShape != M13ProgressViewMetroAnimationShapeLine) {
|
|
//No delay needed
|
|
[dotLayer addAnimation:animation forKey:@"metroAnimation"];
|
|
} else {
|
|
//Delay repeat
|
|
animation.repeatCount = 1;
|
|
CAAnimationGroup *group = [CAAnimationGroup animation];
|
|
group.duration = self.animationDuration * 2;
|
|
group.animations = @[animation];
|
|
group.repeatCount = HUGE_VALF;
|
|
[dotLayer addAnimation:group forKey:@"metroAnimation"];
|
|
}
|
|
|
|
|
|
} else {
|
|
[animationTimer invalidate];
|
|
}
|
|
}
|
|
|
|
-(void)stopAnimating
|
|
{
|
|
animating = NO;
|
|
[animationTimer invalidate];
|
|
for (CALayer *layer in dots) {
|
|
[layer removeAllAnimations];
|
|
[layer removeFromSuperlayer];
|
|
}
|
|
}
|
|
|
|
@end
|