随着达达业务的扩大,越来越多的人开始使用达达客户端,参加到众包物流的行业中。达达客户端分为iOS平台和安卓平台。
APP开发也从快速迭代的粗旷性开发转向高可复用,提升用户提现的精细化方向发展。iOS动画交互良好,使用广泛,良好的用户体验离不开流畅的界面变换。为此,达达iOS团队对动画实现以及背后原理做了学习探索。
Core Animation位于AppKit、UIKit下方,集成于View的Cocoa、Cocoa Touch中。当然Core Animation向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轴的旋转效果也一样。
1. 点坐标为什么要转换为1×4矩阵
2. 变换矩阵为什么必须是4×4矩阵
3. 如何实现移动,缩放,旋转
齐次坐标变换 (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)
只使用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个单位
iOS中的CALayer的3D本质上并不能算真正的3D,而只是3D在二维平面上的投影,投影平面就是手机屏幕也就是xy轴组成的平面。
如此,只使用基本变换矩阵实现的平移、缩放、旋转,不会有近大远小的透视效果。
要达到近大远小目的,需要在系统做垂直投影前,先对图层做一次视点变换。如此垂直投影别是视点观察到的近大远小的物体。
Layer的z轴的位置则是通过anchorPoint来指定的,所谓的anchorPoint(锚点)就是在变换中保持不变的点,也就是某个Layer在变换中的原点,xyz三轴相交于此点。下图为锚点常用位置
方程1
,绿点x轴的值只于视点z轴值有关
方程2
方程1
和 方程2
,最后得到
属性申明
@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相关重点
申明:本文的图片源于苹果CoreAnimation Programming Guide,如果想进一步了解,推荐学习苹果官方文档
更多关于达达技术的文章,敬请关注达达技术公众号。