iOS圓形倒計時控件的實現(xiàn)

這兩個月真的太忙了袁翁,被一堆需求壓著,本來想好好學(xué)學(xué)Clang相關(guān)的知識的秒拔,但一直沒時間莫矗,博客也沒更了,現(xiàn)在終于把手上的需求都提測掉了砂缩,這幾天先好好復(fù)盤一下趣苏,看看這段時間有什么收獲,今天來記錄一下圓形倒計時控件的實現(xiàn)梯轻。

先來看看demo的效果:

demo效果

實現(xiàn)原理:構(gòu)建一個環(huán)形食磕,環(huán)形的起始點為-π/2,終點需要討論順時針/逆時針喳挑、遞增/遞減四種情況彬伦。

設(shè)進度為progress,起始點為startA伊诵,一圈剛好為2π单绑,現(xiàn)在我們需要根據(jù)clockWise和increase的四種組合情況來得出終點endA的公式,再結(jié)合UIBezierPath的接口決定是否順時針構(gòu)建環(huán)形曹宴,從而畫出一個弧搂橙,結(jié)合progress即可有進度條的效果了。

下圖是貝塞爾曲線的坐標(biāo)系笛坦,所以起點是-π/2


  • 順時針遞增

起始值為0区转,順時針構(gòu)建環(huán)形,endA與progress正相關(guān)版扩,得:

endA = startA + progress * 2π


  • 逆時針遞減

起始值為2π废离,順時針構(gòu)建環(huán)形,endA與(1 - progress)正相關(guān)礁芦,得:

endA = startA + (1 - progress) * 2π


  • 順時針遞減

起始值為2π蜻韭,逆時針構(gòu)建環(huán)形悼尾,endA與(1 - progress)負(fù)相關(guān),得:

endA = startA - (1 - progress) * 2π


  • 逆時針遞增

起始值為0肖方,逆時針構(gòu)建環(huán)形闺魏,endA與progress負(fù)相關(guān),得:

endA = startA - progress * 2π


通過以上分析俯画,可以看到

  1. 順時針遞增舷胜、逆時針遞減都是用順時針構(gòu)建環(huán)形,否則用逆時針構(gòu)建環(huán)形

  2. 順時針遞增活翩、逆時針遞增,endA都是與progress相關(guān)翻伺,否則與(1-progress)相關(guān)

  3. 逆時針構(gòu)建環(huán)形需要與相關(guān)參數(shù)負(fù)相關(guān)

得代碼:

- (void)showAnimationWithProgress:(CGFloat)progress {
    CGFloat startA = - M_PI_2;  // 設(shè)置進度條起點位置
    CGFloat endA;               // 設(shè)置進度條終點位置
    CGFloat clockWiseFlag = _clockWise ? 1 : -1;
    CGFloat progressFlag = _increase ? 1 : -1;
    CGFloat percent = _increase ? progress : (1 - progress);
    
    // 順增材泄、逆減,順時針構(gòu)建環(huán)形
    BOOL shouldClockWiseBulid = (clockWiseFlag * progressFlag > 0);
    
    endA = startA + M_PI * 2 * percent * clockWiseFlag * progressFlag;
    
    UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(_radius / 2, _radius / 2) radius:_radius / 2 - self.lineWidth / 2 startAngle:startA endAngle:endA clockwise:shouldClockWiseBulid]; // 構(gòu)建環(huán)形
    self.path = [path CGPath];
}

再加上一些可以自定義的參數(shù)(半徑吨岭、填充顏色等)拉宗,也可以自己根據(jù)倒計時和總計時來實現(xiàn),就可以構(gòu)建自己想要的倒計時環(huán)形啦辣辫!附相關(guān)代碼:

//
//  HobenCountDownCircleLayer.h
//  HobenLayerDemo
//
//  Created by Hoben on 2020/8/5.
//  Copyright ? 2020 Hoben. All rights reserved.
//  用于展示圓形倒計時(順時針/逆時針/遞增/遞減)的Layer

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface HobenCountDownCircleLayer : CAShapeLayer

