如何實現(xiàn)一個圓形進度條按鈕

效果展示.gif

預(yù)備知識

實現(xiàn)分析

  • 整個動畫的實現(xiàn)可以分為兩個部分:
  1. 圓環(huán)放大的過程,如何保證圓環(huán)放大的時候?qū)挾炔蛔?/li>
  2. 進度條繪制的過程
  • 整個過程主要依靠CADisplaylink被觸發(fā)時調(diào)用的方法中去做重繪來實現(xiàn)。
    步驟1 :
    ????????CADisplaylink 是一個計時器對象,可以使用這個對象來保持應(yīng)用中的繪制與顯示刷新的同步。假設(shè)displaylink綁定的方法名字為A方法,在沒有卡頓時,iOS 設(shè)備屏幕顯示每秒刷新60次,意味著 displaylink的屬性frameInterval 為默認(rèn)值時,每秒回調(diào)60次A方法,當(dāng)frameInterval 改為2時,每秒回調(diào)30(60/2)次A方法。假定放大過程設(shè)定為0.2秒,則整個放大過程調(diào)用的A方法次數(shù)為12次。圓環(huán)最大時半徑為x,初始半徑為y,則每次調(diào)用方法時圓環(huán)的半徑為r = (x - y)/12 + y。在A方法中用貝塞爾曲線繪制半徑為r,linewidth為需要的寬度的圓環(huán)就可以達到效果。
    步驟2:
    ????????第二步是進度條繪制的過程。進度條繪制主要依靠的是CAShapeLayer的strokeStart和strokeEnd屬性。首先需要提前繪制好進度條layer的路徑,也就是紅色圓圈所在的路徑。strokeStart和strokeEnd分別表示path的start位置和end位置,取值范圍為[0,1]。假設(shè)strokeStart為0,strokeEnd為1,那么繪制出來的就是一條完整的路徑,strokeStart為0,strokeEnd為0.5,那么繪制出來的就是完整路徑的一半。同時這兩個屬性都支持動畫。然而我們這里用CADisPlayLink,只要直接改值就好。沒錯,只要在初始化的時候繪制好路徑,并且給strokeStart和strokeEnd屬性都賦值為0,這樣就不會繪制出進度條。然后在displayLink調(diào)用A方法的時候,逐漸將strokeEnd加上delta,delta = 1 / (進度條持續(xù)時間 * 60),就可以實現(xiàn)進度條的動畫。

具體實現(xiàn)

//最大錄制時間
static float const maxRecordTime = 10.f;
//初始半徑
static float const startRadius = 35.f;
//最大半徑
static float const endRadius = 50.f;
//圓圈寬度
static float const lineWidth = 6.f;

@interface KiraCircleButton ()

@property (nonatomic, strong) CAShapeLayer *circleLayer;//白色圓圈圖層
@property (nonatomic, strong) CAShapeLayer *maskLayer;//遮罩圖層
@property (nonatomic, strong) CAShapeLayer *drawLayer;//進度條繪制圖層

@property (nonatomic, strong) CADisplayLink *displayLink;
@property (nonatomic, assign) CGFloat currentRadius; //當(dāng)前半徑
@property (nonatomic, strong) UIVisualEffectView* effectView;
@property (nonatomic, assign) CGPoint centerPoint;
@property (nonatomic, assign) float currentRecordTime;

@end

  • 首先初始化View并且給view加上手勢,本文的動畫是通過長按的手勢觸發(fā)的。同時計算好進度條動畫的路徑,提前添加layer到view上,將strokeStart和strokeEnd屬性都賦為0。
- (void)initUI {
    self.centerPoint = CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2);
    [self setUserInteractionEnabled:YES];
//添加手勢
    UITapGestureRecognizer * tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doSomeThingWhenTap)];
    [self addGestureRecognizer:tap];
    
    UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(doSomeThingWhenLongTap:)];
    longPress.minimumPressDuration = 0.2;
    longPress.delegate = self;
    [self addGestureRecognizer:longPress];
    
    //給按鈕增加毛玻璃效果
    UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleRegular];
    self.effectView = [[UIVisualEffectView alloc] initWithEffect:effect];
    self.effectView.userInteractionEnabled = NO;
    [self addSubview:self.effectView];
    [self.effectView setFrame:CGRectMake(startRadius - endRadius, startRadius - endRadius, 2 * endRadius, 2 * endRadius)];
    
    UIBezierPath *backPath = [UIBezierPath bezierPathWithArcCenter:self.centerPoint radius:endRadius - lineWidth/2 startAngle:- M_PI_2 endAngle:3 * M_PI_2 clockwise:YES];
    
    CAShapeLayer *secondLayer = [CAShapeLayer layer];
    secondLayer.strokeColor = [UIColor colorWithRed:1 green:64.f/255.f blue:64.f/255.f alpha:1].CGColor;
    secondLayer.lineWidth = lineWidth;
    secondLayer.fillColor = [UIColor clearColor].CGColor;
    secondLayer.path = backPath.CGPath;
    secondLayer .strokeStart = 0;
    secondLayer.strokeEnd = 0;

    _drawLayer = secondLayer;
    _circleLayer = [CAShapeLayer layer];
    _maskLayer = [CAShapeLayer layer];
    [self resetCaptureButton];
    [self.layer addSublayer:_circleLayer];
    [self.layer setMask:_maskLayer];
    [self.layer addSublayer:secondLayer];
}
  • 然后在長按調(diào)用的方法中,初始化displaylink,綁定changeRedius方法到di splayLink。將displaylink添加到當(dāng)前的runloop中,然后將displaylink的paused置為NO。
