iOS - 使用定时器与贝塞尔绘制圆形进度条

3,696 阅读7分钟
原文链接: www.jianshu.com
前言:

本篇主要讲解的是如何使用定时器与贝塞尔绘制圆形进度条。在此之前,我们先了解一下定时器的一些知识。

效果展示:

zhanshi.gif
目录:
  • 1. CADisplayLink 的介绍
  • 2. 项目演练思想
  • 3. 项目代码示例

1. CADisplayLink 的介绍

1.1 什么是CADisplayLink
  CADisplayLink是一个能让我们以和屏幕刷新率相同的频率将内容画到屏幕上的定时器。我们在应用中创建一个新的 CADisplayLink对象,把它添加到一个runloop中,并给它提供一个 targetselector 在屏幕刷新的时候 调用 。

  一但 CADisplayLink 以特定的模式注册到runloop之后,每当屏幕需要刷新的时候,runloop就会调用CADisplayLink绑定的target上selector,这时target可以读到 CADisplayLink 的每次调用的时间戳,用来准备下一帧显示需要的数据。
  例如:一个视频应用使用时间戳来计算下一帧要显示的视频数据。在UI做动画的过程中,需要通过时间戳来计算UI对象在动画的下一帧要更新的大小等等。

  在添加进runloop的时候我们应该选用高一些的优先级,来保证动画的平滑。可以设想一下,我们在动画的过程中,runloop被添加进来了一个高优先级的任务,那么,下一次的调用就会被暂停转而先去执行高优先级的任务,然后在接着执行CADisplayLink的调用,从而造成动画过程的卡顿,使动画不流畅。

  duration属性提供了每帧之间的时间,也就是屏幕每次刷新之间的的时间。我们可以使用这个时间来计算出下一帧要显示的UI的数值。但是 duration只是个大概的时间,如果CPU忙于其它计算,就没法保证以相同的频率执行屏幕的绘制操作,这样会跳过几次调用回调方法的机会。

  frameInterval属性是可读可写的NSInteger型值,标识间隔多少帧调用一次selector方法,默认值是1,即每帧都调用一次。如果每帧都调用一次的话,对于iOS设备来说那刷新频率就是60HZ也就是每秒60次,如果将frameInterval设为2 那么就会两帧调用一次,也就是变成了每秒刷新30次。

  我们通过pause属性开控制CADisplayLink的运行。当我们想结束一个CADisplayLink的时候,应该调用-(void)invalidate

  从runloop中删除并删除之前绑定的 targetselector,另外CADisplayLink不能被继承。

1.2 CADisplayLink与 NSTimer有什么不同

  iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。

  NSTimer的精确度就显得低了点,比如NSTimer的触发时间到的时候,runloop如果在阻塞状态,触发时间就会推迟到下一个runloop周期。并且 NSTimer新增了tolerance属性,让用户可以设置可以容忍的触发的时间的延迟范围。

  CADisplayLink使用场合相对专一,适合做UI的不停重绘,比如自定义动画引擎或者视频播放的渲染。NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。在UI相关的动画或者显示内容使用CADisplayLink比起用NSTimer的好处就是我们不需要在格外关心屏幕的刷新频率了,因为它本身就是跟屏幕刷新同步的。

给非UI对象添加动画效果

  我们知道动画效果就是一个属性的线性变化,比如UIView 动画的 EasyIn EasyOut 。通过数值按照不同速率的变化我们能生成更接近真实世界的动画效果。我们也可以利用这个特性来使一些其他属性按照我们期望的曲线变化。比如当播放视频时关掉视频的声音我可以通过CADisplayLink来实现一个 EasyOut的渐出效果:先快速的降低音量,在慢慢的渐变到静音。

注意:
  通常来讲:iOS设备的刷新频率事60HZ也就是每秒60次。那么每一次刷新的时间就是1/60秒 大概16.7毫秒。当我们的frameInterval值为1的时候我们需要保证的是CADisplayLink调用的`target`的函数计算时间不应该大于 16.7否则就会出现严重的丢帧现象。

  在mac应用中我们使用的不是CADisplayLink
