如何实现一个圆形进度条按钮

效果展示.gif

预备知识

  • CADisplayLink
  • UIBezierPath

实现分析

  • 整个动画的实现可以分为两个部分:
  1. 圆环放大的过程,如何保证圆环放大的时候宽度不变
  2. 进度条绘制的过程
  • 整个过程主要依靠CADisplaylink被触发时调用的方法中去做重绘来实现。
    步骤1 :
            CADisplaylink 是一个计时器对象,可以使用这个对象来保持应用中的绘制与显示刷新的同步。假设displaylink绑定的方法名字为A方法,在没有卡顿时,iOS 设备屏幕显示每秒刷新60次,意味着 displaylink的属性frameInterval 为默认值时,每秒回调60次A方法,当frameInterval 改为2时,每秒回调30(60/2)次A方法。假定放大过程设定为0.2秒,则整个放大过程调用的A方法次数为12次。圆环最大时半径为x,初始半径为y,则每次调用方法时圆环的半径为r = (x - y)/12 + y。在A方法中用贝塞尔曲线绘制半径为r,linewidth为需要的宽度的圆环就可以达到效果。
    步骤2:
            第二步是进度条绘制的过程。进度条绘制主要依靠的是CAShapeLayer的strokeStart和strokeEnd属性。首先需要提前绘制好进度条layer的路径,也就是红色圆圈所在的路径。strokeStart和strokeEnd分别表示path的start位置和end位置,取值范围为[0,1]。假设strokeStart为0,strokeEnd为1,那么绘制出来的就是一条完整的路径,strokeStart为0,strokeEnd为0.5,那么绘制出来的就是完整路径的一半。同时这两个属性都支持动画。然而我们这里用CADisPlayLink,只要直接改值就好。没错,只要在初始化的时候绘制好路径,并且给strokeStart和strokeEnd属性都赋值为0,这样就不会绘制出进度条。然后在displayLink调用A方法的时候,逐渐将strokeEnd加上delta,delta = 1 / (进度条持续时间 * 60),就可以实现进度条的动画。

具体实现

//最大录制时间
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; //当前半径
@property (nonatomic, strong) UIVisualEffectView* effectView;
@property (nonatomic, assign) CGPoint centerPoint;
@property (nonatomic, assign) float currentRecordTime;

@end

  • 首先初始化View并且给view加上手势,本文的动画是通过长按的手势触发的。同时计算好进度条动画的路径,提前添加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];
}
  • 然后在长按调用的方法中,初始化displaylink,绑定changeRedius方法到di splayLink。将displaylink添加到当前的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];
    }
}
  • 最后是关键的changeRadius方法,也就是A方法。每次displaylink被触发都将调用changeRadius方法,在currentRadius增加到最大值之前执行放大动画,在到达最大值之后就是进度条绘制的动画了。
- (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实现了上述的效果,但是并没有封装成一个复用性比较高的控件,主要以提供实现思路为主。有任何疑问欢迎指出

补充更新: 2019/02/17
  1. 勘误:CADisplayLink 的刷新频率确实是60次/秒左右,但是并不固定,由于每次调用 CADisplayLink 的时间间隔都不是平均的,所以我们不能根据调用次数乘以1/60的时间间隔来得到当前经历的时间。正确计算当前经历时间的方法是通过获取当前时间再减去起始时间来得到。
  2. 扩展:出于 KiraCircleButton Demo的易用性、可维护性、扩展性考虑,我对代码进行了更新,动画支持配置不同缓动函数,Demo地址不变,如果需要看第一版的代码的话可以切到最初的提交阅读。有关新的 KiraCircleButton 的设计实现介绍请点击CADisplayLink 动画进阶

你可能感兴趣的:(如何实现一个圆形进度条按钮)