- (void)doSomeThingWhenLongTap:(UILongPressGestureRecognizer *)gesture {
    if (gesture.state == UIGestureRecognizerStateBegan) {
        NSLog(@"Im long tapped start");
        self.displayLink = [CADisplayLink displayLinkWithTarget:self
                                                       selector:@selector(changeRadius)];
        [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
        self.displayLink.paused = NO;
        if (self.delegate && [self.delegate respondsToSelector:@selector(startProgress)]) {
            [self.delegate startProgress];
        }
    } else if (gesture.state == UIGestureRecognizerStateEnded) {
        NSLog(@"Im long tapped end");
        //end
        self.displayLink.paused = YES;
        if (self.delegate && [self.delegate respondsToSelector:@selector(endProgress)]) {
            [self.delegate endProgress];
        }
        [self resetCaptureButton];
    }
}
  • 最后是關(guān)鍵的changeRadius方法,也就是A方法。每次displaylink被觸發(fā)都將調(diào)用changeRadius方法,在currentRadius增加到最大值之前執(zhí)行放大動畫,在到達最大值之后就是進度條繪制的動畫了。
- (void)changeRadius {
    CGFloat toValue = endRadius - lineWidth/2;
    CGFloat fromValue = startRadius - lineWidth/2;
    CGFloat duration = 0.2;
    CGFloat times = 60 * duration;
    CGFloat delta = (toValue - fromValue) / times;
    _currentRecordTime += 1.f/60;
    _currentRadius += delta;
    if (_currentRadius <= toValue) {
        UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:self.centerPoint radius: _currentRadius startAngle:0 endAngle:M_PI * 2 clockwise:YES];
        _circleLayer.path = path.CGPath;
        _circleLayer.lineWidth = lineWidth;

        UIBezierPath *maskPath = [UIBezierPath bezierPathWithArcCenter:self.centerPoint radius:_currentRadius + 2 startAngle:0 endAngle:M_PI * 2 clockwise:YES];
        _maskLayer = [CAShapeLayer layer];
        _maskLayer.path = maskPath.CGPath;
        [self.layer setMask:_maskLayer];
    } else {
        CGFloat delta = 1 / (maxRecordTime * 60);
        self.drawLayer.strokeEnd += delta;
        if (self.drawLayer.strokeEnd >= 1) {
            self.displayLink.paused = YES;
            if (self.delegate && [self.delegate respondsToSelector:@selector(endProgress)]) {
                [self.delegate endProgress];
            }
        }
    }
    
}

最后

附上 demo鏈接 ,demo實現(xiàn)了上述的效果,但是并沒有封裝成一個復(fù)用性比較高的控件,主要以提供實現(xiàn)思路為主。有任何疑問歡迎指出??

補充更新: 2019/02/17
  1. 勘誤:CADisplayLink 的刷新頻率確實是60次/秒左右,但是并不固定,由于每次調(diào)用 CADisplayLink 的時間間隔都不是平均的,所以我們不能根據(jù)調(diào)用次數(shù)乘以1/60的時間間隔來得到當(dāng)前經(jīng)歷的時間。正確計算當(dāng)前經(jīng)歷時間的方法是通過獲取當(dāng)前時間再減去起始時間來得到。
  2. 擴展:出于 KiraCircleButton Demo的易用性、可維護性、擴展性考慮,我對代碼進行了更新,動畫支持配置不同緩動函數(shù),Demo地址不變,如果需要看第一版的代碼的話可以切到最初的提交閱讀。有關(guān)新的 KiraCircleButton 的設(shè)計實現(xiàn)介紹請點擊CADisplayLink 動畫進階
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1、通過CocoaPods安裝項目名稱項目信息 AFNetworking網(wǎng)絡(luò)請求組件 FMDB本地數(shù)據(jù)庫組件 SD...
    陽明AI閱讀 16,175評論 3 119
  • 2018年6月16日 星期六 風(fēng) 感覺好幾天沒記日記了,確實是有點小忙,太累了!這個點兒本來就...
    小雨點兒_5f38閱讀 193評論 0 0
  • 靈魂和思想,總有一個要在路上不斷前行。 工作時間久了,靈魂就會變,變得消極怠慢不知所向,這時候就真的需要一次洗滌,...
    熊也一閱讀 833評論 27 10
  • 今天,繼續(xù)分享關(guān)于領(lǐng)導(dǎo)力培養(yǎng)這本書,今天重點聽了關(guān)于如何獲取權(quán)力,使得自己變得更重要的一些關(guān)鍵步驟。由于本人對其中...
    小多媛媛閱讀 461評論 3 1

友情鏈接更多精彩內(nèi)容