// // DACircularProgressView.m // DACircularProgress // // Created by Daniel Amitay on 2/6/12. // Copyright (c) 2012 Daniel Amitay. All rights reserved. // #import "DACircularProgressView.h" #import @interface DACircularProgressLayer : CALayer @property(nonatomic, strong) UIColor *trackTintColor; @property(nonatomic, strong) UIColor *progressTintColor; @property(nonatomic, strong) UIColor *innerTintColor; @property(nonatomic) NSInteger roundedCorners; @property(nonatomic) CGFloat thicknessRatio; @property(nonatomic) CGFloat progress; @property(nonatomic) NSInteger clockwiseProgress; @end @implementation DACircularProgressLayer @dynamic trackTintColor; @dynamic progressTintColor; @dynamic innerTintColor; @dynamic roundedCorners; @dynamic thicknessRatio; @dynamic progress; @dynamic clockwiseProgress; + (BOOL)needsDisplayForKey:(NSString *)key { if ([key isEqualToString:@"progress"]) { return YES; } else { return [super needsDisplayForKey:key]; } } - (void)drawInContext:(CGContextRef)context { CGRect rect = self.bounds; CGPoint centerPoint = CGPointMake(rect.size.width / 2.0f, rect.size.height / 2.0f); CGFloat radius = MIN(rect.size.height, rect.size.width) / 2.0f; BOOL clockwise = (self.clockwiseProgress != 0); CGFloat progress = MIN(self.progress, 1.0f - FLT_EPSILON); CGFloat radians = 0; if (clockwise) { radians = (float)((progress * 2.0f * M_PI) - M_PI_2); } else { radians = (float)(3 * M_PI_2 - (progress * 2.0f * M_PI)); } CGContextSetFillColorWithColor(context, self.trackTintColor.CGColor); CGMutablePathRef trackPath = CGPathCreateMutable(); CGPathMoveToPoint(trackPath, NULL, centerPoint.x, centerPoint.y); CGPathAddArc(trackPath, NULL, centerPoint.x, centerPoint.y, radius, (float)(2.0f * M_PI), 0.0f, TRUE); CGPathCloseSubpath(trackPath); CGContextAddPath(context, trackPath); CGContextFillPath(context); CGPathRelease(trackPath); if (progress > 0.0f) { CGContextSetFillColorWithColor(context, self.progressTintColor.CGColor); CGMutablePathRef progressPath = CGPathCreateMutable(); CGPathMoveToPoint(progressPath, NULL, centerPoint.x, centerPoint.y); CGPathAddArc(progressPath, NULL, centerPoint.x, centerPoint.y, radius, (float)(3.0f * M_PI_2), radians, !clockwise); CGPathCloseSubpath(progressPath); CGContextAddPath(context, progressPath); CGContextFillPath(context); CGPathRelease(progressPath); } if (progress > 0.0f && self.roundedCorners) { CGFloat pathWidth = radius * self.thicknessRatio; CGFloat xOffset = radius * (1.0f + ((1.0f - (self.thicknessRatio / 2.0f)) * cosf(radians))); CGFloat yOffset = radius * (1.0f + ((1.0f - (self.thicknessRatio / 2.0f)) * sinf(radians))); CGPoint endPoint = CGPointMake(xOffset, yOffset); CGRect startEllipseRect = (CGRect) { .origin.x = centerPoint.x - pathWidth / 2.0f, .origin.y = 0.0f, .size.width = pathWidth, .size.height = pathWidth }; CGContextAddEllipseInRect(context, startEllipseRect); CGContextFillPath(context); CGRect endEllipseRect = (CGRect) { .origin.x = endPoint.x - pathWidth / 2.0f, .origin.y = endPoint.y - pathWidth / 2.0f, .size.width = pathWidth, .size.height = pathWidth }; CGContextAddEllipseInRect(context, endEllipseRect); CGContextFillPath(context); } CGContextSetBlendMode(context, kCGBlendModeClear); CGFloat innerRadius = radius * (1.0f - self.thicknessRatio); CGRect clearRect = (CGRect) { .origin.x = centerPoint.x - innerRadius, .origin.y = centerPoint.y - innerRadius, .size.width = innerRadius * 2.0f, .size.height = innerRadius * 2.0f }; CGContextAddEllipseInRect(context, clearRect); CGContextFillPath(context); if (self.innerTintColor) { CGContextSetBlendMode(context, kCGBlendModeNormal); CGContextSetFillColorWithColor(context, [self.innerTintColor CGColor]); CGContextAddEllipseInRect(context, clearRect); CGContextFillPath(context); } } @end @interface DACircularProgressView () @end @implementation DACircularProgressView + (void) initialize { if (self == [DACircularProgressView class]) { DACircularProgressView *circularProgressViewAppearance = [DACircularProgressView appearance]; [circularProgressViewAppearance setTrackTintColor:[[UIColor whiteColor] colorWithAlphaComponent:0.3f]]; [circularProgressViewAppearance setProgressTintColor:[UIColor whiteColor]]; [circularProgressViewAppearance setInnerTintColor:nil]; [circularProgressViewAppearance setBackgroundColor:[UIColor clearColor]]; [circularProgressViewAppearance setThicknessRatio:0.3f]; [circularProgressViewAppearance setRoundedCorners:NO]; [circularProgressViewAppearance setClockwiseProgress:YES]; [circularProgressViewAppearance setIndeterminateDuration:2.0f]; [circularProgressViewAppearance setIndeterminate:NO]; } } + (Class)layerClass { return [DACircularProgressLayer class]; } - (DACircularProgressLayer *)circularProgressLayer { return (DACircularProgressLayer *)self.layer; } - (id)init { return [super initWithFrame:CGRectMake(0.0f, 0.0f, 40.0f, 40.0f)]; } - (void)didMoveToWindow { [super didMoveToWindow]; CGFloat windowContentsScale = self.window.screen.scale; self.circularProgressLayer.contentsScale = windowContentsScale; [self.circularProgressLayer setNeedsDisplay]; } #pragma mark - Progress - (CGFloat)progress { return self.circularProgressLayer.progress; } - (void)setProgress:(CGFloat)progress { [self setProgress:progress animated:NO]; } - (void)setProgress:(CGFloat)progress animated:(BOOL)animated { [self setProgress:progress animated:animated initialDelay:0.0]; } - (void)setProgress:(CGFloat)progress animated:(BOOL)animated initialDelay:(CFTimeInterval)initialDelay { CGFloat pinnedProgress = MIN(MAX(progress, 0.0f), 1.0f); [self setProgress:progress animated:animated initialDelay:initialDelay withDuration:fabs(self.progress - pinnedProgress)]; } - (void)setProgress:(CGFloat)progress animated:(BOOL)animated initialDelay:(CFTimeInterval)initialDelay withDuration:(CFTimeInterval)duration { [self.layer removeAnimationForKey:@"indeterminateAnimation"]; [self.circularProgressLayer removeAnimationForKey:@"progress"]; CGFloat pinnedProgress = MIN(MAX(progress, 0.0f), 1.0f); if (animated) { CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"progress"]; animation.duration = duration; animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; animation.fillMode = kCAFillModeForwards; animation.fromValue = [NSNumber numberWithFloat:self.progress]; animation.toValue = [NSNumber numberWithFloat:pinnedProgress]; animation.beginTime = CACurrentMediaTime() + initialDelay; animation.delegate = self; [self.circularProgressLayer addAnimation:animation forKey:@"progress"]; } else { [self.circularProgressLayer setNeedsDisplay]; self.circularProgressLayer.progress = pinnedProgress; } } - (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)flag { NSNumber *pinnedProgressNumber = [animation valueForKey:@"toValue"]; self.circularProgressLayer.progress = [pinnedProgressNumber floatValue]; } #pragma mark - UIAppearance methods - (UIColor *)trackTintColor { return self.circularProgressLayer.trackTintColor; } - (void)setTrackTintColor:(UIColor *)trackTintColor { self.circularProgressLayer.trackTintColor = trackTintColor; [self.circularProgressLayer setNeedsDisplay]; } - (UIColor *)progressTintColor { return self.circularProgressLayer.progressTintColor; } - (void)setProgressTintColor:(UIColor *)progressTintColor { self.circularProgressLayer.progressTintColor = progressTintColor; [self.circularProgressLayer setNeedsDisplay]; } - (UIColor *)innerTintColor { return self.circularProgressLayer.innerTintColor; } - (void)setInnerTintColor:(UIColor *)innerTintColor { self.circularProgressLayer.innerTintColor = innerTintColor; [self.circularProgressLayer setNeedsDisplay]; } - (NSInteger)roundedCorners { return self.roundedCorners; } - (void)setRoundedCorners:(NSInteger)roundedCorners { self.circularProgressLayer.roundedCorners = roundedCorners; [self.circularProgressLayer setNeedsDisplay]; } - (CGFloat)thicknessRatio { return self.circularProgressLayer.thicknessRatio; } - (void)setThicknessRatio:(CGFloat)thicknessRatio { self.circularProgressLayer.thicknessRatio = MIN(MAX(thicknessRatio, 0.f), 1.f); [self.circularProgressLayer setNeedsDisplay]; } - (NSInteger)indeterminate { CAAnimation *spinAnimation = [self.layer animationForKey:@"indeterminateAnimation"]; return (spinAnimation == nil ? 0 : 1); } - (void)setIndeterminate:(NSInteger)indeterminate { if (indeterminate) { if (!self.indeterminate) { CABasicAnimation *spinAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"]; spinAnimation.byValue = [NSNumber numberWithDouble:indeterminate > 0 ? 2.0f*M_PI : -2.0f*M_PI]; spinAnimation.duration = self.indeterminateDuration; spinAnimation.repeatCount = HUGE_VALF; [self.layer addAnimation:spinAnimation forKey:@"indeterminateAnimation"]; } } else { [self.layer removeAnimationForKey:@"indeterminateAnimation"]; } } - (NSInteger)clockwiseProgress { return self.circularProgressLayer.clockwiseProgress; } - (void)setClockwiseProgress:(NSInteger)clockwiseProgres { self.circularProgressLayer.clockwiseProgress = clockwiseProgres; [self.circularProgressLayer setNeedsDisplay]; } @end