/**
 建立一個展示圓形倒計時的Layer
 @param strokeColor 倒計時線的顏色
 @param lineWidth   倒計時線的寬度
 @param radius      倒計時圓的半徑
 */
+ (HobenCountDownCircleLayer *)layerWithStrokeColor:(UIColor *)strokeColor lineWidth:(CGFloat)lineWidth radius:(CGFloat)radius;

@property (nonatomic, assign) BOOL clockWise;   // 是否為順時針

@property (nonatomic, assign) BOOL increase;    // 是否遞增動畫

@property (nonatomic, assign) CGFloat totalCountDown;   // 總計時

@property (nonatomic, assign) CGFloat radius;  // 圓的半徑

/**
 根據(jù)當(dāng)前倒計時展示Layer(需要實現(xiàn)設(shè)置totalCountDown)
 @param countDown 當(dāng)前的倒計時
*/
- (void)showAnimationWithCountdown:(CGFloat)countDown;

/**
 根據(jù)當(dāng)前進度展示Layer
 @param progress 當(dāng)前的進度(0%-100%)
*/
- (void)showAnimationWithProgress:(CGFloat)progress;

@end

NS_ASSUME_NONNULL_END
//
//  HobenCountDownCircleLayer.m
//  HobenLayerDemo
//
//  Created by Hoben on 2020/8/5.
//  Copyright ? 2020 Hoben. All rights reserved.
//

#import "HobenCountDownCircleLayer.h"

@implementation HobenCountDownCircleLayer

+ (HobenCountDownCircleLayer *)layerWithStrokeColor:(UIColor *)strokeColor lineWidth:(CGFloat)lineWidth radius:(CGFloat)radius {
    HobenCountDownCircleLayer *shapeLayer = [HobenCountDownCircleLayer layer];
    shapeLayer.strokeColor = strokeColor.CGColor;
    shapeLayer.lineWidth = lineWidth;
    shapeLayer.fillColor = [UIColor clearColor].CGColor; // 填充色為無色
    shapeLayer.lineCap = kCALineCapRound; // 指定線的邊緣是圓的
    shapeLayer.totalCountDown = 1;
    shapeLayer.radius = radius;
    return shapeLayer;
}

- (void)showAnimationWithCountdown:(CGFloat)countDown {
    if (_totalCountDown <= 0) {
        NSAssert(NO, @"總計時不能為0");
        return;
    }
    [self showAnimationWithProgress:(_totalCountDown - countDown) * 1.0 / _totalCountDown];
}

- (void)showAnimationWithProgress:(CGFloat)progress {
    CGFloat startA = - M_PI_2;  // 設(shè)置進度條起點位置
    CGFloat endA;               // 設(shè)置進度條終點位置
    CGFloat clockWiseFlag = _clockWise ? 1 : -1;
    CGFloat progressFlag = _increase ? 1 : -1;
    CGFloat percent = _increase ? progress : (1 - progress);
    
    // 順增旦事、逆減,順時針構(gòu)建環(huán)形
    BOOL shouldClockWiseBulid = (clockWiseFlag * progressFlag > 0);
    
    endA = startA + M_PI * 2 * percent * clockWiseFlag * progressFlag;
    
    UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(_radius / 2, _radius / 2) radius:_radius / 2 - self.lineWidth / 2 startAngle:startA endAngle:endA clockwise:shouldClockWiseBulid]; // 構(gòu)建環(huán)形
    self.path = [path CGPath];
}

@end

Demo調(diào)用:

//
//  ViewController.m
//  HobenLayerDemo
//
//  Created by Hoben on 2020/8/19.
//  Copyright ? 2020 Hoben. All rights reserved.
//

#import "ViewController.h"
#import "HobenCountDownCircleLayer.h"

#define kHobenRadius 100.f

