最近朋友项目中用到环形进度动画,于是就写了一个简单的 Demo。下面简单介绍一下实现过程。
要想封装一个带有环形进度动画的视图,就要重写 view 的 drawRect 方法。至于如何实现进度的变化,这一点我们可以利用定时器定时调用 setNeedsDisplay 方法实时更新 UI 来实现。关于定时器的选择,Demo 里我使用了 CADisplayLink,好处是该定时器的默认调用频率和屏幕刷新频率是一致的,看起来更加流畅,不会有卡顿效果。该定时器的创建方法为+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel
,创建完成后需要调用- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode
加入到 runloop 中。定时器的停止可以通过将paused
属性置为 YES,并调用 invalidate
方法来实现。话不多说,直接上代码。
#import <UIKit/UIKit.h>
typedef void(^CompletionBlock)(void);
@interface YDCircleProgressView : UIView
@property (nonatomic, assign) CGFloat circleRadius; //背景圆半径
@property (nonatomic, assign) CGFloat circleBorderWidth; //背景圆线条宽度
@property (nonatomic, strong) UIColor *circleColor; //背景圆颜色
@property (nonatomic, strong) UIColor *progressColor; //进度条颜色
@property (nonatomic, assign) CGFloat pointRadius; //小圆点半径
@property (nonatomic, assign) CGFloat pointBorderWidth; //小圆点边框宽度
@property (nonatomic, strong) UIColor *pointColor; //小圆点颜色
@property (nonatomic, strong) UIColor *pointBorderColor; //小圆点边框色
@property (nonatomic, assign) CGFloat curProgress; //当前进度值(0~1)
/**
更新进度动画
@param progress 更新后的进度值
@param duration 动画时间
@param completion 动画结束回调
*/
- (void)updateProgress:(CGFloat)progress duration:(NSTimeInterval)duration completion:(CompletionBlock)completion;
@end
#import "YDCircleProgressView.h"
@interface YDCircleProgressView ()
@property (nonatomic, strong) CADisplayLink *displayLink;
@property (nonatomic, assign) NSTimeInterval duration;
@property (nonatomic, assign) CGFloat progressDelta;
@property (nonatomic, assign) NSInteger runCount;
@property (nonatomic, copy) CompletionBlock completion;
@end
@implementation YDCircleProgressView
#pragma mark - 初始化方法
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor clearColor];
//赋初始值
self.circleBorderWidth = 4.0f;
self.circleColor = [UIColor blackColor];
self.progressColor = [UIColor cyanColor];
self.pointRadius = 2.5f;
self.pointBorderWidth = 0.5f;
self.pointColor = [UIColor whiteColor];
self.pointBorderColor = [UIColor lightGrayColor];
self.curProgress = 0.0f;
}
return self;
}
#pragma mark - 懒加载
- (CGFloat)circleRadius
{
if (!_circleRadius) {
self.circleRadius = self.bounds.size.width * 0.5 - MAX(self.pointRadius, self.circleBorderWidth * 0.5);
}
return _circleRadius;
}
#pragma mark - setter方法
- (void)setCurProgress:(CGFloat)curProgress
{
//安全判断
if (curProgress < 0 || curProgress > 1) {
return;
}
//setter
_curProgress = curProgress;
//刷新UI
[self setNeedsDisplay];
}
#pragma mark - drawRect
- (void)drawRect:(CGRect)rect
{
//背景圆
UIBezierPath *circlePath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(self.bounds.size.width * 0.5 - self.circleRadius, self.bounds.size.width * 0.5 - self.circleRadius, self.circleRadius * 2, self.circleRadius * 2) cornerRadius:self.circleRadius];
[self.circleColor setStroke];
circlePath.lineWidth = self.circleBorderWidth;
[circlePath stroke];
//进度条
UIBezierPath *progressPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.bounds.size.width * 0.5, self.bounds.size.height * 0.5) radius:self.circleRadius startAngle:-M_PI_2 endAngle:M_PI * 2 * self.curProgress - M_PI_2 clockwise:YES];
[self.progressColor setStroke];
progressPath.lineWidth = self.circleBorderWidth;
[progressPath stroke];
//小圆点
UIBezierPath *pointPath = [UIBezierPath bezierPathWithArcCenter:progressPath.currentPoint radius:self.pointRadius startAngle:0 endAngle:M_PI * 2 clockwise:YES];
[self.pointColor setFill];
[pointPath fill];
[self.pointBorderColor setStroke];
pointPath.lineWidth = self.pointBorderWidth;
[pointPath stroke];
}
#pragma mark - 公开方法
- (void)updateProgress:(CGFloat)progress duration:(NSTimeInterval)duration completion:(CompletionBlock)completion
{
//保存属性值
self.duration = duration;
self.progressDelta = progress - self.curProgress;
self.runCount = 0;
self.completion = completion;
//停止定时器
self.displayLink.paused = YES;
[self.displayLink invalidate];
//开启定时器
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateDisplay)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
#pragma mark - 定时器事件
- (void)updateDisplay
{
//更新计数器
self.runCount++;
//计算最新进度
NSInteger count = ceil(self.duration / self.displayLink.duration);
count = count > 0 ? count : 1;
CGFloat progress = self.curProgress + self.progressDelta / count;
//更新进度
self.curProgress = progress;
//停止计时器
if (self.runCount == count || progress < 0 || progress > 1) {
self.displayLink.paused = YES;
[self.displayLink invalidate];
if (self.completion) self.completion();
}
}
@end
绘制过程中唯一的难点就在于实时进度的计算。这里使用
NSInteger count = ceil(self.duration / self.displayLink.duration);
count = count > 0 ? count : 1;
CGFloat progress = self.curProgress + self.progressDelta / count;
来计算。其中 self.displayLink.duration 是定时器的调用间隔,默认为 1/60 s,也即是屏幕刷新的时间间隔。利用动画总时间除以定时器的调用间隔,即可得出调用次数,进度的总增量除以调用次数即可得出每次增加的进度值。接下来只要在每次定时器调用时在当前进度值的基础上进行累加即可得出实时进度。利用实时进度乘以 2π 即可得到实时角度,随后就可以利用贝塞尔曲线来画出圆弧了。
本人能力有限,有错误的地方欢迎各位大神指正。想要下载文章中 Demo 的朋友可以前往我的Github:Github地址