// // M13ProgressViewRadiative.m // M13ProgressSuite // // Created by Brandon McQuilkin on 3/13/14. // Copyright (c) 2014 Brandon McQuilkin. All rights reserved. // #import "M13ProgressViewRadiative.h" #import @interface M13ProgressViewRadiative () /**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 M13ProgressViewRadiative { NSMutableArray *ripplePaths; } - (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 own background color self.backgroundColor = [UIColor clearColor]; self.clipsToBounds = YES; //Set defauts self.animationDuration = 1.0; _originationPoint = CGPointMake(0.5, 0.5); self.numberOfRipples = 10; self.shape = M13ProgressViewRadiativeShapeCircle; _rippleWidth = 1.0; _ripplesRadius = 20; _pulseWidth = 5; _progressOutwards = YES; //Set default colors 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]; } #pragma mark Setters - (void)setOriginationPoint:(CGPoint)originationPoint { _originationPoint = originationPoint; [self setNeedsLayout]; [self setNeedsDisplay]; } - (void)setRipplesRadius:(CGFloat)ripplesRadius { _ripplesRadius = ripplesRadius; [self setNeedsLayout]; [self setNeedsDisplay]; } - (void)setNumberOfRipples:(NSUInteger)numberOfRipples { _numberOfRipples = numberOfRipples; [self setNeedsLayout]; [self setNeedsDisplay]; } - (void)setRippleWidth:(CGFloat)rippleWidth { _rippleWidth = rippleWidth; for (UIBezierPath *path in ripplePaths) { path.lineWidth = _rippleWidth; } [self setIndeterminate:self.indeterminate]; } - (void)setShape:(M13ProgressViewRadiativeShape)shape { _shape = shape; [self setNeedsLayout]; [self setNeedsDisplay]; } - (void)setPulseWidth:(NSUInteger)pulseWidth { _pulseWidth = pulseWidth; self.indeterminate = self.indeterminate; } - (void)setProgressOutwards:(BOOL)progressOutwards { _progressOutwards = progressOutwards; [self setNeedsDisplay]; } - (void)setPrimaryColor:(UIColor *)primaryColor { [super setPrimaryColor:primaryColor]; [self setNeedsDisplay]; } - (void)setSecondaryColor:(UIColor *)secondaryColor { [super setSecondaryColor:secondaryColor]; [self setNeedsDisplay]; } #pragma mark animations - (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 setNeedsDisplay]; } 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 setNeedsDisplay]; return; } //Set progress [super setProgress:self.animationFromValue + dt * (self.animationToValue - self.animationFromValue) animated:YES]; [self setNeedsDisplay]; }); } - (void)setIndeterminate:(BOOL)indeterminate { [super setIndeterminate:indeterminate]; //Need animation } #pragma mark Layout - (void)layoutSubviews { [super layoutSubviews]; //Create the paths to draw the ripples ripplePaths = [NSMutableArray array]; for (int i = 0; i < _numberOfRipples - 1; i++) { if (_shape == M13ProgressViewRadiativeShapeCircle) { //If circular UIBezierPath *path = [UIBezierPath bezierPath]; //Calculate the radius CGFloat radius = _ripplesRadius * ((float)i / (float)(_numberOfRipples - 1)); //Draw the arc [path moveToPoint:CGPointMake((_originationPoint.x * self.bounds.size.width)+ radius, _originationPoint.y * self.bounds.size.height)]; [path addArcWithCenter:CGPointMake(self.bounds.size.width * _originationPoint.x, self.bounds.size.height * _originationPoint.y) radius:radius startAngle:0.0 endAngle:(2 * M_PI) clockwise:YES]; //Set the width path.lineWidth = _rippleWidth; [ripplePaths addObject:path]; } else if (_shape == M13ProgressViewRadiativeShapeSquare) { //If square CGFloat radius = _ripplesRadius * ((float)i / (float)(_numberOfRipples - 1)); CGFloat delta = radius * (1 / sqrtf(2)); UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake((_originationPoint.x * self.bounds.size.width) - delta, (_originationPoint.y * self.bounds.size.height) - delta, delta * 2, delta * 2)]; path.lineWidth = _rippleWidth; [ripplePaths addObject:path]; } } } - (CGSize)intrinsicContentSize { //The width and height should be set with constraints. Can't think of a good way to figure out the minimum size with the point and scale based size calculations. return CGSizeMake(UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric); } #pragma mark Drawing - (void)drawRect:(CGRect)rect { [super drawRect:rect]; //Get the current context CGContextRef context = UIGraphicsGetCurrentContext(); //For each of the paths draw it in the view. NSEnumerator *enumerator; if (_progressOutwards) { enumerator = [ripplePaths objectEnumerator]; } else { enumerator = [ripplePaths reverseObjectEnumerator]; } UIBezierPath *path; int i = 0; int indexOfLastFilledPath = (int)ceilf((float)self.progress * (float)_numberOfRipples); while ((path = [enumerator nextObject])) { //Set the path's color if (!self.indeterminate) { //Show progress if (i <= indexOfLastFilledPath && self.progress != 0) { //Highlighted CGContextSetStrokeColorWithColor(context, self.primaryColor.CGColor); CGContextAddPath(context, path.CGPath); CGContextStrokePath(context); } else { //Not highlighted CGContextSetStrokeColorWithColor(context, self.secondaryColor.CGColor); CGContextAddPath(context, path.CGPath); CGContextStrokePath(context); } i++; } else { //Indeterminate } } } @end