而是 CVDisplayLink它是基于C接口的用起来配置有些麻烦但是用起来还是很简单的。

2. 项目演练思想

演练步骤:

 - 1> 自定义UIView
     1. 添加到UIView上
     2. 初始化UIView  (bg = 黄)

     3. 画圆 底下灰色的圆(辅助圆)
        3.1 利用UIBezierPath和CAShapeLayer绘制进度条圆弧,bezierPathWithArcCenter:用这个方法,需要获知:
            (ArcCenter = ? ,radius = ?,startAngle = ?,endAngle = ?,clockwise = ?)
        3.2 由于半径需要我们自己设置,我们可以先声明一个,在进行初始化
        3.3 要利用上面方法,画圆,起点和终点的角度 (0,360)就可以画一个圆

      4. 画显示圆,滚动的 颜色为 红
        4.1 配置显示圆的CAShapeLayer,在利用CADisplayLink将显示圆的内容画到屏幕上
             (strokeColor = red)
        4.2 配置CADisplayLink(添加方法,加入运行循环,设置运行模式,默认暂停)
        4.3 响应定义器事件,绘制显示圆的路径(显示圆的路径提前要在init 中进行初始化,在这个基础上添加圆弧)
        4.4  显示圆:addArcWithCenter:==> (ArcCenter ,radius ,clockwise相同)  需要一个新的,startAngle和endAngle

               startAngle:  我们根据要求获知起点是从顶点开始的,则我们需要声明一个startAngle,初始化 - 90
                            (startAngle = (M_PI / 180.0) * _startAngle)

                endAngle: 对于终点来说,我们是在起点的基础上添加度数,由于使用定时器,我们需要_startAngle+= 3.6,来完
                           成要求,宣示圆的滚动。随着定时器,起点会等于终点,来回滚动。
                             ((M_PI / 180.0) * (_startAngle + 3.6)==/  _startAngle += 3.6;)

        5. 添加动画
           5.1 判断开启定时器startAnimation
                (如果,定时器是暂停状态,我们_startAngle = -90,开启定时器,否则~~)

        6. 配置中间显示数字的Label
            6.1 中间lab 初始化为0,滚动之后的值等于显示圆的终点
            6.2 声明一个显示的值,我一个初始化的值。
            6.3 在定时器的响应时间中我们要判断,当我们初始化的值也就是开始的值大于或等于显示的值,我们才可以
                创建显示圆的路径,并且定时器暂停,否则,_startRate ++;以达到同步绘制.
            6.4 设置动画的时候,_startRate = 0;
            6.5 设置显示值的范围 (写setRate:方法)
                           <= 0 / > 100  ==> rate = 100;
                           else { _rate = rate }
            6.6 给lab 赋值(赋值  只能是0~100)
/****************************************************************************************************************************/
 使用:
    CustomCircleView *circleView = [[CustomCircleView alloc]initWithFrame:(CGRect){(self.view.bounds.size.width - 100) * 0.5,100,100,100}];
      _circleView.rate =?; (必须为纯数字0~100)
     [self.view addSubview:_circleView];

//   为什莫+ 3.6    ,100 X3.6 = 360  lab 与绘图同步

3. 项目代码示例

CustomCircleView.h

#import <UIKit/UIKit.h>

@interface CustomCircleView : UIView
// 中间显示的数字
@property (nonatomic, assign) NSInteger rate;
// 开始动画
- (void)startAnimation;
@end

CustomCircleView.m

#import "CustomCircleView.h"

#define LineWidth 4

@interface CustomCircleView ()
{
    CGFloat _startAngle; // 开始的角度
    NSInteger _startRate;

}
//   半径r
@property(nonatomic,assign) CGFloat rWidth;
//    显示圆的边缘图层
@property(nonatomic,strong) CAShapeLayer *shapeLayer;
//    定时器
@property(nonatomic,strong) CADisplayLink *displayLink;
//     显示圆的路径
@property(nonatomic,strong) UIBezierPath *bPath;
//      显示Lab
@property (nonatomic, strong) UILabel *rateLbl;
@end

