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

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

要想封裝一個帶有環(huán)形進度動畫的視圖浊吏,就要重寫 view 的 drawRect 方法而昨。至于如何實現(xiàn)進度的變化,這一點我們可以利用定時器定時調(diào)用 setNeedsDisplay 方法實時更新 UI 來實現(xiàn)找田。關于定時器的選擇歌憨,Demo 里我使用了 CADisplayLink,好處是該定時器的默認調(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; //進度條顏色

@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;  //當前進度值(0~1)

/**
 更新進度動畫

 @param progress 更新后的進度值
 @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];

    //進度條
    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++;
    
    //計算最新進度
    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 是定時器的調(diào)用間隔唆铐,默認為 1/60 s,也即是屏幕刷新的時間間隔奔滑。利用動畫總時間除以定時器的調(diào)用間隔艾岂,即可得出調(diào)用次數(shù),進度的總增量除以調(diào)用次數(shù)即可得出每次增加的進度值朋其。接下來只要在每次定時器調(diào)用時在當前進度值的基礎上進行累加即可得出實時進度王浴。利用實時進度乘以 2π 即可得到實時角度,隨后就可以利用貝塞爾曲線來畫出圓弧了令宿。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末粒没,一起剝皮案震驚了整個濱河市筛婉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌癞松,老刑警劉巖爽撒,帶你破解...
    沈念sama閱讀 212,185評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異响蓉,居然都是意外死亡硕勿,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,445評論 3 385
  • 文/潘曉璐 我一進店門枫甲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來源武,“玉大人,你說我怎么就攤上這事想幻×黄埽” “怎么了?”我有些...
    開封第一講書人閱讀 157,684評論 0 348
  • 文/不壞的土叔 我叫張陵脏毯,是天一觀的道長闹究。 經(jīng)常有香客問我,道長食店,這世上最難降的妖魔是什么渣淤? 我笑而不...
    開封第一講書人閱讀 56,564評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮吉嫩,結(jié)果婚禮上价认,老公的妹妹穿的比我還像新娘。我一直安慰自己自娩,他們只是感情好用踩,可當我...
    茶點故事閱讀 65,681評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般捶箱。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上动漾,一...
    開封第一講書人閱讀 49,874評論 1 290
  • 那天丁屎,我揣著相機與錄音,去河邊找鬼旱眯。 笑死晨川,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的删豺。 我是一名探鬼主播共虑,決...
    沈念sama閱讀 39,025評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼呀页!你這毒婦竟也來了妈拌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,761評論 0 268
  • 序言:老撾萬榮一對情侶失蹤蓬蝶,失蹤者是張志新(化名)和其女友劉穎尘分,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體丸氛,經(jīng)...
    沈念sama閱讀 44,217評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡培愁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,545評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了缓窜。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片定续。...
    茶點故事閱讀 38,694評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖禾锤,靈堂內(nèi)的尸體忽然破棺而出私股,到底是詐尸還是另有隱情,我是刑警寧澤时肿,帶...
    沈念sama閱讀 34,351評論 4 332
  • 正文 年R本政府宣布庇茫,位于F島的核電站,受9級特大地震影響螃成,放射性物質(zhì)發(fā)生泄漏旦签。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,988評論 3 315
  • 文/蒙蒙 一寸宏、第九天 我趴在偏房一處隱蔽的房頂上張望宁炫。 院中可真熱鬧,春花似錦氮凝、人聲如沸羔巢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,778評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽竿秆。三九已至启摄,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間幽钢,已是汗流浹背歉备。 一陣腳步聲響...
    開封第一講書人閱讀 32,007評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留匪燕,地道東北人蕾羊。 一個月前我還...
    沈念sama閱讀 46,427評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像帽驯,于是被迫代替她去往敵國和親龟再。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,580評論 2 349

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