核心动画知识导入
CoreAnimation
框架是基于OpenGL ES 手机端/OpenGL PC端
(iOS13开始为Metal
)与CoreGraphics
图像处理框架的一个跨平台的框架。
了解CoreAnimation
-
CoreAnimation
的封装核心就是去简化OpenGL
图形处理,原因是OpenGL
的学习成本是非常高的;CoreAnimation也可以用作Mac OS
开发 -
Core Graphics
核心绘图 -
Graphics Hardware
图形加速硬件,这个图形硬件就是GPU芯片
,GPU专门用来做计算,GPU并不是显卡,而是显卡需要GPU -
iOS 13
之后OpenGL
更新为Metal
,Metal只针对iOS
和Mac OS
系统;而OpenGL ES
是可以针对整个嵌入式环境
(安卓、黑莓等都可以使用)
GPU
与CPU
的区别
CPU: 中央处理器
(英文Central Processing Unit)是一台计算机的运算核心和控制核心;其功能主要是解释计算机指令以及处理计算机软件中的数据
。
GPU:图形处理器
(英文Graphic Processing Unit)是一个专门的图形核心处理器;GPU是显卡的大脑,决定了该显卡的档次和大部分性能,同时也是2D显卡
和3D显卡
的区别依据;2D显示芯片在处理3D图像和特效时主要依赖CPU的处理能力,称为软加速
;3D显示芯片是将三维图像和特效处理功能集中在显示芯片内,也即所谓的硬件加速
。
主要区别如下:
- CPU需要很强的通用性来处理
各种不同的数据类型
,同时又要逻辑判断
又会引入大量的分支跳转
和中断
的处理,这些都使得CPU的内部结构异常复杂;而GPU面对的则是类型高度统一的
、相互无依赖
的大规模数据和不需要被打断
的纯净
的计算环境 - GPU采用了数量众多的
计算单元
和超长的流水线
,但只有非常简单的控制逻辑并省去了Cache
,而CPU不仅被Cache占据了大量空间,而且还有有复杂的控制逻辑和诸多优化电路,相比之下计算能力只是CPU很小的一部分
。
核心动画的优点
- 简单易用的
高性能混合编程模型
- 用类似于
UIView
一样,使⽤图层来创建复杂的编程接口,更加高效的使用 - 轻量化的数据结构,它可以同时显示让上百个图层产⽣动画效果
- 一套⾮常简单的动画接口,能让动画运⾏在独立的线程中,并可以独⽴于主线程之外
- 一旦动画配置完成并启动,核⼼动画就能独立并完全控制相应的动画帧
- 提⾼应用性能,应⽤程序只有当发生改变的时候才会重绘内容,使用Core Animation 可以不使⽤其他图形API,例如
OpenGL
来获取高效的动画性能. - 灵活的布局管理模型,允许图层相对同级图层的关系来设置属性的位置和⼤小
核心动画图层树结构
CoreAnimation
核心动画的结构图
CAAnimation
是所有动画对象的父类(抽象类,虚类),实现CAMediaTiming
协议,负责控制动画的时间、速度和时间曲线等等,是一个抽象类。
核心动画类中可以直接使用的类
有五个,其中CAAnimation
、CAPropertyAnimation
是抽象类,不能直接使用。
-
CAAnimationGroup
: 动画组,可以将很多种动画合并到一起,组成动画效果 -
CATransition
: 转场动画效果 -
CAKeyframeAnimation
: 关键帧动画效果;values
: 一个NSArray对象;里面的元素称为关键帧
(keyframe),动画对象会在指定的时间(duration)内,依次显示values数组中的每一个关键帧;简单理解为,很多动画帧执行 -
CABasicAnimation
: 基础动画,简单常见的动画效果 -
CASpringAnimation
: iOS9.0之后新增的弹簧效果动画,是CABasicAnimation
的子类
CALayer
与UIView
的区别
-
CALayer
:继承于NSObject
,所以不具备响应不能处理用户交互,负责绘制、渲染图形
-
UIView
: 继承于UIResponder
,所以可以进行事件响应,属性CALayer负责图形绘制与渲染;UIView
是CALayer
的delegate,可以实现一些简单的CALayer的方法,但要实现稍微复杂些的动画效果,就需要借助CALayer,如:阴影,圆角,带颜色的边框
、3D变换
、非矩形范围
、透明遮罩
、多级非线性动画
等,这也就是开发者为什么要使用CALayer
的原因 -
UIView
是用来管理CALayer
,CALayer
才是用来展示
疑问:苹果为什么要拆分成CALayer
与UIView
两个类呢?
CoreAnimation
是iOS与Mac OS共用的框架,而iOS与Mac OX两者的用户交互方式是不同的,iOS是通过手势触摸,Mac OX是通过键盘鼠标;为了兼容两者,单独把CALayer
拆出来只用来绘制渲染图形
CALayer
与UIView
的关系
每一个UIView
上面都会有一个CALayer
作为它的实例图层属性;我们添加的动画实际上是针对CALayer
来做。
Layer Tree
图层树(模型数)主要是设置一些属性
-
模型树
( layer tree):程序中接触最频繁,模型树的对象是模型对象,储存着动画的目标值;当你修改layer的属性时,便是通过模型树上的对象 -
呈现树
(presentation tree):包含正在运行中的动画的动态值,与模型树不同,呈现树始终存储着layer在屏幕当前的状态值,呈现树无法修改,只读;可以通过读取当前值,来做一些其他处理 -
渲染树
(render tree):执行实际的动画,为CoreAnimation
私有
小结:动画的三个动作创建执行动画的CALayer
、创建动画
、添加动画
CALayer常用属性详解
动画案例准备工作:新建空工程CoreAnimation
,在Main.storyboard
文件拖入一个UIView
,背景色配置成红色并进行关连,命名为redView
动画案例一
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *redView;
@property (nonatomic,strong) CALayer *layer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 新建layer,并添加到self.view.layer上
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(100, 100, 100, 100);
layer.backgroundColor = [UIColor greenColor].CGColor;
_layer = layer;
[self.view.layer addSublayer:layer];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
CABasicAnimation *animation = [CABasicAnimation animation];
// 修改动画y的位置到600
animation.keyPath = @"position.y";
animation.toValue = @600;
animation.duration = 1;
[_redView.layer addAnimation:animation forKey:nil];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
运行工程并点击屏幕,我们会发现动画执行完毕后,又回到了初始位置,为什么会这样?
其实在执行动画的过程中会有两个图层:layer层
与presentation层
,真正移动的是presentation层
;动画开始时会先把layer层
隐藏,让presentation层
做动画;动画结束后presentation层
就会移除,layer层
再出现;原因是视图的layer层
根本没有发生变化,动画结束就会恢复到原来的状态。
解决办法:设置animation
的两个属性
//解决动画恢复到初始位置
//当动画完成后,不把presentation层从render树中移除(默认是移除的)
animation.removedOnCompletion = NO;
//当动画结束后,把layer层状态同步到presentation层;此时_redView的frame才会发生变化
animation.fillMode = kCAFillModeForwards;
CABasicAnimation
相当于是一个数据模型
,把该数据模型绑定到layer
上面。
CABasicAnimation
动画的fillMode
属性介绍
-
kCAFillModeForwards
:动画结束后,layer会一直保持动画最后的状态 -
kCAFillModeBackwards
:动画开始前,只要将动画加入一个layer
,layer
便立即进入动画的初始状态并等待动画开始 -
kCAFillModeBoth
:kCAFillModeForwards
与kCAFillModeBackwards
两者的结合,开始前保持动画初始状态,结束后保持动画的最后状态 -
kCAFillModeRemoved
:默认属性
动画案例二:隐式动画
疑问:上面红色图层动画结束恢复到原来的状态,恢复的过程给人的感觉是回弹动画,这就是隐式动画
?
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
_layer.backgroundColor = [UIColor orangeColor].CGColor;
}
我们没有给_layer
层添加动画,但是点击页面_layer
层由绿色变成橙色
的过程,有一种动画的效果,这就是隐式动画
;隐式动画是由CoreAnimation
框架帮我们做的,其默认动画时长是0.25秒
,通过runloop
来执行。上面添加的CABasicAnimation
属于显式动画
。
动画案例三:修改隐式动画
如何修改系统的隐式动画
呢?
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//begin a new transaction
[CATransaction begin];
//设置隐式动画的时长
[CATransaction setAnimationDuration:2.0];
_layer.backgroundColor = [UIColor orangeColor].CGColor;
//动画执行完成的回调
[CATransaction setCompletionBlock:^{
//添加转场动画
//rotate the layer 90 degrees
CGAffineTransform transform = self.layer.affineTransform;
transform = CGAffineTransformRotate(transform, M_PI_2);
self.layer.affineTransform = transform;
}];
[CATransaction commit];
}
CATransaction
类没有属性,也没有实例方法;不能通过alloc init
去创建,只能通过begin
、commit
压栈出栈的方式来管理。
动画案例四:CALayer
常用属性 - Contents
- (void)viewDidLoad {
[super viewDidLoad];
//除了UIImageView 能够显示图片,layer也可以加载图片
UIImage *image = [UIImage imageNamed:@"test.png"];
//不用CGImage的话,图片加载不出来
self.view.layer.contents = (__bridge id)(image.CGImage);
//填充方式
self.view.contentMode = UIViewContentModeScaleAspectFit;
self.view.layer.contentsGravity = kCAGravityResizeAspect;
}
Contents
属性是id
类型,原因是在Mac OS
系统上Contents
属性对CGImage
和NSImage
都会起作用,image.CGImage
实际上赋值的是CGImageRef
类型,CGImageRef
指向的是CGImage结构
,需要进行桥接处理
,开发中如果需要设置背景图可以使用layer
;Contents
是id类型就可以直接在layer
的contents
上面加载一张图片。
-
CALayer
常用属性 -contentsScale
self.view.layer.contentsScale = [[UIScreen mainScreen] scale];
当用代码设置contents
图片时,要⼿动设置图层的contentsScale
属性,避免Retina
屏幕显示错误。
CALayer
常用属性 -makeToBounds
makeToBounds
属性类似于UIView
中的clipsToBounds
属性,含义:是否显示超出边界的内容?
CALayer
常用属性 -contentsRect
-
contentsRect
不是按点来计算的,而是按照单位坐标
; -
OpenGL
的坐标系横向是从-1到1,纵向从1到-1
的过程,center
坐标就是{0, 0}
,而手机都是长方形的,所以OpenGL
会把单位坐标系
转换为设备坐标系
,不同设备有不同的坐标;
contentsRect
比contentsGravity
属性要灵活很多,contentsGravity
属性只能展示图片固定的位置与大小,而contentsRect
可以展示图片的任意内容(只要把单位坐标计算好即可
);如果contentsGravity
不能满足我们的需求时,可以使用contentsRect
属性。
CALayer中HitTest属性的实际使用
下面我们来了解一下UIView
与CALayer
的图层几何
frame
是相对于父视图的坐标,bounds
是从视图自身出发即内部坐标
,center
与position
相当于父视图上面的一个锚点
锚点就是视图中的center属性
和position
属性,实际上就是一个坐标,锚点anchorPoint
是layer
的属性(即position
)。
-
CALayer
常用属性 -ZPosition
手机开发是基于二维平面
的,并不是一个优秀的三维图形显示载体,但是手机中会出现一些立体的粒子效果,于是CALayer
就提供了一个属性ZPosition
,也就意味着CALayer
是三维的;OpenGL
和Metal
默认是一个3D图形api,默认坐标系是三维坐标系,在描述平面图形的时候,z坐标是0;核心动画把z坐标
单独摘出来就是ZPosition
;呈现粒子的时候就必须要用到ZPosition
。
动画案例准备工作:新建空工程CoreAnimation2
,在Main.storyboard
文件拖入两个UIView
,背景色配置成橙色
与红色
并进行关连,命名为view1
与view2
,两个view
的层级关系如下
// ViewController.h文件
- (void)viewDidLoad {
[super viewDidLoad];
self.view1.layer.zPosition = 1.0;
}
运行工程,我们发现橙色view
出现在了红色view
上面,原因是什么呢?
在平面图形上面z轴
默认值是0
,这里把橙色view
的z轴
值设置为1
,就意味着把橙色view
放在了上面;修改了zPosition
属性的值,就更改了深度缓冲区-深度测试
,这里主要与深度缓冲区
有关,本质并不是修改图层的层级关系
。
-
CALayer
常用属性 -Hit Testing
动画案例准备工作:新建空工程CoreAnimation3
,在Main.storyboard
文件拖入一个UIView,背景色配置成红色并进行关连,命名为layerView
,如下图所示
#import "ViewController.h"
@interface ViewController ()
@property (strong, nonatomic) UIView *layerView;
@property (strong,nonatomic) CALayer *blueLayer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//1.Create sublayer
self.blueLayer = [CALayer layer];
self.blueLayer.frame = CGRectMake(0, 0, 100, 100);
self.blueLayer.backgroundColor = [UIColor blueColor].CGColor;
[self.layerView.layer addSublayer:self.blueLayer];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//1.get touchu position relative to main view(获取相对于主视图的触摸位置)
CGPoint point = [[touches anyObject]locationInView:self.view];
//2.get touched layer
CALayer *layer = [self.layerView.layer hitTest:point];
//3.get layer using using hitTest
if(layer == self.blueLayer)
{
NSLog(@"Inside Blue layer");
} else if (layer == self.layerView.layer) {
NSLog(@"Inside Red layer");
}
}
@end
// 运行工程,查看打印日志,成功获取点击的layer层
2022-09-23 22:25:56.044062+0800 CoreAnimation3[22331:14483795] Inside Blue layer
2022-09-23 22:25:59.342807+0800 CoreAnimation3[22331:14483795] Inside Red layer
CALayer
不能响应事件,但是CALayer
的Hit Testing
属性能够获取到点击的图层。
hitTest
方法介绍
// point : 在接收器的局部坐标系(界)中指定的点
// event : 系统保证调用此方法的事件。如果从事件处理代码外部调用此方法,则可以指定nil
// returnValue : 视图对象是当前视图和包含点的最远的后代。
// 如果点完全位于接收方的视图层次结构之外,则返回nil
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
不止CALayer
中有hitTest
方法,UIView
中同样有hitTest
方法;UIView
中该方法的作用:在视图的层次结构中寻找一个最合适的view
来响应触摸事件;该方法会被系统调用,调用时如果返回nil
,即事件被丢弃,否则返回最合适的view
来响应事件。
-
Hit Test
调用顺序
touch -> UIApplication -> UIWindow -> UIViewController.view -> subViews -> ....-> 合适的view
- 事件的传递顺序,与
Hit Test
调用顺序刚好相反
view -> superView ...- > UIViewController.view -> UIViewController ->UIWindow -> UIApplication -> 丢弃事件
说明:
- 首先由
view
来尝试处理事件,如果处理不了,事件将被传递到父视图superView
-
superView
也尝试处理事件,如果处理不了,继续传递给它的父视图UIViewController.view
-
UIViewController.view
尝试处理事件,如果处理不了,把该事件传递给UIViewController
-
UIViewController
尝试处理事件,如果处理不了,把事件传递给UIWindow
- 主窗口
UIWindow
尝试来处理事件, 如果处理不了,将传递给应用单例UIApplication
- 如果
UIApplication
也处理不了,该事件将被丢弃
UIView
的Hit Test
底层实现思路
常见的hitTest不实现的四种情况
(即view不响应事件情况)
view.userInteractionEnabled = NO;
view.hidden = YES;
view.alpha < 0.05;
view 超出 superview 的 bounds;
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
//1.是否响应事件的必然性条件
if (self.userInteractionEnabled == NO || self.alpha < 0.05 || self.hidden == YES)
{
return nil;
}
//2.touch的point在self.bounds内
if ([self pointInside:point withEvent:event])
{
for (UIView *subView in self.subviews)
{
//进行坐标转化
CGPoint coverPoint = [subView convertPoint:point fromView:self];
// 调用子视图的 hitTest 重复上面的步骤。找到了,返回hitTestview ,没找到返回有自身处理
UIView *hitTestView = [subView hitTest:coverPoint withEvent:event];
if (hitTestView)
{
return hitTestView;
}
}
return self;
}
return nil;
}
Hit Testing
应⽤场景-⼦视图超出⽗视图范围
,点击没有响应;如果需求是让超出父视图的范围也能点击,代码实现如下
仿射变换数学原理讲解
-
刚体变换
:只有物体的位置
(平移变换)和朝向
(旋转变换)发生改变,而形状不变;刚性变换是最一般的变换。 -
仿射变换
:仿射变换具有两个旋转因子
和两个缩放因子
,因此具有6个自由度
。不具有保角性和保持距离比的性质,但是原图平行线变换后仍然是平行线。仿射变换主要包括平移变换
、旋转变换
、缩放变换
(也叫尺度变换)、倾斜变换
(也叫错切变换、剪切变换、偏移变换)、翻转变换
,有六个自由度。 -
投影变换
:是最一般的线性变换,有8个自由度
;射影变换保持重合关系和交比不变。但不会保持平行性。即它会使得仿射变换产生非线性效应
。
对于复杂的立体图形,我们要想平移是非常困难的;但是通过仿射变换
,我们就可以很容易的实现,可以对立体图形的任意顶点进行平移,实现代码如下图所示:
下面我们介绍官方文档中的几个矩阵
上图展现了常见的transformations
的矩阵配置;任何乘以identity
矩阵的coordinate
将不会变化,当乘以其他矩阵时,coordinate
的变化和矩阵每个分量都有关;例如,沿着X轴平移,我们需要提供非零的 tx 分量
并让ty
和tz
为0;对于旋转操作,我们应该提供合适的 sine
和 cosine
值。
-
Identity
:单元矩阵 -
Translate
:平移 -
Scale
:缩放 -
Rotate around X axis
:围绕X
轴旋转,X值不变;这里的四维
原因是OpenGL ES/Metal
中描述顶点除了围绕X
、Y
、Z
轴,还有一个W
缩放因子。 -
Rotate around Y axis
:围绕Y
轴旋转,Y值不变 -
Rotate around Z axis
:围绕Z
轴旋转,Z值不变
围绕任意轴旋转
中参数n
表示向量(x,y,z)
,第二个参数表示角度
。