@implementation CustomCircleView

-(instancetype)initWithFrame:(CGRect)frame
{
      self =  [super initWithFrame:frame];
   if (self) {
        _startAngle = -90; // 从圆的最顶部开始
        _rWidth = frame.size.width;
        _bPath = [UIBezierPath bezierPath];
        // 先画一个底部的圆
        [self configBgCircle];
        // 配置CAShapeLayer
        [self configShapeLayer];
        // 配置CADisplayLink
        [self configDisplayLink];
        // label
        [self configLab];
    }
    return self;

}

#pragma mark - 底下灰色的圆(辅助圆)
- (void)configBgCircle
{
    UIBezierPath *bPath = [UIBezierPath bezierPathWithArcCenter:(CGPoint){self.bounds.size.width *0.5,self.bounds.size.height *0.5} radius:_rWidth * 0.5 startAngle:0 endAngle:360 clockwise:YES];
     CAShapeLayer *shaperLayer = [CAShapeLayer layer];
     shaperLayer.lineWidth = LineWidth;
     shaperLayer.strokeColor = [UIColor lightGrayColor].CGColor;
     shaperLayer.fillColor = nil;
     shaperLayer.path = bPath.CGPath;
     [self.layer addSublayer:shaperLayer];
}

#pragma mark 配置CAShaperLayer(用于显示圆)
- (void)configShapeLayer
{
    _shapeLayer = [CAShapeLayer layer];
    _shapeLayer.lineWidth = LineWidth;
    _shapeLayer.strokeColor = [UIColor redColor].CGColor;
    _shapeLayer.fillColor = nil;
    [self.layer addSublayer:_shapeLayer];
}

#pragma mark 配置CADisplayLink
- (void)configDisplayLink
{
    _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawCircle)];
    [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    _displayLink.paused = YES; //  默认暂停
}

#pragma mark - 中间显示数字的Label
- (void)configLab
{
    CGFloat rateLabX = 10;
    CGFloat rateLabW = self.frame.size.width - 2 * rateLabX;
    CGFloat rateLabH = 40;
    CGFloat rateLabY = (self.frame.size.height - rateLabH) * 0.5;
    UILabel *lab = [[UILabel alloc] initWithFrame:CGRectMake(rateLabX, rateLabY, rateLabW, rateLabH)];
    _rateLbl = lab;
    lab.textAlignment = NSTextAlignmentCenter;
    lab.textColor = [UIColor blackColor];
    lab.text = @"0%";
    [self addSubview:lab];
}
#pragma mark - event response
- (void)drawCircle
{
    if (_startRate >= _rate) {
        _bPath = [UIBezierPath bezierPath];
        _displayLink.paused = YES;
        return;
     }
  _startRate ++;
  _rateLbl.text = [NSString stringWithFormat:@"%ld%%",_startRate];

    [_bPath addArcWithCenter:CGPointMake(self.frame.size.width * 0.5, self.frame.size.height * 0.5) radius:_rWidth * 0.5  startAngle:(M_PI /180.0) *_startAngle endAngle:(M_PI /180.0) *(_startAngle + 3.6) clockwise:YES];
    _shapeLayer.path = _bPath.CGPath;
    _startAngle += 3.6;
    }
#pragma mark - public methods
- (void)startAnimation
{
if (_displayLink.paused == YES) {
    _startAngle = -90;
    _startRate = 0;
    _displayLink.paused = NO;
}
}

#pragma mark - getter/setter
- (void)setRate:(NSInteger)rate
{
    if (rate <= 0 || rate >100) {
        rate = 100;
    }else{
     _rate = rate;
    }
}
@end

总结,是一个学习的过程,虽然仍很迷茫,希望遇伯乐,北京。