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.
444 lines
11 KiB
444 lines
11 KiB
// |
|
// UAProgressView.m |
|
// UAProgressView-Example |
|
// |
|
// Created by Matt Coneybeare on 5/25/14. |
|
// Copyright (c) 2014 Urban Apps. All rights reserved. |
|
// |
|
|
|
#import "UAProgressView.h" |
|
|
|
NSString * const UAProgressViewProgressAnimationKey = @"UAProgressViewProgressAnimationKey"; |
|
|
|
@interface UACircularProgressView : UIView |
|
|
|
- (void)updateProgress:(CGFloat)progress; |
|
- (CAShapeLayer *)shapeLayer; |
|
|
|
@end |
|
|
|
@interface UAProgressView () <UIGestureRecognizerDelegate> |
|
|
|
@property (nonatomic, strong) UACircularProgressView *progressView; |
|
@property (nonatomic, assign) int valueLabelProgressPercentDifference; |
|
@property (nonatomic, strong) NSTimer *valueLabelUpdateTimer; |
|
@property (nonatomic, strong) NSTimer *longPressTimer; |
|
|
|
@end |
|
|
|
@implementation UAProgressView |
|
@synthesize tintColor = _tintColor; |
|
|
|
#pragma mark - Init |
|
|
|
- (id)initWithFrame:(CGRect)frame { |
|
self = [super initWithFrame:frame]; |
|
if (self) { |
|
[self sharedSetup]; |
|
} |
|
return self; |
|
} |
|
|
|
- (id)initWithCoder:(NSCoder *)aDecoder { |
|
self = [super initWithCoder:aDecoder]; |
|
if (self) { |
|
[self sharedSetup]; |
|
} |
|
return self; |
|
} |
|
|
|
- (void)sharedSetup { |
|
self.progressView = [[UACircularProgressView alloc] initWithFrame:self.bounds]; |
|
self.progressView.shapeLayer.fillColor = [UIColor clearColor].CGColor; |
|
[self addSubview:self.progressView]; |
|
|
|
[self resetDefaults]; |
|
} |
|
|
|
- (void)resetDefaults { |
|
|
|
self.fillChangedBlock = nil; |
|
self.didSelectBlock = nil; |
|
self.progressChangedBlock = nil; |
|
self.centralView = nil; |
|
|
|
_fillOnTouch = YES; |
|
_progress = 0.0; |
|
_animationDuration = 0.3f; |
|
_longPressDuration = 0.0f; |
|
_longPressCancelsSelect = NO; |
|
|
|
self.borderWidth = 1.0f; |
|
self.lineWidth = 2.0f; |
|
|
|
[self setupGestureRecognizer]; |
|
|
|
[self tintColorDidChange]; |
|
} |
|
|
|
- (void)setupGestureRecognizer { |
|
// while this is a long press gesture, it is actually recognizing any presses < longPressDuration |
|
_gestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(touchDetected:)]; |
|
_gestureRecognizer.delegate = self; |
|
_gestureRecognizer.minimumPressDuration = 0.0; |
|
[self addGestureRecognizer:_gestureRecognizer]; |
|
} |
|
|
|
#pragma mark - Public Accessors |
|
|
|
|
|
- (void)setBorderWidth:(CGFloat)borderWidth { |
|
_borderWidth = borderWidth; |
|
self.progressView.shapeLayer.borderWidth = borderWidth; |
|
} |
|
|
|
- (void)setLineWidth:(CGFloat)lineWidth { |
|
_lineWidth = lineWidth; |
|
self.progressView.shapeLayer.lineWidth = lineWidth; |
|
} |
|
|
|
- (void)setCentralView:(UIView *)centralView { |
|
if (_centralView != centralView) { |
|
[_centralView removeFromSuperview]; |
|
_centralView = centralView; |
|
[self addSubview:self.centralView]; |
|
} |
|
} |
|
|
|
#pragma mark - Color |
|
|
|
- (void)tintColorDidChange { |
|
if ([[self superclass] instancesRespondToSelector: @selector(tintColorDidChange)]) { |
|
[super tintColorDidChange]; |
|
} |
|
|
|
UIColor *tintColor = self.tintColor; |
|
|
|
self.progressView.shapeLayer.strokeColor = tintColor.CGColor; |
|
self.progressView.shapeLayer.borderColor = tintColor.CGColor; |
|
} |
|
|
|
- (UIColor*) tintColor |
|
{ |
|
if (_tintColor == nil) { |
|
_tintColor = [UIColor colorWithRed: 0.0 green: 122.0/255.0 blue: 1.0 alpha: 1.0]; |
|
} |
|
return _tintColor; |
|
} |
|
|
|
- (void) setTintColor:(UIColor *)tintColor |
|
{ |
|
[self willChangeValueForKey: @"tintColor"]; |
|
_tintColor = tintColor; |
|
[self didChangeValueForKey: @"tintColor"]; |
|
[self tintColorDidChange]; |
|
} |
|
|
|
- (UIColor*) fillColor |
|
{ |
|
if (_fillColor == nil) { |
|
return _tintColor; |
|
} |
|
return _fillColor; |
|
} |
|
|
|
#pragma mark - Layout |
|
|
|
- (void)layoutSubviews { |
|
[super layoutSubviews]; |
|
|
|
self.progressView.frame = self.bounds; |
|
self.centralView.center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); |
|
} |
|
|
|
#pragma mark - Progress Control |
|
|
|
- (void)setProgress:(CGFloat)progress animated:(BOOL)animated { |
|
|
|
progress = MAX( MIN(progress, 1.0), 0.0); // keep it between 0 and 1 |
|
|
|
if (_progress == progress) { |
|
return; |
|
} |
|
|
|
if (animated) { |
|
|
|
[self animateToProgress:progress]; |
|
|
|
} else { |
|
|
|
[self stopAnimation]; |
|
_progress = progress; |
|
[self.progressView updateProgress:_progress]; |
|
|
|
} |
|
|
|
if (self.progressChangedBlock) { |
|
self.progressChangedBlock(self, _progress); |
|
} |
|
} |
|
|
|
- (void)setProgress:(CGFloat)progress { |
|
[self setProgress:progress animated:NO]; |
|
} |
|
|
|
- (void)setAnimationDuration:(CFTimeInterval)animationDuration { |
|
if (_animationDuration < 0) |
|
return; |
|
|
|
_animationDuration = animationDuration; |
|
} |
|
|
|
- (void)animateToProgress:(CGFloat)progress { |
|
[self stopAnimation]; |
|
|
|
// Add shape animation |
|
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"]; |
|
animation.removedOnCompletion = NO; |
|
animation.fillMode = kCAFillModeForwards; |
|
animation.duration = self.animationDuration; |
|
animation.fromValue = @(self.progress); |
|
animation.toValue = @(progress); |
|
animation.delegate = self; |
|
[self.progressView.layer addAnimation:animation forKey:UAProgressViewProgressAnimationKey]; |
|
|
|
// Add timer to update valueLabel |
|
_valueLabelProgressPercentDifference = (progress - self.progress) * 100; |
|
CFTimeInterval timerInterval = self.animationDuration / ABS(_valueLabelProgressPercentDifference); |
|
self.valueLabelUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:timerInterval |
|
target:self |
|
selector:@selector(onValueLabelUpdateTimer:) |
|
userInfo:nil |
|
repeats:YES]; |
|
|
|
|
|
_progress = progress; |
|
} |
|
|
|
- (void)stopAnimation { |
|
// Stop running animation |
|
[self.progressView.layer removeAnimationForKey:UAProgressViewProgressAnimationKey]; |
|
|
|
// Stop timer |
|
[self.valueLabelUpdateTimer invalidate]; |
|
self.valueLabelUpdateTimer = nil; |
|
} |
|
|
|
- (void)onValueLabelUpdateTimer:(NSTimer *)timer { |
|
if (_valueLabelProgressPercentDifference > 0) { |
|
_valueLabelProgressPercentDifference--; |
|
} else { |
|
_valueLabelProgressPercentDifference++; |
|
} |
|
} |
|
|
|
#pragma mark - Highlighting |
|
|
|
- (void)addFill { |
|
if (self.fillOnTouch) { |
|
// update the layer model |
|
self.progressView.layer.backgroundColor = [self fillColor].CGColor; |
|
|
|
// call block |
|
if (self.fillChangedBlock) { |
|
self.fillChangedBlock(self, YES, NO); |
|
} |
|
} |
|
} |
|
|
|
- (void)removeFillAnimated:(BOOL)animated { |
|
if (self.fillOnTouch) { |
|
|
|
// add the fade-out animation |
|
if (animated) { |
|
CABasicAnimation *highlightAnimation = [CABasicAnimation animationWithKeyPath:@"backgroundColor"]; |
|
highlightAnimation.fromValue = (id)self.progressView.layer.backgroundColor; |
|
highlightAnimation.toValue = (id)[UIColor clearColor].CGColor; |
|
highlightAnimation.removedOnCompletion = NO; |
|
[self.progressView.layer addAnimation:highlightAnimation forKey:@"backgroundColor"]; |
|
} |
|
|
|
// update the layer model. |
|
self.progressView.layer.backgroundColor = [UIColor clearColor].CGColor; |
|
|
|
// call block |
|
if (self.fillChangedBlock) { |
|
self.fillChangedBlock(self, NO, animated); |
|
} |
|
} |
|
} |
|
|
|
- (void)removeFill { |
|
[self removeFillAnimated:YES]; |
|
} |
|
|
|
#pragma mark - CAAnimationDelegate |
|
|
|
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag { |
|
[self.progressView updateProgress:_progress]; |
|
[self.valueLabelUpdateTimer invalidate]; |
|
self.valueLabelUpdateTimer = nil; |
|
} |
|
|
|
#pragma mark - Gesture Recognizers |
|
|
|
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { |
|
if (self.centralView && [touch.view isDescendantOfView:self.centralView] && self.centralView.userInteractionEnabled) { |
|
return NO; |
|
} |
|
|
|
return YES; |
|
} |
|
|
|
- (void)touchDetected:(UILongPressGestureRecognizer *)gestureRecognizer { |
|
|
|
CGPoint touch = [gestureRecognizer locationOfTouch:0 inView:self]; |
|
|
|
if (UIGestureRecognizerStateBegan == gestureRecognizer.state) { // press is being held down |
|
|
|
[self addFill]; |
|
|
|
[self startLongPressTimer]; |
|
|
|
} else if (UIGestureRecognizerStateChanged == gestureRecognizer.state) { // press was recognized, but then moved |
|
|
|
if (CGRectContainsPoint(self.bounds, touch)) { |
|
|
|
[self addFill]; |
|
|
|
if (self.longPressTimer == nil) { |
|
[self startLongPressTimer]; |
|
} |
|
|
|
} else { |
|
|
|
[self removeFillAnimated:NO]; |
|
|
|
[self stopLongPressTimer]; |
|
} |
|
|
|
} else if (UIGestureRecognizerStateEnded == gestureRecognizer.state) { // the touch has been picked up |
|
|
|
if (CGRectContainsPoint(self.bounds, touch)) { |
|
|
|
[self removeFill]; |
|
|
|
if (self.didSelectBlock) { |
|
self.didSelectBlock(self); |
|
} |
|
|
|
} else { |
|
|
|
[self removeFillAnimated:NO]; |
|
|
|
} |
|
|
|
[self stopLongPressTimer]; |
|
|
|
} else { |
|
|
|
[self removeFillAnimated:NO]; |
|
|
|
[self stopLongPressTimer]; |
|
|
|
} |
|
|
|
} |
|
|
|
- (void)stopLongPressTimer |
|
{ |
|
if (self.longPressTimer != nil) { |
|
[self.longPressTimer invalidate]; |
|
self.longPressTimer = nil; |
|
} |
|
} |
|
|
|
- (void)startLongPressTimer |
|
{ |
|
if (self.longPressDuration > 0.0) { |
|
[self.longPressTimer invalidate]; |
|
self.longPressTimer = [NSTimer scheduledTimerWithTimeInterval:_longPressDuration |
|
target:self |
|
selector:@selector(longPressTimerFired:) |
|
userInfo:nil |
|
repeats:NO]; |
|
} |
|
} |
|
|
|
- (void)longPressTimerFired:(NSTimer *)timer { |
|
if (_longPressCancelsSelect) { |
|
_gestureRecognizer.enabled = NO; |
|
_gestureRecognizer.enabled = YES; |
|
} |
|
|
|
if (self.didLongPressBlock) { |
|
self.didLongPressBlock(self); |
|
} |
|
} |
|
|
|
- (void)setLongPressDuration:(CGFloat)longPressDuration |
|
{ |
|
longPressDuration = MAX(0.0, longPressDuration); // keep it above 0.0 |
|
|
|
if (_longPressDuration == longPressDuration) { |
|
return; |
|
} else { |
|
_longPressDuration = longPressDuration; |
|
} |
|
} |
|
|
|
@end |
|
|
|
#pragma mark - UACircularProgressView |
|
|
|
@implementation UACircularProgressView |
|
|
|
+ (Class)layerClass { |
|
return CAShapeLayer.class; |
|
} |
|
|
|
- (CAShapeLayer *)shapeLayer { |
|
return (CAShapeLayer *)self.layer; |
|
} |
|
|
|
- (instancetype)initWithFrame:(CGRect)frame { |
|
if (self = [super initWithFrame:frame]) { |
|
[self updateProgress:0]; |
|
} |
|
|
|
return self; |
|
} |
|
|
|
- (void)layoutSubviews { |
|
[super layoutSubviews]; |
|
|
|
self.shapeLayer.cornerRadius = self.frame.size.width / 2.0f; |
|
self.shapeLayer.path = [self layoutPath].CGPath; |
|
} |
|
|
|
- (UIBezierPath *)layoutPath { |
|
const double TWO_M_PI = 2.0 * M_PI; |
|
const double startAngle = 0.75 * TWO_M_PI; |
|
const double endAngle = startAngle + TWO_M_PI; |
|
|
|
CGFloat width = self.frame.size.width; |
|
CGFloat borderWidth = self.shapeLayer.borderWidth; |
|
return [UIBezierPath bezierPathWithArcCenter:CGPointMake(width/2.0f, width/2.0f) |
|
radius:width/2.0f - borderWidth |
|
startAngle:startAngle |
|
endAngle:endAngle |
|
clockwise:YES]; |
|
} |
|
|
|
- (void)updateProgress:(CGFloat)progress { |
|
[self updatePath:progress]; |
|
} |
|
|
|
- (void)updatePath:(CGFloat)progress { |
|
[CATransaction begin]; |
|
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions]; |
|
self.shapeLayer.strokeEnd = progress; |
|
[CATransaction commit]; |
|
} |
|
|
|
@end
|
|
|