Core Animation基础
文章目录
- 简述
- CALayer简述
- 形状与边框
- 背景与透明度
- 阴影效果
- 遮罩
- 基础的动画
- CABasicAnimation
- 平行层级关系
- CGAffineTransform
- 平移,旋转~~,轴对称~~
- 缩放
- CATransform3D
- 参考文献
“嗯,圆和椭圆还不错,但如果是带圆角的矩形呢?
我们现在能做到那样了么?”
史蒂芬·乔布斯
摘录来自
ios核心动画高级技巧
此材料受版权保护。
简述
核心动画的名字虽然叫核心动画,但实际上做动画这只是核心动画的一角
核心动画的存在不是为了替代View,而是与View集成,提供更好的性能和动画效果
核心动画通过将View缓存为位图,让GPU可以直接管理
核心动画的架构如下
比较重要的分支有两个
一个是CAAnimation的动画分支
更重要的是CALayer的图形分支
CALayer中有巨量的专用图层,可以实现复杂的效果
CALayer简述
CALayer
在与UIView
很像,都有层级关系,都可以填充文本,颜色,图片,关键在于CALayer
不可以响应用户交互事件(单一职责原则)
与UIView的关系
每一个UIView
都有layer,叫做backing layer, UIView
的职责在于管理创建这个CALayer
确保我们可以显示视图
同时可以使用layer来对view进行一些美化操作
这里介绍一些属性:
形状与边框
-
cornerRadius
:让视图的四个角变得圆润。这个属性非常常用,可以用来制作卡片、圆形头像等效果// 设置圆角半径为 8 myView.layer.cornerRadius = 8.0; // 如果要让视图变成圆形,将半径设置为视图宽度的一半 myView.layer.cornerRadius = myView.frame.size.width / 2.0;
-
borderWidth
:设置边框的宽度// 设置边框宽度为 2 点 myView.layer.borderWidth = 2.0;
-
borderColor
:设置边框的颜色。注意,它的类型是CGColor
,需要从UIColor
转换// 设置边框颜色为蓝色 myView.layer.borderColor = [UIColor blueColor].CGColor;
背景与透明度
-
backgroundColor
:设置图层的背景色。同样需要转换为CGColor
// 设置背景色为红色 myView.layer.backgroundColor = [UIColor redColor].CGColor;
-
opacity
:控制图层的不透明度,范围从0.0
(完全透明)到1.0
(完全不透明)。常用于实现淡入淡出效果// 设置透明度为 50% myView.layer.opacity = 0.5;
阴影效果
阴影是增加视图立体感和层次感的最佳方式。
-
shadowColor
:阴影的颜色,通常使用黑色。myView.layer.shadowColor = [UIColor blackColor].CGColor;
-
shadowOpacity
:阴影的透明度,范围从0.0
到1.0
。如果设置为0
,阴影将不可见。myView.layer.shadowOpacity = 0.5;
-
shadowOffset
:阴影的偏移量。width
控制水平方向,height
控制垂直方向。默认是(0, -3)
,即向上偏移// 设置阴影向右下角偏移 myView.layer.shadowOffset = CGSizeMake(3, 3);
-
shadowRadius
:阴影的模糊半径。值越大,阴影越柔和// 设置阴影模糊半径 myView.layer.shadowRadius = 5.0;
遮罩
-
masksToBounds
:一个布尔值,决定图层的内容是否被它的边界所裁剪// 裁剪掉超出边界的内容 myView.layer.masksToBounds = YES;
基础的动画
核心动画的内部实际上是有巨多巨复杂的概念,所以这里我就简单介绍一下动画该怎么显示和使用,不会涉及内部的原理之类的
CABasicAnimation
使用animationWithKeyPath
来创建一个基础动画
CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:@"position.y"];
对于要修改的属性,OC中一般使用字符串来表示,如这里的position.y
在获取到实例之后,我们就可以编辑这个动画对象了
这里变换的是一个控件的高度
animation.toValue = @(400);
animation.duration = 1;
然后把这个动画交给对应视图的layer即可
[self.mainView.glass.layer addAnimation:animation forKey:nil];
这里可以看到,动画结束后,对应的控件又回到了原本的位置,这里涉及到一个平行层级关系
平行层级关系
每个UIView
有一个CALayer
类型的layer
属性,被称为backing layer。View 负责创建、管理 layer,以确保子视图添加、移除时,其关联的图层也在相应层级中进行同样操作
屏幕内容的显示和动画都是由 backing layer 负责的,UIView
仅仅是它的封装,提供一些针对 iOS 的功能(如响应手势事件)和核心动画底层功能的高级封装
为什么 iOS 有两个平行层级的UIView
和CALayer
?
为什么不使用一个层级处理所有事件?
同时使用UIView
和CALayer
是为了职责分离,避免代码重复
iOS 和 macOS 中事件和用户交互的处理逻辑有很多不同。多点触摸的触控屏与鼠标、键盘控制的完全不同,这也是 iOS 使用UIKit
的UIView
,macOS 使用AppKit
的NSView
的原因,它们功能相同,但实现方式不同
在 iOS 和 macOS 中,都有渲染、布局、动画这些共同概念,将这些基础功能独立到 Core Animation框架,可以在 iOS、macOS 间共享代码,方便 Apple 系统开发人员开发系统,以及第三方开发者开发跨平台应用。
实际中,这里不是两个平行层级,而是四个平行层级,各自执行不同功能。层级如下:
- 视图层级(View tree)
- 图层树(Layer tree)
- 呈现树(Presentation Tree)
- 渲染树(Render Tree)
-
视图层级
视图层级实际上就是我们在平常创建的UIView对象的层级结构,它包含了所有
UIView
实例和他们的复制关系它主要负责处理:
用户的交互,例如触摸手势,点击事件
管理布局,负责视图的排列和大小
响应者链,管理事件的传递和响应
-
图层树
每个
UIView
都有一个与之关联的CALayer
对象。图层树就是这些CALayer
对象所构成的层级结构。它是视图层级的核心数据模型,包含了视图的所有视觉属性,如颜色、边框、阴影、三维变换等它主要负责:
存储视觉数据,它是
CALayer
的真实数据模型,存储了所有的可动画属性的最终状态隐式动画,当没有包裹在动画块里时,直接修改它的属性会默认出发默认的隐式动画(理论上)
它定义了视图最终的样子包括颜色,形状,位置等等所有的视觉细节
-
呈现树
一个动态副本,只在动画执行期间存在,反映了所有动画层在每一时刻的实时状态
它负责:
- 提供实时的数据,在动画过程中,它的属性值会不断的变化,提供当前动画的当前帧数据
- 渲染引擎会从呈现树中的数据来绘制屏幕上的每一帧
-
渲染树
一个核心动画框架内部高度维护的,用于渲染的树状结构,是一个私有对象,一般来说无法直接访问
当我们把动画添加到图层的时候,我们并没有改变原本的视图层级中的数据,所以在动画执行完毕,被移除的时候,图层还是会移动到之前的位置
在我们使用CABasicAnimation
来创建动画的前提下,更改属性即可让视图变为原来的样子
在代码中加入如下代码:
animation.removedOnCompletion = NO; // 是否恢复原始状态
animation.fillMode = kCAFillModeForwards;
/*
kCAFillModeForwards:动画结束后,layer会一直保持动画最后的状态
kCAFillModeBackwards:在动画开始的时候,只要将动画加入layer,layer便立即进入动画的初始状态(第一帧),有准备过程,并等待动画开始,并且保持最后
kCAFillModeBoth:上述二者结合
kCAFillModeRemoved:对原始的VIew不会有任何的影响,默认! */
CGAffineTransform
CGAffineTransform是属于Core Graphics框架的,绘制二维图像的仿射变换矩阵
常见的坐标变化如下:
这里使用CGAffineTransform和之后的CATransform3D实际上是给一个控件的transform属性赋值
这个属性
变化,实际上就是系统根据你写的矩阵来对控件的每个点进行运算,最后呈现出变化后的图像
CGAffineTransformMakeTranslation
等这类方法实际上制作了一个矩阵输入给控件的属性,让他呈现一个变化的效果而已
平移,旋转~~,轴对称~~
平移使用CGAffineTransformMakeTranslation
方法,方法中的两个参数分别应x轴和y轴平移的大小
- (void) pressBtn:(UIButton*)button {[UIView animateWithDuration:0.3 animations:^{button.transform = CGAffineTransformMakeTranslation(100, 50);}];NSLog(@"%@",button);NSLog(@"动画变化");
}
效果
旋转使用CGAffineTransformMakeRotation
方法,其接受一个弧度制的数值
其中M_PI_2
表示二分之派的大小,即90度
- (void) pressBtn:(UIButton*)button {[UIView animateWithDuration:0.3 animations:^{button.transform = CGAffineTransformMakeRotation(M_PI_2);}];NSLog(@"%@",button);NSLog(@"动画变化");
}
缩放
缩放使用的方法是CGAffineTransformMakeScale
这里贴出与按钮绑定的方法
- (void) pressBtn:(UIButton*)button {[UIView animateWithDuration:0.3 animations:^{button.transform = CGAffineTransformMakeScale(0.8, 0.8);}];NSLog(@"%@",button);NSLog(@"动画变化");
}
这样我们就在点击的过程中,让按钮在0.3s(第一个参数)的时间内将按钮变为原本的0.8倍
由于按钮不会不会自己变回原本的大小,所以我们增加一个回调操作
- (void) pressBtn:(UIButton*)button {[UIView animateWithDuration:0.3 animations:^{button.transform = CGAffineTransformMakeScale(0.8, 0.8);}completion:^(BOOL finished) {if (finished) {button.transform = CGAffineTransformIdentity;}}];NSLog(@"%@",button);NSLog(@"动画变化");
}
但是这样的代码有一个问题,在回弹的时候,会直接跳转到原本状态
我们在回调里再加入一个动画即可
最终代码和效果如下
- (void) pressBtn:(UIButton*)button {[UIView animateWithDuration:0.3 animations:^{button.transform = CGAffineTransformMakeScale(0.8, 0.8);}completion:^(BOOL finished) {if (finished) {[UIView animateWithDuration:0.2 animations:^{button.transform = CGAffineTransformIdentity;}];}}];NSLog(@"%@",button);NSLog(@"动画变化");
}
CATransform3D
Core Graphics 是 2D 绘制 API,CGAffineTransform
只用于 2D 变换。CATransform3D
允许在三维空间操控 layer
CATransform3D
是个 4*4 矩阵,能够对任意点进行 3D 变换
创建CATransform3D
简便方法如下:
CATransform3DMakeRotation(angle, x, y, z)
:返回围绕向量(x, y, z) 旋转 angle 弧度的变换CATransform3DMakeScale(sx, sy, sz)
:返回缩放比例为(sx, sy, sz) 的变换CATransform3DMakeTranslation(tx, ty, tz)
:返回平移为 (tx, ty, tz) 的变换
我们已经很熟悉x、y轴,z 轴垂直于x、y轴,并向外延伸
并且对上面的函数基本上的功能也了解,所以这里就不多做介绍了
为了便于识别我们加入一张图片作为背景
展示动画代码
// 动画设置
- (void) pressBtn:(UIButton*)button {[UIView animateWithDuration:0.3 animations:^{button.transform3D = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);}];
}
观察移动效果,可以发现,这里的旋转好像并没有立体感
这是因为我们是面向屏幕观看的,没有透视(perspective),为解决此问题,还需修改变换矩阵以包含透视变换,也称为z变换。CATransform3D
的m34
控制着透视效果,m34
的值在变换计算中用于按比例缩放x和y的值,其反映了距离视点的距离
m34
默认值为0,为其赋值 -1.0/d 以添加透视投影,d 为设想中视点与屏幕的距离,单位为 point
通常,500到1000之间会有很好效果,有时大值或小值对某些 layer 排布效果更好,减少距离可以增加透视效果
因此,很小的值看起来会非常失真,而很大的值看起来就像根本没有透视
由于我们使用的animation是动态调整数值的方法,所以你不能像之前一样直接修改m34
// 动画设置
- (void) pressBtn:(UIButton*)button {[UIView animateWithDuration:2 animations:^{CATransform3D transform = CATransform3DIdentity;transform.m34 = -1.0 / 500.0;transform = CATransform3DRotate(transform, M_PI / 4.0, 0, 1, 0);button.transform3D = transform;}];NSLog(@"%@",button);NSLog(@"动画变化");
}
当沿着铁路线去看两条铁轨,或者沿着公路线看两遍整齐的树木时,其连线相交于很远很远的某一点,这点在透视投影中称为消失点(vanishing point),又称为灭点
在真实世界中,消失点一般位于视野的中心。想要在 app 中添加灭点效果,灭点应位于屏幕中心,或者包含所有 3D 视图的中心,在这里简单了解后即可
Core Animation 消失点位于未进行变换的 layer 的anchorPoint
处。如果图层添加了平移变换,layer 平移到了其他位置,anchorPoint
仍然位于平移前位置,灭点也位于平移前的位置
改变position
属性会修改 vanishing point。因此,想要修改 layer 的m34
属性时,应先将 layer 放置于屏幕中心,后使用 translation transform 平移 layer 到目标位置,这样其可以与其他 3D 图层共享同一个消失点,就可以解决立体感出现问题的情况了
参考文献
- https://www.bookstack.cn/read/ios_core_animation_advanced_techniques/chapter1-layers-and-trees.md
- https://www.jianshu.com/p/abf9bde5bd6a
- https://zh.zlibicu.ru/book/17751253/311eb3/ios核心动画高级技巧.html?ts=1512
- https://dev59.com/P2w05IYBdhLWcg3w3lkl
- 【iOS开发-核心动画原理解析,底层进阶/面试/跳槽加薪必看iOS开发教程】 https://www.bilibili.com/video/BV1np4y1r714/?p=3&share_source=copy_web&vd_source=fb5d66e534ab71bb9575a728199792ff