动画黄金搭档:CADisplayLink & CAShapeLayer

我们在开发中有时会遇到一些看似非常复杂的动画,不知该如何下手,今天的这篇文章中我会讲到如何利用CADisplayLink和CAShapeLayer来构建一些复杂的动画,希望能在你下次构建动画中,给你一些启发。(备注:收藏下来以供学习,如需转载请备注原创:夏树正茂 投稿)

在接下来的文章中,我们会构建如下的一个动画:

该动画是在du的轮廓中进行,类似一个镂空效果,轮廓的填充是用双波浪的形式,类似于水流慢慢注入容器的过程。
动画使用CADisplayLink来进行刷新,保证了动画的流程性,利用CAShapeLayer来构建波浪的轮廓,最后利用CALayer的mask属性来实现逐渐填充的过程。

背景知识介绍

在动画创建过程的讲解之前,先介绍一下会使用到的一些知识点:

  • CADisplayLink
  • UIBezierPath
  • CAShapeLayer
  • mask

如果你已经对这些概念有了充分的了解,可以略过背景知识介绍这一节。

1、CADisplayLink

用绘制的方式构建的动画,必然需要不断的刷新绘制的内容来呈现流畅的动画,CADisplayLink就像是一个定时器,每隔几毫秒刷新一次屏幕。能让我们以和屏幕刷新频率相同的频率去刷新我们绘制到屏幕上的内容。
CADisplayLink的使用方式如下:


1

2

3

4

  _displayLink = [CADisplayLink displayLinkWithTarget:self

                                            selector:@selector(updateContent:)];

   [_displayLink addToRunLoop:[NSRunLoop currentRunLoop]

                      forMode:NSRunLoopCommonModes];

当CADisplayLink注册到runloop以后,屏幕刷新的时候就会调用绑定到它上面的target所拥有的selector方法。停止CADisplayLink的运行非常的简单,只需要调用它的invalidate方法。

NSTimer和CADisplayLink有什么不同?

iOS设备的屏幕每秒会刷新60次,正常情况下CADisplayLink在屏幕每次刷新时都会调用,精确度非常高,并且CADisplayLink的使用场合相对专一,适合做UI的不停重绘,比如动画的连续绘制。

NSTimer的使用范围要广泛很多,可以做单次或者循环处理某个任务,精度相比CADisplayLink要低。

2、UIBezierPath

使用UIBezierPath类可以创建基于矢量的路径,它是Core

Graphics框架关于CGPathRef类型数据的封装,利用它创建直线或者曲线来构建我们想要的形状,每一个直线段或者曲线段的结束位置就是下一个线段开始的地方。这些连接的直线或者曲线的集合成为subpath。一个UIBezierPath对象的完整路径包括一个或者多个subpath。

创建一个完整的UIBezierPath对象的完整步骤如下:

  • 创建一个Bezier Path对象。
  • 使用方法moveToPoint:去设置初始线段的起点。
  • 添加line或者curve去定义一个或者多个subpath。
  • 修改UIBezierPath对象跟绘图相关的属性。

3、CAShapeLayer

CAShapeLayer是一个通过矢量图形而不是bitmap来绘制的图层子类。CAShapeLayer可以用来绘制所有通过CGPath来表示的形状,上面讲到了可以用UIBezierPath来创建任何你想要的路径,使用CAShapeLayer的属性path配合UIBezierPath创建的路径,就可以呈现出我们想要的形状。
这个形状不一定要闭合,图层路径也不一定是连续不断的,你可以在CAShapeLayer的图层上绘制好几个不同的形状,但是你只有一次机会去设置它的path、lineWith、lineCap等属性,如果你想同时设置几个不同颜色的多个形状,你就需要为每个形状准备一个图层。

