iOS动画涉及图形学的一些内容,已经忘记的差不多了,关于动画的笔记准备分两篇,第一篇总结动画基础,第二篇则是完成一个旋转动画的实例。初学iOS,没有太多经验,总结的若有错漏,请各位指正。
1 图层和视图
在学习动画之前,需要先明确几个基本概念,首先是图层和视图。视图是比较熟悉的了,最初学习的时候就会见到有UIViewController,然后控制器会对应一个UIView,这个UIView就是视图。我们知道视图是有层级关系的,从UIWindow->UIView->SubView等。而之前学习中一直没有深究的是,其实每个UIView都有一个CALayer实例的图层属性layer。视图(UIView)的职责就是创建和管理图层(CALayer),视图是对图层的封装,真正在iPhone屏幕上面显示和做动画的其实都是视图所关联的图层。视图和图层的关系是一一对应的,如图1所示为图层树结构,Window Layer, View Layer等分别对应视图中的UIWindow,UIView等。
最初看到这里也很疑惑,为什么要多出来一层封装呢?看了参考资料1才知道是为了提高复用性,因为苹果公司除了iOS还有macOS,一个适用于iPhone,一个用于Mac,Mac基于鼠标和触控板和iPhone基于多点触控的交互很不相同,因此iPhone里面是UIView,Mac里面则是NSView,它们功能类似,但是实现并不同。可是对于绘图,布局以及动画等两个系统其实有很多可以共用的地方,因此独立出一个Core Animation框架(CALayer中的CA就是Core Animation的缩写)用于复用。当然除了视图层级和图层树这两个层级,还有呈现树和渲染树,一共是四个,在动画执行过程中我们要获取图层属性的话要使用呈现树presentationLayer,因为我们的图层树总是指向动画结束的最终位置,无法捕获动画执行过程中的属性值。图2为视图、图层树、呈现树以及渲染树的示意图。
那么CALayer不能做什么,能做什么呢?下面总结一下:
CALayer不能做什么
- 既然是个独立出来可复用的库,那么CALayer是不能响应和处理触控事件的。
CALayer能做什么
- 图形阴影,边框,圆角等。
- 仿射变换。
- 3D变换。
- 透明遮罩,多级非线性动画...
那么既然CALayer可以做这些事情,我们写个demo测试一下,在一个黄色的UIView对应的图层CALayer上面添加一个蓝色背景的子图层。
//CALayerDemo1-测试添加子图层
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *layerView;
@property (strong, nonatomic) CALayer *blueLayer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.blueLayer = [CALayer layer];
self.blueLayer.frame = CGRectMake(25.0f, 25.0f, 50.0f, 50.0f);
self.blueLayer.backgroundColor = [UIColor blueColor].CGColor;
[self.layerView.layer addSublayer:self.blueLayer];
}
@end
注意到外面的黄色UIView的origin为{50,50},大小为200*200,可以发现子图层的frame坐标是相对于其父图层坐标系而言的(用addSubview添加子视图的坐标也是一样),运行效果如下:
当然还可以设置CALayer的contents属性为一个Image来设置图片,设置contentGravity来指定图层内容的拉伸方式。更多属性可以参见:iOS核心动画部分章节
2 坐标系
2.1视图坐标系和图层坐标系
关于视图的坐标系,我在学习笔记一里面已经总结过,这里顺便一起看看视图和图层的坐标系。可以发现与视图相比,在图层中也有frame,bounds,不同的是,图层没有视图中的center,而是多了个position。当然我们可以发现,这两个值是一样的。这里我们看到的只是二维的坐标系,在后面我们会看到三维的坐标系。
2.2 锚点
center和position都指定了锚点(anchorPoint)相对于父图层坐标空间的位置,图层的锚点通过position来控制图层的位置,可以把锚点认为是移动图层的一个把柄。
如图3为锚点的示意图,锚点用单位坐标来表示,图层左上角为{0,0},中心为{0.5,0.5},这也是默认值,右下角为{1,1}。右图中将锚点设置到了{0,0},可以发现图层位置向右下发生了移动,注意,图层frame的值发生了变化,但是position的值并没有变化。这里可以用之前的例子来继续测试一下,加入如下代码在视图要出现的时候修改锚点的位置,可以发现打印出来的结果是符合我们预期的。
- (void)viewDidLoad {
......
NSLog(@"frame:%@, sublayer frame:%@, position:%@", NSStringFromCGRect(self.layerView.frame), NSStringFromCGRect(self.blueLayer.frame), NSStringFromCGPoint(self.layerView.layer.position));
//output: frame:{{50, 50}, {100, 100}}, sublayer frame:{{25, 25}, {50, 50}}, position:{100, 100}
}
- (void)viewWillAppear:(BOOL)animated {
self.layerView.layer.anchorPoint = CGPointMake(0.0, 0.0);
NSLog(@"frame:%@, sublayer frame:%@, position:%@", NSStringFromCGRect(self.layerView.frame), NSStringFromCGRect(self.blueLayer.frame), NSStringFromCGPoint(self.layerView.layer.position));
//output: frame:{{100, 100}, {100, 100}}, sublayer frame:{{25, 25}, {50, 50}}, position:{100, 100}
}
这里可能会有个疑惑,就是根据锚点如何计算frame的位置,计算公式如下,由于position是锚点在superLayer的位置坐标,是保持不变的,通过修改锚点的值可以导致图层的frame.origin发生变化,从而导致图层位置发生变化:
frame.origin.x = position.x - anchorPoint.x * bounds.size.width;
frame.origin.y = position.y - anchorPoint.y * bounds.size.height;
默认情况下,锚点为{0.5,0.5},因此position正好位于图层中心。当锚点改成{0,0}时,则此时由于position不变,可以看到我们上面例子的frame的origin变成了postion的值,也就是{100,100},运行效果如图4所示。锚点的用法有个很经典的闹钟例子,参见这篇文章。
2.3 三维坐标系
据说平面直角坐标系是笛卡尔在一次生病的时候发明的,而三维坐标系是后人在二维坐标系基础上发展而来。三维坐标系通常分为两种:左手坐标系和右手坐标系。iOS用的是左手坐标系(Mac用的是右手坐标系,我们这里不讨论)。可以通过左手定则(图6)来判断旋转的方向:使用左手握住拳头,拇指指向旋转轴的正方向,四指弯曲方向就是旋转的正方向。
我们知道iOS坐标中,原点位于左上角,X轴向右,Y轴向下为正方向。图7给出了iOS中三维坐标中各个轴旋转方向的示意图,通过左手定则比划一下应该就清楚了。
3 变换
在iOS的动画效果中,变换是很常见的,包括仿射变换和3D变换等。变换的终极原理就是矩阵的乘法运算,到这个时候终于发现以前本科学习矩阵的用处了。
3.1 仿射变换
通过设置UIView的transform属性可以实现图层的二维旋转,缩放以及平移,这一系列的变换归类为仿射变换,如图8所示就是多次复合变换,包括了旋转,缩放,平移。
UIView的transform是一个CGAffineTransform类型的实例,CGAffineTransform是一个可以和二维空间向量(如CGPoint)做乘法的3X3的矩阵。矩阵乘法如下:
注意到我们对CGPoint增加一列,对变换矩阵也增加了[0 0 1]那个第三列,多增加的一列主要是为了复合变换中的矩阵相乘,试想,如果我们不加第三列,那么两个 3*2的矩阵是不能相乘的。由上面的矩阵计算可以得到变换后的坐标值,如下:
因此我们可以发现,当变换矩阵为图11这样时,可以得到新的坐标值如图12所示,即完成了一次平移操作。
而当变换矩阵为图13这样时,则可以完成一次缩放操作,注意缩放的时候center保持不变。
同理,要完成旋转,则旋转的变换矩阵如下,相比前面的显而易见,旋转的稍微复杂一点,不过你可以画一个单位圆,然后通过旋转一个角度a,然后运用下正弦和余弦的几个定理就可以得到这个公式了。
同样的,还是用之前的那个实例,即把layerView先缩放,再旋转然后平移,viewDidLoad中增加代码如下,运行效果如图17所示。
......
//创建transform对象
CGAffineTransform transform = CGAffineTransformIdentity;
//缩放为原来大小的50%
transform = CGAffineTransformScale(transform, 0.5, 0.5);
//旋转30度
transform = CGAffineTransformRotate(transform, M_PI / 180.0 * 30.0);
//X方向平移200
transform = CGAffineTransformTranslate(transform, 200, 0);
//设置transform
self.layerView.layer.affineTransform = transform;
3.2 3D变换
在iOS中使用CATransform3D这个结构体来表示三维的齐次坐标变换矩阵。3D变换涉及到三维透视投影的一些原理知识,具体原理可以参见图形学的相关书籍,这里只是给出iOS里面的3D变换用法以及基本的结论。关于三维透视投影的一些介绍可以参见参考资料3,4。
CATransform3D结构体在iOS中的定义如下:
struct CATransform3D{
CGFloat m11, m12, m13, m14;
CGFloat m21, m22, m23, m24;
CGFloat m31, m32, m33, m34;
CGFloat m41, m42, m43, m44;
};
iOS的3D变换用的变换矩阵如下所示,注意到坐标是1X4的矩阵,而变换矩阵是4X4的矩阵,这里面的m34这个值是用来设置透视效果的。我们可以通过设置m34为-1.0 / d
来应用透视效果,d代表了想象中视角相机和屏幕之间的距离,以像素为单位。通过设置d的值可以达到近大远小的效果,也就是我们看到在iOS开发中以坐标轴旋转图层时,产生的3D效果。d越大,效果越不明显,d越小,效果越明显甚至导致失真。d的一个推荐的值是500-1000之间。
在例子里面加上3D旋转的代码如下,这里是沿Y轴旋转45度:
......
CATransform3D transform = CATransform3DIdentity;
transform.m34 = - 1.0 / 500.0;
transform = CATransform3DRotate(transform, M_PI_4, 0, 1, 0);
self.layerView.layer.transform = transform;
如图19就是沿Y轴旋转45度的得到的效果图。这里设置的d为500,我们可以发现3D效果还算明显且没有很夸张。旋转45度,靠近我们的边会变大而远离的边会缩小,这样从视觉上产生了3D效果。如果我们设置d为10,这样会发现3D效果会夸张到失真。而如果设置d为1000000会更大的值,会发现3D效果很不明显,iOS默认设置的d就是无穷大,因此如果不设置m34的值,我们旋转是没有3D效果的。
4 总结
iOS动画开发涉及内容很多,这里只是摘取了一些我目前了解的基础知识,后面会写一篇笔记来做一个动画的实例。对于3D透视投影这一块的理论没有细究,希望后面会有时间研究清楚并补充了。
5 参考资料
- iOS核心动画
- iOS的三维透视投影
- Mac、iOS中的三维坐标系
- 透视投影的原理和实现
- Core Animation Programming Guide