【iOS】環(huán)形進(jìn)度動畫

最近朋友項目中用到環(huán)形進(jìn)度動畫,于是就寫了一個簡單的 Demo。下面簡單介紹一下實現(xiàn)過程。

要想封裝一個帶有環(huán)形進(jìn)度動畫的視圖,就要重寫 view 的 drawRect 方法。至于如何實現(xiàn)進(jìn)度的變化,這一點我們可以利用定時器定時調(diào)用 setNeedsDisplay 方法實時更新 UI 來實現(xiàn)。關(guān)于定時器的選擇,Demo 里我使用了 CADisplayLink,好處是該定時器的默認(rèn)調(diào)用頻率和屏幕刷新頻率是一致的,看起來更加流暢,不會有卡頓效果。該定時器的創(chuàng)建方法為+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel,創(chuàng)建完成后需要調(diào)用- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode加入到 runloop 中。定時器的停止可以通過將paused 屬性置為 YES,并調(diào)用 invalidate 方法來實現(xiàn)。話不多說,直接上代碼。

#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; //進(jìn)度條顏色

@property (nonatomic, assign) CGFloat pointRadius;  //小圓點半徑
@property (nonatomic, assign) CGFloat pointBorderWidth;  //小圓點邊框?qū)挾?@property (nonatomic, strong) UIColor *pointColor;  //小圓點顏色
@property (nonatomic, strong) UIColor *pointBorderColor; //小圓點邊框色

@property (nonatomic, assign) CGFloat curProgress;  //當(dāng)前進(jìn)度值(0~1)

/**
 更新進(jìn)度動畫

 @param progress 更新后的進(jìn)度值
 @param duration 動畫時間
 @param completion 動畫結(jié)束回調(diào)
 */
- (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];

    //進(jìn)度條
    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
{
    //更新計數(shù)器
    self.runCount++;
    
    //計算最新進(jìn)度
    NSInteger count = ceil(self.duration / self.displayLink.duration);
    count = count > 0 ? count : 1;
    CGFloat progress = self.curProgress + self.progressDelta / count;
    
    //更新進(jìn)度
    self.curProgress = progress;
    
    //停止計時器
    if (self.runCount == count || progress < 0 || progress > 1) {
        self.displayLink.paused = YES;
        [self.displayLink invalidate];
        if (self.completion) self.completion();
    }
}

@end

繪制過程中唯一的難點就在于實時進(jìn)度的計算。這里使用

NSInteger count = ceil(self.duration / self.displayLink.duration);
count = count > 0 ? count : 1;
CGFloat progress = self.curProgress + self.progressDelta / count;

來計算。其中 self.displayLink.duration 是定時器的調(diào)用間隔,默認(rèn)為 1/60 s,也即是屏幕刷新的時間間隔。利用動畫總時間除以定時器的調(diào)用間隔,即可得出調(diào)用次數(shù),進(jìn)度的總增量除以調(diào)用次數(shù)即可得出每次增加的進(jìn)度值。接下來只要在每次定時器調(diào)用時在當(dāng)前進(jìn)度值的基礎(chǔ)上進(jìn)行累加即可得出實時進(jìn)度。利用實時進(jìn)度乘以 2π 即可得到實時角度,隨后就可以利用貝塞爾曲線來畫出圓弧了。

本人能力有限,有錯誤的地方歡迎各位大神指正。想要下載文章中 Demo 的朋友可以前往我的Github:Github地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容