这两个月真的太忙了,被一堆需求压着,本来想好好学学Clang相关的知识的,但一直没时间,博客也没更了,现在终于把手上的需求都提测掉了,这几天先好好复盘一下,看看这段时间有什么收获,今天来记录一下圆形倒计时控件的实现。
先来看看demo的效果:
demo效果实现原理:构建一个环形,环形的起始点为-π/2,终点需要讨论顺时针/逆时针、递增/递减四种情况。
设进度为progress,起始点为startA,一圈刚好为2π,现在我们需要根据clockWise和increase的四种组合情况来得出终点endA的公式,再结合UIBezierPath的接口决定是否顺时针构建环形,从而画出一个弧,结合progress即可有进度条的效果了。
- 顺时针递增
起始值为0,顺时针构建环形,endA与progress正相关,得:
endA = startA + progress * 2π
- 逆时针递减
起始值为2π,顺时针构建环形,endA与(1 - progress)正相关,得:
endA = startA + (1 - progress) * 2π
- 顺时针递减
起始值为2π,逆时针构建环形,endA与(1 - progress)负相关,得:
endA = startA - (1 - progress) * 2π
- 逆时针递增
起始值为0,逆时针构建环形,endA与progress负相关,得:
endA = startA - progress * 2π
通过以上分析,可以看到
-
顺时针递增、逆时针递减都是用顺时针构建环形,否则用逆时针构建环形
-
顺时针递增、逆时针递增,endA都是与progress相关,否则与(1-progress)相关
-
逆时针构建环形需要与相关参数负相关
得代码:
- (void)showAnimationWithProgress:(CGFloat)progress {
CGFloat startA = - M_PI_2; // 设置进度条起点位置
CGFloat endA; // 设置进度条终点位置
CGFloat clockWiseFlag = _clockWise ? 1 : -1;
CGFloat progressFlag = _increase ? 1 : -1;
CGFloat percent = _increase ? progress : (1 - progress);
// 顺增、逆减,顺时针构建环形
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]; // 构建环形
self.path = [path CGPath];
}
再加上一些可以自定义的参数(半径、填充颜色等),也可以自己根据倒计时和总计时来实现,就可以构建自己想要的倒计时环形啦!附相关代码:
//
// 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; // 圆的半径
/**
根据当前倒计时展示Layer(需要实现设置totalCountDown)
@param countDown 当前的倒计时
*/
- (void)showAnimationWithCountdown:(CGFloat)countDown;
/**
根据当前进度展示Layer
@param progress 当前的进度(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; // 设置进度条起点位置
CGFloat endA; // 设置进度条终点位置
CGFloat clockWiseFlag = _clockWise ? 1 : -1;
CGFloat progressFlag = _increase ? 1 : -1;
CGFloat percent = _increase ? progress : (1 - progress);
// 顺增、逆减,顺时针构建环形
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]; // 构建环形
self.path = [path CGPath];
}
@end
Demo调用:
//
// 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
总的来说,构建这个环形看上去不是很难,但是实际写的时候还是有点绕的,四种情形各不相同,需要先分开分析再结合规律总结,才能造出一个比较通用的轮子~
网友评论