下面的示例代码是通过UIBezierPath和CAShapeLayer来创建一个简单的火柴人。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

  - (void)viewDidLoad {

    [super viewDidLoad];

    

    UIBezierPath *path = [[UIBezierPath alloc] init];

    [path moveToPoint:CGPointMake(175, 100)];

    

    [path addArcWithCenter:CGPointMake(150, 100) radius:25 startAngle:0 endAngle:2*M_PI clockwise:YES];

    [path moveToPoint:CGPointMake(150, 125)];

    [path addLineToPoint:CGPointMake(150, 175)];

    [path addLineToPoint:CGPointMake(125, 225)];

    [path moveToPoint:CGPointMake(150, 175)];

    [path addLineToPoint:CGPointMake(175, 225)];

    [path moveToPoint:CGPointMake(100, 150)];

    [path addLineToPoint:CGPointMake(200, 150)];

    

    //create shape layer

    CAShapeLayer *shapeLayer = [CAShapeLayer layer];

    shapeLayer.strokeColor = [UIColor colorWithRed:147/255.0 green:231/255.0 blue:182/255.0 alpha:1].CGColor;

    shapeLayer.fillColor = [UIColor clearColor].CGColor;

    shapeLayer.lineWidth = 5;

    shapeLayer.lineJoin = kCALineJoinRound;

    shapeLayer.lineCap = kCALineCapRound;

    shapeLayer.path = path.CGPath;

    //add it to our view

    [self.view.layer addSublayer:shapeLayer];

}

显示的形状如下:

4、mask

CALayer有一个属性叫做mask,通常被称为蒙版图层,这个属性本身也是CALayer类型,有和其他图层一样的绘制和布局属性。它类似于一个子视图,相对于父图层(即拥有该属性的图层)布局,但是它却不是一个普通的子视图。不同于一般的subLayer,mask定义了父图层的可见区域,简单点说就是最终父视图显示的形态是父视图自身和它的属性mask的交集部分。

mask图层的color属性是无关紧要的,真正重要的是它的轮廓,mask属性就像一个切割机,父视图被mask切割,相交的部分会留下,其他的部分则被丢弃。
CALayer的蒙版图层真正厉害的地方在于蒙版图层不局限于静态图,任何有图层构成的都可以作为mask属性,这意味着蒙版可以通过代码甚至是动画实时生成。这也为我们实现示例中波浪的变化提供了支持。

绘制波浪轮廓

我们利用UIBezierPath来绘制波浪的轮廓,通过正弦函数和余弦函数来创建顶部的波浪曲线,在单位为1的右手直角坐标系中的曲线变化如下:

可以看到在(-2π , 2π )的范围类,y值在[-1, 1]之间变化。
以正弦曲线为例,它可以表示为y=Asin(ωx+φ)+k,公式中各符号表示的含义:

  • A–振幅,即波峰的高度。
  • (ωx+φ)–相位,反应了变量y所处的位置。
  • φ–初相,x=0时的相位,反映在坐标系上则为图像的左右移动。
  • k–偏距,反映在坐标系上则为图像的上移或下移。
  • ω–角速度,控制正弦周期(单位角度内震动的次数)。

通过上面的函数,我们就能计算出波浪曲线上任意位置的坐标点。通过UIBezierPath的函数addLineToPoint来把这些点连接起来,就构建了波浪形状的path。只要我们的设定相邻两点的间距够小,就能构建出平滑的正弦曲线,构建正弦波浪的代码如下:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

  - (UIBezierPath *)createSinPath