#define kHobenTotalCountDown 20.f

@interface ViewController ()

@property (nonatomic, strong) HobenCountDownCircleLayer *circleCountDownLayer;

@property (nonatomic, strong) UIButton *circleButton;

@property (nonatomic, strong) NSTimer *timer;

@property (nonatomic, assign) NSInteger countdown;

@property (nonatomic, strong) UISwitch *clockWiseSwitch;

@property (nonatomic, strong) UISwitch *increaseSwitch;

@property (nonatomic, strong) UILabel *clockWiseLabel;

@property (nonatomic, strong) UILabel *increaseLabel;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    [self.view addSubview:self.circleButton];
    [self.circleButton.layer addSublayer:self.circleCountDownLayer];
    
    [self.view addSubview:self.clockWiseLabel];
    [self.view addSubview:self.increaseLabel];
    
    [self.view addSubview:self.clockWiseSwitch];
    [self.view addSubview:self.increaseSwitch];
}

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];
    
    self.circleButton.center = self.view.center;
    self.circleCountDownLayer.frame = CGRectMake(0, 0, kHobenRadius, kHobenRadius);
    
    
    self.clockWiseLabel.frame = CGRectMake(100.f, 430.f, 20.f, 20.f);
    [self.clockWiseLabel sizeToFit];
    self.clockWiseSwitch.frame = CGRectMake(180.f, 420.f, 20.f, 20.f);
    
    self.increaseLabel.frame = CGRectMake(100.f, 480.f, 20.f, 20.f);
    [self.increaseLabel sizeToFit];
    self.increaseSwitch.frame = CGRectMake(180.f, 470.f, 20.f, 20.f);
}

- (void)stopTimer {
    if (self.timer) {
        [self.timer invalidate];
        self.timer = nil;
    }
}

#pragma mark - Action

- (void)onClockWiseChanged:(UISwitch *)switcher {
    BOOL closeWise = switcher.on;
    self.circleCountDownLayer.clockWise = closeWise;
}

- (void)onIncreaseChanged:(UISwitch *)switcher {
    BOOL increase = switcher.on;
    self.circleCountDownLayer.increase = increase;
}

- (void)startTimer {
    [self stopTimer];
    _countdown = kHobenTotalCountDown;
    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer timerWithTimeInterval:.1f repeats:YES block:^(NSTimer * _Nonnull timer) {
        weakSelf.countdown--;
    }];
    
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}

#pragma mark - Setter & Getter

- (void)setCountdown:(NSInteger)countdown {
    _countdown = countdown;
    if (countdown > 0) {
        self.circleCountDownLayer.hidden = NO;
        [self.circleCountDownLayer showAnimationWithCountdown:countdown];
    } else {
        [self stopTimer];
        self.circleCountDownLayer.hidden = YES;
    }
}

- (UIButton *)circleButton {
    if (!_circleButton) {
        _circleButton = [UIButton buttonWithType:UIButtonTypeCustom];
        _circleButton.frame = CGRectMake(0, 0, kHobenRadius, kHobenRadius);
        _circleButton.layer.cornerRadius = kHobenRadius / 2;
        _circleButton.layer.masksToBounds = YES;
        [_circleButton setTitle:@"Start" forState:UIControlStateNormal];
        _circleButton.titleLabel.font = [UIFont systemFontOfSize:16.f];
        [_circleButton setTitleColor:[UIColor greenColor] forState:UIControlStateNormal];
        [_circleButton addTarget:self action:@selector(startTimer) forControlEvents:UIControlEventTouchUpInside];
        _circleButton.backgroundColor = [UIColor blackColor];
    }
    return _circleButton;
}

