本文转载至 http://www.tuicool.com/articles/e2qaYjA
背景
随着达达业务的扩大,越来越多的人开始使用达达客户端,参加到众包物流的行业中。达达客户端分为iOS平台和安卓平台。
APP开发也从快速迭代的粗旷性开发转向高可复用,提升用户提现的精细化方向发展。iOS动画交互良好,使用广泛,良好的用户体验离不开流畅的界面变换。为此,达达iOS团队对动画实现以及背后原理做了学习探索。
目的
- 了解Core Animation 基本框架
- 理解图层的定义
- 解析动画的流程
- 掌握3D动画的原理
- 实现3D动画的代码实现
基本框架
Core Animation位于AppKit、UIKit下方,集成于View的Cocoa、Cocoa Touch中。当然Core Animation向view提供很多接口,好让开发者更好的控制动画。
图层的定义
- 开发者所有关于Core Animation的操作都是基于图层,图层对象是3D空间向后垂直投影的一个2D平面。
- 一个图层抓取由View提供的内容,并把这些内容缓存在一张位图中。
- 在硬件层面中对位图的操作远远快于在软件层面。
理解
-
垂直投影可以想象成,一个物体站在一面墙前,一个垂直于墙面的光源,照射物体,在墙上留下的阴影,由于是垂直光源,无论物体是远离墙面,还是靠近墙面,阴影的大小都不会发生变化。
-
iOS程序中每个View控件都有自己的Layer,View上的子控件,例如:Label,Image等,便是View向Layer提供的内容,而平移,旋转,缩放等参数,便是状态信息。
-
基于Layer的动画,Core Animation把layer保留的bitmap和状态信息传给图形硬件,图形硬件负责使用新的信息操作bitmap。基于View 绘制,是通过调用view的drawRect方法来使用新的参数重绘内容改变view。这种绘制代价很高,因为绘制在总线程消耗CPU完成工作。
动画的流程
动画的原理
下面内容将会涉及的知识,依次是数学坐标系,线性代数矩阵,物理成像原理,数学相似三角形,数学方程组。不用担心,我们会从基础入手,让理解更加高效。
坐标系
在图层平面坐标系中,使用两种坐标系。
- 基于点的坐标系
原点位于图层的左上角,向右为x轴的正方向,向下为y轴的正方向,一个点的x、y坐标以点为单位。
- 单位坐标系
原点位于图层的左上角,向右为x轴的正方向,向下为y轴的正方向,一个点的x、y坐标以相对x轴、y轴的比例为值,取值范围[0,1]。锚点(anchorPoint)使用单位坐标系, 如下图所示position根据锚点而变。
锚点的作用
锚点决定了动画在变化时,z轴的位置。如下图,由于锚点不同,图层绕z轴的旋转效果也一样。
矩阵
- 通过矩阵对图层位图进行平移、旋转、缩放变换。
- 矩阵相乘只有在第一个矩阵的列数(column)和第二个矩阵的行数(row)相同时才有意义。
- 图层的bitmap由点组成,每个点可以对应1×4矩阵,乘以一个4×4变换矩阵,得到一个1×4矩阵,即为变换后的结果。
矩阵乘法
- 矩阵C的行数等于矩阵A的行数,C的列数等于B的列数。
- 乘积C的第m行第n列的元素等于矩阵A的第m行的元素与矩阵B的第n列对应元素乘积之和。
思考?
1. 点坐标为什么要转换为1×4矩阵 2. 变换矩阵为什么必须是4×4矩阵 3. 如何实现移动,缩放,旋转
齐次矩阵
- 齐次坐标就是将一个原本是n维的向量用一个n+1维向量来表示。
- 使用1×4矩阵,是相对点的三维坐标进行齐次坐标。
齐次坐标变换 (x, y, z) -> (x × h, y × h, z × h, h) -> (xˊ, yˊ, zˊ, h)
齐次坐标还原 (xˊ, yˊ, zˊ, h) -> (x / h, y / h, z / h, 1) -> (x, y, z)
如果不使用1×4齐次矩阵和4×4变换矩阵?
只使用3×3变换矩阵:
m11, m12, m13 {x, y, z} * { m21, m22, m23 } = {x', y', z'} m31, m32, m33
xˊ=x × m11 + y × m21 + z × m31 在预先不对变量系数(m11, m21, m31)做其他计算的情况下,只能实现在各个坐标轴的缩放
但是使用使用1×4齐次矩阵和4×4变换矩阵后
xˊ= x × m11 + y × m21 + z × m31 + 1 × m41 m11=2 m21=0 m31=0 m41=8 可同时实现向x轴正方向放大2倍,在沿着x轴正方向平移8个单位
引入齐次坐标的目的主要是合并矩阵运算中的乘法和加法。
基本变换矩阵
- 矩阵就是利用矩阵内特殊位置的值,在做矩阵乘法时,达到对点坐标进行变换,下面时常用变换矩阵
3D动画效果
iOS中的CALayer的3D本质上并不能算真正的3D,而只是3D在二维平面上的投影,投影平面就是手机屏幕也就是xy轴组成的平面。
如此,只使用基本变换矩阵实现的平移、缩放、旋转,不会有近大远小的透视效果。
那该如何产生近大远小呢?
-
要达到近大远小目的,需要在系统做垂直投影前,先对图层做一次视点变换。如此垂直投影别是视点观察到的近大远小的物体。
-
Layer的z轴的位置则是通过anchorPoint来指定的,所谓的anchorPoint(锚点)就是在变换中保持不变的点,也就是某个Layer在变换中的原点,xyz三轴相交于此点。下图为锚点常用位置
- 在原点(0 , 0)沿着Y轴的正方向,得到如图坐标系, 首先在Z轴选择一个视点
- 添加两个child layer,观察区域便能看到两个child layer顶部的短线,绿色在前,红色在后,且长度相等
- 通过视点对顶部,作相对X轴的投影,得到视点投影
- 绿线、红线本来长度相等,通过视点投影后造成了“近大远小”的透视效果
所以只要在iOS垂直投影前,对layer作视点投影变换,就能得到透视效果
实践透视原理
- 使用上图的坐标系,红点为观察区域一点,对红点做视点投影,得到绿点,同时对红点做z轴的垂直线得到黑点。
- 使用相似三角形原理,得到如下公式
- 简化公式后,得到
方程1
,绿点x轴的值只于视点z轴值有关
- 对红点做h = 1的齐次坐标(6, 0, 5, 1),通过乘以一个矩阵,得到变换后的绿点的齐次矩阵
- 变换后的矩阵只与视点z轴值有关,所以只设置m34,对(6, 0, 5, 1 + 5r)还原得到
方程2
- 结合
方程1
和方程2
,最后得到
至此只要修改变换矩阵m34的值为视点z轴值,便能得到相应的视点投影变换矩阵
动画的代码实现
- 使用达达启动页面来实践以上部分内容。PS:为了查看简介,未对方法封装
-
属性申明
@property (weak, nonatomic) IBOutlet UIImageView *logoImg; //达达Logo @property (weak, nonatomic) IBOutlet UILabel *nameLab; // 达达 @property (weak, nonatomic) IBOutlet UILabel *desLab; // 可靠配送,在你身边
-
初始化设置,对两个Label设置透明度为0,缩小到原来的0.5倍
- (void)viewDidLoad { [super viewDidLoad]; self.nameLab.alpha = 0.f; self.nameLab.layer.transform = CATransform3DMakeScale(0.5f, 0.5f, 1.f); self.desLab.alpha = 0.f; self.desLab.layer.transform = CATransform3DMakeScale(0.5f, 0.5f, 1.f); }
-
动画设置,对Logo和Label的分开实现动画
- (void)viewDidAppear:(BOOL)animated { [self animationDaDaLabel]; [self animationDaDaLogo]; }
-
对Label的动画,使用UIView自带的block方式
- (void) animationDaDaLabel { [UIView animateWithDuration:0.5f animations:^{ // 放大并模糊 self.nameLab.alpha = 0.5f; self.nameLab.layer.transform = CATransform3DMakeScale(1.2f, 1.2f, 1.f); self.desLab.alpha = 0.5f; self.desLab.layer.transform = CATransform3DMakeScale(1.2f, 1.2f, 1.f); } completion:^(BOOL finished) { [UIView animateWithDuration:0.5f animations:^{ // 恢复并清晰 self.nameLab.alpha = 1.f; self.nameLab.layer.transform = CATransform3DMakeScale(1.f, 1.f, 1.f); self.desLab.alpha = 1.f; self.desLab.layer.transform = CATransform3DMakeScale(1.f, 1.f, 1.f); }]; }]; }
-
对Logo的动画,使用CABasicAnimation对象
- (void) animationDaDaLogo { CATransform3D transform = CATransform3DIdentity; transform.m34 = - 1 / 100.0f; // 设置视点在Z轴正方形z=100 // 动画结束时,在Z轴负方向60 CATransform3D startTransform = CATransform3DTranslate(transform, 0, 0, -60); // 动画结束时,绕Y轴逆时针旋转90度 CATransform3D firstTransform = CATransform3DRotate(startTransform, M_PI_2, 0, 1, 0); // 通过CABasicAnimation修改transform属性 CABasicAnimation *animation1 = [CABasicAnimation animationWithKeyPath:@"transform"]; // 向后移动同时绕Y轴逆时针旋转90度 animation1.fromValue = [NSValue valueWithCATransform3D:CATransform3DIdentity]; animation1.toValue = [NSValue valueWithCATransform3D:firstTransform]; // 虽然只有一个动画,但用Group只为以后好扩展 CAAnimationGroup *animationGroup = [CAAnimationGroup animation]; animationGroup.animations = [NSArray arrayWithObjects:animation1, nil]; animationGroup.duration = 0.5f; animationGroup.delegate = self; // 动画回调,在动画结束调用animationDidStop animationGroup.removedOnCompletion = NO; // 动画结束时停止,不回复原样 // 对logoImg的图层应用动画 [self.logoImg.layer addAnimation:animationGroup forKey:@"FristAnimation"]; }
-
实际上,只对Logo使用“一半动画”,Logo一边向后移动,一边逆时针绕Z轴旋转90度,在此动画结束后,通过回调补全剩下的“一半动画”。利用这两部分,实现,向后移动同时逆时针旋转,旋转到90度时,向前移动,同时继续逆时针旋转90度
- (void) animationDidStop:(CAAnimation *)anim finished:(BOOL)flag { if (flag) { if (anim == [self.logoImg.layer animationForKey:@"FristAnimation"]) { CATransform3D transform = CATransform3DIdentity; transform.m34 = - 1 / 100.0f; // 设置视点在Z轴正方形z=100 // 动画开始时,在Z轴负方向60 CATransform3D startTransform = CATransform3DTranslate(transform, 0, 0, -60); // 动画开始时,绕Y轴顺时针旋转90度 CATransform3D secondTransform = CATransform3DRotate(startTransform, -M_PI_2, 0, 1, 0); // 通过CABasicAnimation修改transform属性 CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform"]; // 向前移动同时绕Y轴逆时针旋转90度 animation.fromValue = [NSValue valueWithCATransform3D:secondTransform]; animation.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity]; animation.duration = 0.5f; // 对logoImg的图层应用动画 [self.logoImg.layer addAnimation:animation forKey:@"SecondAnimation"]; } } }
-
最终效果(PS:仅用于讲解)
小结
根据以上内容,总结以下Core Animation相关重点
- 理解图层意义,图层是动画的核心和载体
- 理解两种平面坐标系的用途,在做3D视点变换的时,要通过三维坐标系来协助思考
- 理解矩阵,齐次坐标的使用目的
- 如果对成像原理不了解,可以搜索相关资料
- 通过代码进一步实践
申明:本文的图片源于苹果CoreAnimation Programming Guide,如果想进一步了解,推荐学习苹果官方文档