{

    UIBezierPath *wavePath = [UIBezierPath bezierPath];

    CGFloat endX = 0;

    for (CGFloat x = 0; x < self.waveWidth + 1; x += 1) {

        endX=x;

        CGFloat y = self.maxAmplitude * sinf(360.0 / _waveWidth * (x  * M_PI / 180) * self.frequency + self.phase * M_PI/ 180) + self.maxAmplitude;

        if (x == 0) {

            [wavePath moveToPoint:CGPointMake(x, y)];

        else {

            [wavePath addLineToPoint:CGPointMake(x, y)];

        }

    }

   

    CGFloat endY = CGRectGetHeight(self.bounds) + 10;

    [wavePath addLineToPoint:CGPointMake(endX, endY)];

    [wavePath addLineToPoint:CGPointMake(0, endY)];

    

    return wavePath;

}

显示的效果如下:

在这里我们设定了两个正弦曲线上的点的横坐标间距是1,现在来解释一下通过横坐标x来得出y的计算过程:


1

y = self.maxAmplitude * sinf(360.0 / _waveWidth * (x  * M_PI / 180) * self.frequency + self.phase * M_PI/ 180) + self.maxAmplitude;

第一个self.maxAmplitude表示曲线的波峰值,360.0
/ _waveWidth计算出单位间距1pixel代表的度数,x * M_PI /
180表示将横坐标值转换为角度。self.frequency表示角速度,即单位面积内波动次数,波浪的大小。self.phase * M_PI/
180代表上面公式中的初相,通过规律的变化初相,可以制造出曲线上的点动起来的效果,self.maxAmplitude代表偏距,由于我们需要让波浪曲线的波峰在layer的范围内显示,所以需要将整个曲线向下移动波峰大小的单位,因为CALayer使用左手坐标系,所以向下移动需要加上波峰的大小。

让波浪曲线动起来

正弦或者余弦曲线上的点,不论角度如何,在y轴上的变化范围在它的波峰与波谷之间。拿单位正交直角坐标系来说,只要我们规律性的增加角度值,那么曲线上的点就会在[1, -1]之间变化,我们以曲线上x=0的点为例,角度的不断增加,会让它的y值规律性的来回变化:

如若曲线上的点都能这样规律的变化,我们就能让波浪曲线浪起来。
要让曲线上所有的点都动起来,在这里我们使用CADisplayLink来不断刷新由UIBezierPath创建的形状,两次刷新之间曲线的变化通过增加初相来实现,代码如下:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

- (void)startLoading

{

    [_displayLink invalidate];

    self.displayLink = [CADisplayLink displayLinkWithTarget:self

                                                   selector:@selector(updateWave:)];

    [_displayLink addToRunLoop:[NSRunLoop currentRunLoop]

                       forMode:NSRunLoopCommonModes];

}

- (void)updateWave:(CADisplayLink *)displayLink

{

    self.phase += 8;//逐渐累加初相

    self.waveSinLayer.path = [self createSinPath].CGPath;

}

 

- (UIBezierPath *)createSinPath

{

    UIBezierPath *wavePath = [UIBezierPath bezierPath];

    CGFloat endX = 0;

    for (CGFloat x = 0; x < self.waveWidth + 1; x += 1) {

        endX=x;

        CGFloat y = self.maxAmplitude * sinf(360.0 / _waveWidth * (x  * M_PI / 180) * self.frequency + self.phase * M_PI/ 180) + self.maxAmplitude;

        if (x == 0) {

            [wavePath moveToPoint:CGPointMake(x, y)];

        else {

            [wavePath addLineToPoint:CGPointMake(x, y)];

        }

    }

    

    CGFloat endY = CGRectGetHeight(self.bounds) + 10;

    [wavePath addLineToPoint:CGPointMake(endX, endY)];

    [wavePath addLineToPoint:CGPointMake(0, endY)];

    

    return wavePath;

}

把CAShapeLayer的背景色设置为淡红色,波浪曲线会在Layer的bounds类波动,动起来的波浪曲线如下:

构建波浪上升的镂空效果

到目前为止,我们利用CAShapeLayer、UIBezierPath以及CADisplayLink实现了动起来的波浪效果,下面我们需要实现的是在du的轮廓里,水波不断上升填充的过程,整个动画过程中,会呈现一个du的镂空效果,在它轮廓之外的水波则不会显示,这样的效果需要借助CALayer的mask属性来实现。

我们需要的动画素材如下:

将这三个图片位置设置为一样,底层放置动画中一直显示的底层轮廓,中间层用以实现余弦波浪,最外层用于实现正弦波浪,将中间层和最外层图片的mask属性赋值为我们创建的用来呈现余弦波浪和正弦波浪的CAShapeLayer,这样动画开始后,利用CADisplayLink来不断刷新两个CAShapeLayer的path来让波浪浪起来,再利用CABasicAnimation来对两个CAShapeLayer的position进行动画,实现从下到上的填充效果。我们想要的效果就完成了:

完整的代码示例在这里

参考

时间: 2024-10-03 07:35:46

动画黄金搭档:CADisplayLink & CAShapeLayer的相关文章

动画黄金搭档:CADisplayLink&amp;CAShapeLayer

我们在开发中有时会遇到一些看似非常复杂的动画,不知该如何下手,今天的这篇文章中我会讲到如何利用CADisplayLink和CAShapeLayer来构建一些复杂的动画,希望能在你下次构建动画中,给你一些启发. 在接下来的文章中,我们会构建如下的一个动画: 该动画是在du的轮廓中进行,类似一个镂空效果,轮廓的填充是用双波浪的形式,类似于水流慢慢注入容器的过程.动画使用CADisplayLink来进行刷新,保证了动画的流程性,利用CAShapeLayer来构建波浪的轮廓,最后利用CALayer的ma

&nbsp; &nbsp; &nbsp; MHVTL安装--搭建备份软件测试环境的黄金搭档

MHVTL安装--搭建备份软件测试环境的黄金搭档 mhvtl download https://sites.google.com/site/linuxvtl2/home#vtl-getting-started VTL OS CENTOS6.5 64bit Backuper Server:Networker8 for Windows Server2008 x64RedHat/CentOS/Scientific Linux/Oracle Linux distributions: 1.安装MHVTL

JavaScript大杂烩8 - 理解文本解析的"黄金搭档"

文本解析"黄金搭档" - String与RegExp对象 文本解析是任何语言中最常用的功能,JavaScript中也是一样,而正则表达式作为最常用的方式,JavaScript也同样是支持的,下面就来看看字符串对象与正则表达式对象的配合. 字符串的恒定性 在正式开始讨论字符串对象的成员之前,我们需要了解一点,那就是:与C#一样,JavaScript 的字符串是不可变的(immutable),String对象定义的方法都不会改变字符串的内容.像toUpperCase这样的方法,返回的是全新

mutex&condition variable 黄金搭档之 多消费者多生产者

Condition Variable都会搭配一个Mutex来用.我们知道Mutex的普通意义上是维持一个互斥变量,从而保证一个或一组操作的原子性.同样,简单的说Mutex加在Condition Variable上也是为了保证它的原子性了.Condition Variable,有条件的唤醒机制.最经典不过的就是生产--消息者模型了.但有一个细节,消费者需要有"产品"才能继续它的消费行为,因此当消费者发现"产品"被消费完了?它该怎么办?没错,普通情况下它就会进入等待挂起

深度学习的黄金搭档:GPU正重塑计算方式(转载)

转载:原文链接 深度学习的黄金搭档:GPU正重塑计算方式 OFweek电子工程网讯 随着神经网络和深度学习研究的不断深入——尤其是语音识别和自然语言处理.图像与模式识别.文本和数据分析,以及其他复杂领域——研究者们不断在寻找新的更好的方法来延伸和扩展计算能力. 几十年来,这一领域的黄金标准一直是高性能计算(HCP)集群,它解决了大量处理能力的问题,虽然成本有点过高.但这种方法已经帮助推动了多个领域的进步,包括天气预测.金融服务,以及能源勘探. 然而,2012 年,一种新的方法出现了.伊利诺伊大学

大型网站网络结构过渡与软件架构的黄金搭档

前言 你可能常常听说,F5.集群.网站架构等名词,却又不知道何为网站架构,更不知一个大型高并发网站的演变过程. 如果你是个JavaEE初级程序员,想明白除了软件开发,你还可以做什么. ---那么,这篇文章可以让你领略一二. 了解数据怎么变大,网站由小向大过渡结构(通过不断硬件的扩充实现大的负载) 1) 静态网站 背景:最开始,我们想做一个小网站,一个静态网页,把自己的个人简历放上去. 需求:需要一个网络空间(在服务器上,这个服务器很多用户一起使用) 结构图: 说明:为了保证服务器数据的安全性,个

触发时,文本出现颜色。事件是黏合应用程序中所有用户交互的胶水。DOM和事件的是JavaScript中的黄金搭档

<html> <head> <title>demo</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <script type="text/javascript"> /* 示例操作DOM元素 */ window.onload = function(){ //给Dom元素添

安卓开发黄金搭档:android-studio+Genymotion模拟器

最近换了电脑,重新配置安卓开发环境,记录一下: 早期研究安卓开发,用eclipse觉得挺臃肿庞大,后来出了android-studio,等版本渐渐稳定下来用起来还不错,但是还是比较难忍受AVD模拟器的缓慢启动速度,又不喜欢一直用真机联调,后来发现Genymotion,立马有种拨开云雾见月明的感觉,启动快,即有真机速度,又适配多种机型.配置步骤如下: 一.安装android-studio android-studio不用说了,出了多个版本,现在最新的是0.8.9,可以在下面网址下载: https:

黄金点游戏的尝试与编程习惯的改变尝试

这一次,软件工程老师给我们所有同学出了个难题,什么呢?结对编程!说到结对编程,我想至少对于我来说是一个完全陌生的领域,为什么这么说呢?原来的C语言作业也好,C++作业也好,我大多是单枪匹马的自己在做前期的构想,中期的编程调试,到后期的查找错误修改问题,即使是有其他人的参与也只限于我腆着脸去找同班同学或者其他班的大神求助,但大体上还是我一个人在战斗.但是这次不一样,这次按照老师布置的作业要求,需要结对的两人坐在一台电脑前,合用一个键盘,一个鼠标,共同编写一个程序代码.这样,编程的全过程就变得和原来