iOS学习笔记(7)-动画基础

iOS动画涉及图形学的一些内容,已经忘记的差不多了,关于动画的笔记准备分两篇,第一篇总结动画基础,第二篇则是完成一个旋转动画的实例。初学iOS,没有太多经验,总结的若有错漏,请各位指正。

1 图层和视图

在学习动画之前,需要先明确几个基本概念,首先是图层和视图。视图是比较熟悉的了,最初学习的时候就会见到有UIViewController,然后控制器会对应一个UIView,这个UIView就是视图。我们知道视图是有层级关系的,从UIWindow->UIView->SubView等。而之前学习中一直没有深究的是,其实每个UIView都有一个CALayer实例的图层属性layer。视图(UIView)的职责就是创建和管理图层(CALayer),视图是对图层的封装,真正在iPhone屏幕上面显示和做动画的其实都是视图所关联的图层。视图和图层的关系是一一对应的,如图1所示为图层树结构,Window Layer, View Layer等分别对应视图中的UIWindow,UIView等。

图1 图层树结构图

最初看到这里也很疑惑,为什么要多出来一层封装呢?看了参考资料1才知道是为了提高复用性,因为苹果公司除了iOS还有macOS,一个适用于iPhone,一个用于Mac,Mac基于鼠标和触控板和iPhone基于多点触控的交互很不相同,因此iPhone里面是UIView,Mac里面则是NSView,它们功能类似,但是实现并不同。可是对于绘图,布局以及动画等两个系统其实有很多可以共用的地方,因此独立出一个Core Animation框架(CALayer中的CA就是Core Animation的缩写)用于复用。当然除了视图层级和图层树这两个层级,还有呈现树和渲染树,一共是四个,在动画执行过程中我们要获取图层属性的话要使用呈现树presentationLayer,因为我们的图层树总是指向动画结束的最终位置,无法捕获动画执行过程中的属性值。图2为视图、图层树、呈现树以及渲染树的示意图。

图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添加子视图的坐标也是一样),运行效果如下:

图1 添加子图层

当然还可以设置CALayer的contents属性为一个Image来设置图片,设置contentGravity来指定图层内容的拉伸方式。更多属性可以参见:iOS核心动画部分章节

2 坐标系

2.1视图坐标系和图层坐标系

关于视图的坐标系,我在学习笔记一里面已经总结过,这里顺便一起看看视图和图层的坐标系。可以发现与视图相比,在图层中也有frame,bounds,不同的是,图层没有视图中的center,而是多了个position。当然我们可以发现,这两个值是一样的。这里我们看到的只是二维的坐标系,在后面我们会看到三维的坐标系。

图2 视图和图层的坐标系

2.2 锚点

center和position都指定了锚点(anchorPoint)相对于父图层坐标空间的位置,图层的锚点通过position来控制图层的位置,可以把锚点认为是移动图层的一个把柄。

图3 锚点

如图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所示。锚点的用法有个很经典的闹钟例子,参见这篇文章

图4 设置锚点为{0,0}

2.3 三维坐标系

据说平面直角坐标系是笛卡尔在一次生病的时候发明的,而三维坐标系是后人在二维坐标系基础上发展而来。三维坐标系通常分为两种:左手坐标系和右手坐标系。iOS用的是左手坐标系(Mac用的是右手坐标系,我们这里不讨论)。可以通过左手定则(图6)来判断旋转的方向:使用左手握住拳头,拇指指向旋转轴的正方向,四指弯曲方向就是旋转的正方向。

图5 三维坐标系
图6 左手定则

我们知道iOS坐标中,原点位于左上角,X轴向右,Y轴向下为正方向。图7给出了iOS中三维坐标中各个轴旋转方向的示意图,通过左手定则比划一下应该就清楚了。

图7 iOS三个轴的旋转方向示意图

3 变换

在iOS的动画效果中,变换是很常见的,包括仿射变换和3D变换等。变换的终极原理就是矩阵的乘法运算,到这个时候终于发现以前本科学习矩阵的用处了。

3.1 仿射变换

通过设置UIView的transform属性可以实现图层的二维旋转,缩放以及平移,这一系列的变换归类为仿射变换,如图8所示就是多次复合变换,包括了旋转,缩放,平移。

图8 仿射变换实例

UIView的transform是一个CGAffineTransform类型的实例,CGAffineTransform是一个可以和二维空间向量(如CGPoint)做乘法的3X3的矩阵。矩阵乘法如下:

图9 仿射变换矩阵

注意到我们对CGPoint增加一列,对变换矩阵也增加了[0 0 1]那个第三列,多增加的一列主要是为了复合变换中的矩阵相乘,试想,如果我们不加第三列,那么两个 3*2的矩阵是不能相乘的。由上面的矩阵计算可以得到变换后的坐标值,如下:

图10 矩阵计算

因此我们可以发现,当变换矩阵为图11这样时,可以得到新的坐标值如图12所示,即完成了一次平移操作。

图11 平移的变换矩阵
图12 平移的坐标计算

而当变换矩阵为图13这样时,则可以完成一次缩放操作,注意缩放的时候center保持不变

图13 缩放矩阵
图14 缩放的坐标计算

同理,要完成旋转,则旋转的变换矩阵如下,相比前面的显而易见,旋转的稍微复杂一点,不过你可以画一个单位圆,然后通过旋转一个角度a,然后运用下正弦和余弦的几个定理就可以得到这个公式了。

图15 旋转矩阵
图16 旋转的坐标计算

同样的,还是用之前的那个实例,即把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;
图17 复合变换效果图

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之间。

图18 一个3D像素点CATransform3D矩阵变换

在例子里面加上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效果的。

图19 3D变换效果图 d=500
图20 3D变换效果图 d=50

4 总结

iOS动画开发涉及内容很多,这里只是摘取了一些我目前了解的基础知识,后面会写一篇笔记来做一个动画的实例。对于3D透视投影这一块的理论没有细究,希望后面会有时间研究清楚并补充了。

5 参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351

推荐阅读更多精彩内容