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

//
// 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