美文网首页
CoreAnimation专题一 CADisplayLink –

CoreAnimation专题一 CADisplayLink –

作者: 路飞_Luck | 来源:发表于2019-08-17 15:42 被阅读0次
    目录
    • iOS绘图系统
      • FPS
      • 绘制动画
    • CADisplayLink
      • 构建CADisplayLink
      • 线性插值
      • 基于CADisplayLink的动画
      • 非线性的插值
    一 iOS绘图系统

    虽然CoreAnimation框架的名字和苹果官方文档的简介中都是一个关于动画的框架,但是它在iOS和OS X系统体系结构中扮演的角色却是一个绘图的角色。

    About Core Animation

    系统体系结构:

    image.png

    解释说明

    1. 可以看到,最上面一层是是应用层(UI层),直接和用户打交道(UIKit框架也就是干这件事的),而真正的绘图层则在下面一层,绿色的这一层。

    2. 绘图层由3个部分组成:最上面是CoreAnimation,是面向对象的。往下就是更底层的东西了:OpenGLCoreGraphics,它们提供了统一的接口来访问绘图硬件。而绘图硬件则是绘图真正发生的地方。

    3. 那我们就可以这样来理解这个体系结构:真正干事的是绘图硬件(通常是GPU),也就是最下面那一块,它负责把像素画到屏幕上。而我们为了命令它画图(如何绘制)需要有方法能访问到它,当然这种硬件层面的东西肯定不能直接访问的,操作系统一定会做限制(如果不加以限制的话可能一些错误的操作将导致系统故障),这里就和面向对象的封装很像了,操作系统封装了硬件层,只提供简单的能够由开发者直接访问的接口,而不同的硬件可能有不同的封装方式,直接访问起来势必相当麻烦(我们的代码需要适配不同的硬件),于是就有了OpenGL,它统一了所有绘图硬件的接口,我们使用OpenGL提供的同一套API就能控制任意的绘图硬件了。

    4. 而OpenGL虽然很强大,但是很少会用到它一些复杂的功能,而简单的功能也是C语言不太好使用,所以具体地针对iOS和OS X系统,苹果为我们封装了OpenGL,没错这就是CoreAnimation

    所以大家可以体会一下,实际上CoreAnimation虽然表面上更多的是提供了动画的功能,但是动画是基于绘图的,所以完全可以把CoreAnimation框架当做一个用来绘图的框架来处理。它直接提供的动画接口实际上是相当少的,而大量的提供了辅助动画的API,我们这里将用到一个大杀器:CADisplayLink

    1.1 FPS

    首先我们从FPS的概念入手来帮助理解CADisplayLink。这里的FPS不是第一人称射击游戏,而是frame per second,也就是帧率,表示屏幕每秒钟刷新多少次。如果帧率为60,表示屏幕每秒刷新60次,并不代表每1/60秒刷新一次,只能表示在1秒钟的时间内屏幕会刷新60次,每次屏幕刷新的间隔并不一定是平均的。

    1.2 绘制动画

    动画是一系列静态图片以极快的速度进行切换形成的,这个速度要快到人眼察觉不出其中的间隙(两张图片切换之间的间隔时间),具体地,这个切换频率必须大于人眼的刷新频率:每秒钟60次。也就是说,如果屏幕刷新频率大于每秒钟60次,那么我们人眼就感受不到两帧图片切换之间的间隙,所以我们感觉起来这些切换就是连续的,这就是动画的产生。也就是说,动画实际上就是以尽量大于60fps的速度在多张静态图片之间进行快速切换。

    二 CADisplayLink

    我们的屏幕每时每刻都在以>60fps的帧率进行刷新,每次刷新都会根据最新的绘制信息重绘屏幕上显示的内容,这样你才能顺利的看见各种动画,比如一个UITableView的滚动效果。CADisplayLink提供了API,每当屏幕刷新的时候,系统会回调我们向CADisplayLink注册的一个方法,也就是说,我们可以在屏幕每次刷新的时候调用一个我们自己的方法。基于上面对绘制动画的认识,肯定我们就能够像系统那样一帧一帧地画动画了。

    2.1 构建CADisplayLink

    我们先提供一个回调方法

    - (void)onDisplayLink:(CADisplayLink *)displayLink {
        NSLog(@"display link callback");
    }
    

    通过target-action的形式来向系统注册回调,然后向runloop中添加displayLink。这里要注意一下runloop中mode的概念。

    1.一个runloop只能在某一个mode中跑,runloop可以在多个mode之间进行切换,
    2.默认的,系统提供了两个mode:NSDefaultRunloopMode和UITrackingRunloopMode。正常情况下是default,但是如果一个scrollView滑动的时候(UITableView是scrollView的子类)runloop就会切换到UITrackingRunloopMode,这时候所有往default里面添加的内容都没法跑起来了。
    3.这也是为什么,如果使用NSTimer的schedule方法来调度timer,当一个tableView滚动的时候timer会停止,就是因为schedule将把timer添加进default,而tableView滚动的时候runloop切换到了UITrackingRunloopMode,此时default中的timer就跑不起来了。

    我们的CADisplayLink应该在这两种情况都能跑,所以我们可以这样来添加:

    [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
    [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:UITrackingRunLoopMode];
    

    这样就把displayLink添加进了两种mode,无论runloop处于哪种mode,我们的displayLink都能被系统调度。这里其实还有一种写法

    [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
    

    NSRunLoopCommonModes后面多了一个s,表示mode的复数形式,意味着多个mode,这里表示向所有被注册为common的mode中添加displayLink。实际上,NSDefaultRunloopMode和UITrackingRunloopMode都被系统注册成了common,所以这样写的效果和前一种是一样的,你在自己使用runloop的时候也可以自定义mode,然后把它注册成为common。

    打印结果

    2019-08-17 15:00:49.010520+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:010
    2019-08-17 15:00:49.026609+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:027
    2019-08-17 15:00:49.043214+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:043
    2019-08-17 15:00:49.060706+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:061
    2019-08-17 15:00:49.076248+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:076
    2019-08-17 15:00:49.094041+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:094
    2019-08-17 15:00:49.109894+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:110
    2019-08-17 15:00:49.126611+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:127
    2019-08-17 15:00:49.143636+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:144
    2019-08-17 15:00:49.160757+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:161
    2019-08-17 15:00:49.176317+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:176
    2019-08-17 15:00:49.193994+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:194
    2019-08-17 15:00:49.210705+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:211
    2019-08-17 15:00:49.226364+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:226
    2019-08-17 15:00:49.243284+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:243
    2019-08-17 15:00:49.260316+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:260
    2019-08-17 15:00:49.276499+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:276
    2019-08-17 15:00:49.294020+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:294
    2019-08-17 15:00:49.310249+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:310
    2019-08-17 15:00:49.326591+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:327
    2019-08-17 15:00:49.343819+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:344
    2019-08-17 15:00:49.359610+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:360
    2019-08-17 15:00:49.376512+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:376
    2019-08-17 15:00:49.393997+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:394
    2019-08-17 15:00:49.410703+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:411
    2019-08-17 15:00:49.427444+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:427
    2019-08-17 15:00:49.444050+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:444
    2019-08-17 15:00:49.460548+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:461
    2019-08-17 15:00:49.476770+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:477
    2019-08-17 15:00:49.493034+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:493
    2019-08-17 15:00:49.510546+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:511
    2019-08-17 15:00:49.526190+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:526
    2019-08-17 15:00:49.543370+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:543
    2019-08-17 15:00:49.560688+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:561
    2019-08-17 15:00:49.576429+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:576
    2019-08-17 15:00:49.592880+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:593
    2019-08-17 15:00:49.610707+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:611
    2019-08-17 15:00:49.626664+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:627
    2019-08-17 15:00:49.644023+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:644
    2019-08-17 15:00:49.660715+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:661
    2019-08-17 15:00:49.676896+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:677
    2019-08-17 15:00:49.693550+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:693
    2019-08-17 15:00:49.709966+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:710
    2019-08-17 15:00:49.726642+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:727
    2019-08-17 15:00:49.743258+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:743
    2019-08-17 15:00:49.760633+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:761
    2019-08-17 15:00:49.776308+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:776
    2019-08-17 15:00:49.794030+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:794
    2019-08-17 15:00:49.809933+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:810
    2019-08-17 15:00:49.826592+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:827
    2019-08-17 15:00:49.842970+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:843
    2019-08-17 15:00:49.859638+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:860
    2019-08-17 15:00:49.876409+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:876
    2019-08-17 15:00:49.894044+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:894
    2019-08-17 15:00:49.909634+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:910
    2019-08-17 15:00:49.926425+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:926
    2019-08-17 15:00:49.943997+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:944
    2019-08-17 15:00:49.960561+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:961
    2019-08-17 15:00:49.976851+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:977
    2019-08-17 15:00:49.994037+0800 CADisplayLinkAnimation[5258:3737973] display link callback:2019-08-17 15:00:49:994
    

    一旦我们把displayLink添加进了runloop,它就已经准备好进行回调了,每当屏幕刷新的时候,就会调用我们注册的回调方法。运行我们的程序,就会发现控制台开始疯狂的进行打印输出。NSLog是日志打印,所以能提供该次打印的系统时间,看看两次打印的间隔,差不多在1/60秒左右,即在15:00:49一秒内打印了60次。

    2.2 线性插值

    为了实现基于CADisplayLink的动画,我们首先要弄清一个概念:插值。插值在不同的地方有不同的解释。大家思考一下,我们现在要自己在每一帧进行重绘来实现动画,想象这样一个动画:让一个质点从(10,20)点移动到(300,400),持续时间2.78秒。我们要做的是,在每一次屏幕刷新的时候根据当前已经经历的时间(从动画开始到当前时间)计算出该质点的坐标点并更新它的坐标,也就是我们要解决的是:对于任意时刻t,质点的坐标是多少?

    这里我们将引入线性插值,我们把问题改一下:你现在距离家f米,学校距离家t米,现在你要从当前的位置匀速走到学校,整个过程将持续d秒,问:当时间经过△t后,你距离家多远?

    这是一道很简单的匀速直线运动问题,首先根据距离和持续时间来获得速度:

    v = (t-f)/d

    然后用速度乘以已经经过的时间来获得当前移动的距离:

    △s = v△t = (t-f)/d * △t

    最后再用已经移动的距离加上初始的距离得到当前距离家有多远:

    s = △s + f = (t-f)/d * △t + f

    我们把上面的公式稍微变一下形:

    s = f + (t-f) * (△t/d)

    这里令p = △t/d就有:

    s = f + (t-f) * p

    这就是线性插值的公式:

    value = from + (to - from) * percent

    from表示起始值,to表示目标值,percent表示当前过程占总过程的百分比(上个例子中就是当前已经经历的时间占总时间的百分比所以是△t/d),这个公式成立的前提是变化是线性的,也就是匀速变化,所以叫做线性插值。

    有了这个公式,我们回到代码上面来,使用CADisplayLink加上线性插值来计算每帧所需的数据以实现一个匀速动画

    1.3 基于CADisplayLink的动画

    我们已经构建好了CADisplayLink,剩下的只需要添加一个视图然后在CADisplayLink的回调方法中改变视图的坐标就行了,创建一个view。

    @property (nonatomic, strong) UIView * myView;
    
    - (UIView *)myView {
        if (!_myView) {
            _myView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 80, 80)];
            _myView.backgroundColor = [UIColor yellowColor];
        }
        return _myView;
    }
    

    接下来我们用一个私有方法来实现线性插值的公式:

    - (CGFloat)interpolateFrom:(CGFloat)from to:(CGFloat)to percent:(CGFloat)percent {
        return from + (to - from) * percent;
    }
    

    然后在onDisplayLink方法中解决以下问题:

    1.计算当前经历的时间 
    2.当前时间占总时间的百分比 
    3.利用线性插值计算当前的坐标 
    4.更新视图的坐标
    

    首先是如何计算当前经历的时间,由于每次调用onDisplayLink的间隔都不是平均的,我们就不能通过调用次数乘以间隔来得到当前经历的时间,只能用当前时刻减去动画开始的时刻,所以我们声明一个属性用来记录动画开始的时刻,并且在把CADisplayLink添加进runloop的代码后面赋值。

    self.beginTime = CACurrentMediaTime();
    

    这样我们就可以在onDisplayLink方法里面这样获取动画经历的时间了:

    NSTimeInterval currentTime = CACurrentMediaTime() - self.beginTime;
    

    然后计算出百分比,我们先在方法开头定义出动画的起始值、终止值、持续时间:

    CGPoint fromPoint = CGPointMake(10, 20);
    CGPoint toPoint = CGPointMake(300, 400);
    NSTimeInterval duration = 2.78;
    

    这样的话百分比就是:

    CGFloat percent = currentTime / duration;
    

    然后使用线性插值来计算视图的x和y,直接调用公式即可:

    CGFloat x = [self _interpolateFrom:fromPoint.x to:toPoint.x percent:percent];
    CGFloat y = [self _interpolateFrom:fromPoint.y to:toPoint.y percent:percent];
    

    接下来直接使用计算结果来更新视图的center:

    self.myView.center = CGPointMake(x, y);
    

    运行结果如下

    Aug-17-2019 15-29-23.gif

    然后运行就能看见,视图如我们所愿的以动画的形式开始移动了

    1.4 非线性的插值

    刚才的动画是基于线性插值来实现的,也就是匀速变化,如果我们要实现类似ease效果的变速运动应该如何来做呢?这里对大家的数学能力有一定挑战了。

    我们先来看一个easeIn的效果,easeIn的s-t图像大概是这样的:

    image.png

    首先要搞清楚x和y分别代表什么。为了让我们的函数能在任意一种动画情况中使用,我们把定义域和值域都设置为[0,1],那么x代表的就是动画时间的进程了,y代表的就是动画值的进程。进程的意思表示当前值占总进度的百分比,比如考虑这样一个函数y = f(x) = x^2(抛物线函数,拥有easeIn的效果,也就是点的斜率随着x的增大而增大),其中一个点(0.5, 0.25)代表的就是当动画时间进行到50%的时候,动画进程执行了25%。

    如果对动画进程还有不清楚的地方,考虑上面一个动画的例子,视图的center.x从10变为300,也就是f=10, to=300,那么动画进程s就等于视图的x已经改变的值(x-f)除以x一共可以改变的值(t-f)也就是s= (x-f)/(t-f)

    那么我们就建立了一个从动画时间进程p到动画值进程s的一个映射(函数):
    s = f(p),这个映射只要满足其图像上面的点的斜率随着p的增大而增大就能达到easeIn的效果了,因为点的斜率就代表这一时刻动画的速度,比如s = f(p) = p^2就满足这一easeIn的条件。

    这样我们就有了两个方程:

    s = (x-f)/(t-f) 
    s = f(p) 
    

    那我们就解得动画当前值x和时间进程p的关系

    x = f(p) * (t-f) + f
    

    其中f(p)是一个缓冲函数,满足值域和定义域均为[0,1],你可以任意修改f(p)的表达式来达到各种不同的变速效果。仔细观察就能发现,当f(p)=p时,就是线性插值,这样我们就可以通过时间来求出p后,把p作用于缓冲函数f(p),返回的值再带进线性插值的公式,就能算出我们的动画值了,而匀速动画的缓冲函数恰好就是f(p)=p。

    如果你想实现匀加速动画,恰好匀加速s-t映射就是一个二次函数:s = 1/2at^2 + v0t,其中初速度v0 = 0,那么我们的缓冲函数f(p) = 1/2ap^2。

    现在我们可以将代码修改一下以达到一个easeIn的效果。

    首先定义一个easeIn的缓冲函数:

    - (CGFloat)easeIn:(CGFloat)p {
        return p * p;
    }
    

    然后在回调中作用于percent,将回调方法修改为:

    - (void)onDisplayLink:(CADisplayLink *)displayLink {
    //    NSLog(@"display link callback:%@",[self.dateFormatter stringFromDate:[NSDate date]]);
        // 获取时间间隔
        NSTimeInterval currentTime = CACurrentMediaTime() - self.beginTime;
        CGPoint fromPoint = CGPointMake(10, 20);
        CGPoint toPoint = CGPointMake(300, 400);
        NSTimeInterval duration = 2.78;
        CGFloat percent = currentTime / duration;   // 百分比
        
        if (percent > 1) {  // stop
            percent = 1;
            [displayLink invalidate];
        }
        
        percent = [self easeIn:percent];
        
        // 计算X和Y值
        CGFloat x = [self interpolateFrom:fromPoint.x to:toPoint.x percent:percent];
        CGFloat y = [self interpolateFrom:fromPoint.y to:toPoint.y percent:percent];
        
        // 赋值
        self.myView.center = CGPointMake(x, y);
    }
    

    这样我们就有了一个匀速加速启动的效果了,运行看看。

    Aug-17-2019 15-38-59.gif

    以上就是我们这次关于CADisplayLink的全部内容,我们使用它来实现了一个基于帧重绘的动画,并且我们深入研究了插值和easeIn效果的数学实现。我们将在实践篇中再用一篇来看看CADisplayLink的另一种用法:利用系统自带的一些动画效果实现更多的动画。


    本文摘自
    iOS CoreAnimation专题——技巧篇(一)CADisplayLink –同步屏幕刷新的神器
    非常感谢该作者。


    项目链接地址 - CADisplayLinkAnimation

    相关文章

      网友评论

          本文标题:CoreAnimation专题一 CADisplayLink –

          本文链接:https://www.haomeiwen.com/subject/mxmhsctx.html