- (HobenCountDownCircleLayer *)circleCountDownLayer {
    if (!_circleCountDownLayer) {
        _circleCountDownLayer = ({
            HobenCountDownCircleLayer *progressLayer = [HobenCountDownCircleLayer layerWithStrokeColor:[UIColor greenColor] lineWidth:5.f radius:kHobenRadius];
            progressLayer.clockWise = YES;
            progressLayer.increase = YES;
            progressLayer.totalCountDown = kHobenTotalCountDown;
            progressLayer;
        });
    }
    return _circleCountDownLayer;
}

- (UISwitch *)clockWiseSwitch {
    if (!_clockWiseSwitch) {
        _clockWiseSwitch = ({
            UISwitch *switcher = [[UISwitch alloc] init];
            [switcher setOn:YES];
            [switcher addTarget:self action:@selector(onClockWiseChanged:) forControlEvents:UIControlEventValueChanged];
            switcher;
        });
    }
    return _clockWiseSwitch;
}

- (UISwitch *)increaseSwitch {
    if (!_increaseSwitch) {
        _increaseSwitch = ({
            UISwitch *switcher = [[UISwitch alloc] init];
            [switcher setOn:YES];
            [switcher addTarget:self action:@selector(onIncreaseChanged:) forControlEvents:UIControlEventValueChanged];
            switcher;
        });
    }
    return _increaseSwitch;
}

- (UILabel *)clockWiseLabel {
    if (!_clockWiseLabel) {
        _clockWiseLabel = ({
            UILabel *label = [[UILabel alloc] init];
            label.font = [UIFont systemFontOfSize:14.f];
            label.text = @"是否順時針";
            label;
        });
    }
    return _clockWiseLabel;
}

- (UILabel *)increaseLabel {
    if (!_increaseLabel) {
        _increaseLabel = ({
            UILabel *label = [[UILabel alloc] init];
            label.font = [UIFont systemFontOfSize:14.f];
            label.text = @"是否遞增";
            label;
        });
    }
    return _increaseLabel;
}

@end

總的來說急灭,構(gòu)建這個環(huán)形看上去不是很難姐浮,但是實際寫的時候還是有點繞的,四種情形各不相同葬馋,需要先分開分析再結(jié)合規(guī)律總結(jié)卖鲤,才能造出一個比較通用的輪子~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市畴嘶,隨后出現(xiàn)的幾起案子蛋逾,更是在濱河造成了極大的恐慌,老刑警劉巖窗悯,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件区匣,死亡現(xiàn)場離奇詭異,居然都是意外死亡蒋院,警方通過查閱死者的電腦和手機亏钩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來欺旧,“玉大人铸屉,你說我怎么就攤上這事∏卸耍” “怎么了彻坛?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我昌屉,道長钙蒙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任间驮,我火速辦了婚禮躬厌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘竞帽。我一直安慰自己扛施,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布屹篓。 她就那樣靜靜地躺著疙渣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪堆巧。 梳的紋絲不亂的頭發(fā)上妄荔,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天,我揣著相機與錄音谍肤,去河邊找鬼啦租。 笑死,一個胖子當(dāng)著我的面吹牛荒揣,可吹牛的內(nèi)容都是我干的篷角。 我是一名探鬼主播,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼系任,長吁一口氣:“原來是場噩夢啊……” “哼内地!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起赋除,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤阱缓,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后举农,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體荆针,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年颁糟,在試婚紗的時候發(fā)現(xiàn)自己被綠了航背。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡棱貌,死狀恐怖玖媚,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情婚脱,我是刑警寧澤今魔,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布勺像,位于F島的核電站,受9級特大地震影響错森,放射性物質(zhì)發(fā)生泄漏吟宦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一涩维、第九天 我趴在偏房一處隱蔽的房頂上張望殃姓。 院中可真熱鬧,春花似錦瓦阐、人聲如沸蜗侈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽踏幻。三九已至,卻和暖如春薄湿,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背偷卧。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工豺瘤, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人听诸。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓坐求,卻偏偏與公主長得像,于是被迫代替她去往敵國和親晌梨。 傳聞我的和親對象是個殘疾皇子桥嗤,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,933評論 2 355