本文转载至 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提供很多接口,好让开发者更好的控制动画。

图层的定义

  1. 开发者所有关于Core Animation的操作都是基于图层,图层对象是3D空间向后垂直投影的一个2D平面。
  2. 一个图层抓取由View提供的内容,并把这些内容缓存在一张位图中。
  3. 在硬件层面中对位图的操作远远快于在软件层面。

理解

  1. 垂直投影可以想象成,一个物体站在一面墙前,一个垂直于墙面的光源,照射物体,在墙上留下的阴影,由于是垂直光源,无论物体是远离墙面,还是靠近墙面,阴影的大小都不会发生变化。

  2. iOS程序中每个View控件都有自己的Layer,View上的子控件,例如:Label,Image等,便是View向Layer提供的内容,而平移,旋转,缩放等参数,便是状态信息。

  3. 基于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. 通过矩阵对图层位图进行平移、旋转、缩放变换。
  2. 矩阵相乘只有在第一个矩阵的列数(column)和第二个矩阵的行数(row)相同时才有意义。
  3. 图层的bitmap由点组成,每个点可以对应1×4矩阵,乘以一个4×4变换矩阵,得到一个1×4矩阵,即为变换后的结果。

矩阵乘法

  1. 矩阵C的行数等于矩阵A的行数,C的列数等于B的列数。
  2. 乘积C的第m行第n列的元素等于矩阵A的第m行的元素与矩阵B的第n列对应元素乘积之和。

思考?

1. 点坐标为什么要转换为1×4矩阵
2. 变换矩阵为什么必须是4×4矩阵
3. 如何实现移动,缩放,旋转

齐次矩阵

  1. 齐次坐标就是将一个原本是n维的向量用一个n+1维向量来表示。
  2. 使用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,如果想进一步了解,推荐学习苹果官方文档

简析iOS动画原理及实现——Core Animation的更多相关文章

  1. iOS动画-从UIView到Core Animation

    首先,介绍一下UIView相关的动画. UIView普通动画: [UIView beginAnimations: context:]; [UIView commitAnimations]; 动画属性设 ...

  2. iOS动画原理

    1. iOS动画原理 本质:动画对象(这里是UIView)的状态,基于时间变化的反应 分类:可以分为显式动画(关键帧动画和逐帧动画)和隐式动画 关键帧和逐帧总结:关键帧动画的实现方式,只需要修改某个属 ...

  3. 解析 iOS 动画原理与实现

    这篇文章不会教大家如何实现一个具体的动画效果,我会从动画的本质出发,来说说 iOS 动画的原理与实现方式. 什么是动画 动画,顾名思义,就是能“动”的画.人的眼睛对图像有短暂的记忆效应,所以当眼睛看到 ...

  4. iOS开发基础知识:Core Animation(核心动画)

    Core Animation,中文翻译为核心动画,它是一组非常强大的动画处理API,使用它能做出非常炫丽的动画效果,而且往往是事半功倍.也就是说,使用少量的代码就可以实现非常强大的功能. Core A ...

  5. [转]简析 IOS 程序图标的设计

    表现形态**** 在有限的空间里表达出相对应的信息,在IOS 程序图标设计中,直观是第一个解决的问题,不应该出现大多繁琐的修饰,当然还要有很好的视觉表现力,使用户可以更容易理解此应用的实际作用,更轻松 ...

  6. java Spring系列之 配置文件的操作 +Bean的生命周期+不同数据类型的注入简析+注入的原理详解+配置文件中不同标签体的使用方式

    Spring系列之 配置文件的操作 写在文章前面: 本文带大家掌握Spring配置文件的基础操作以及带领大家理清依赖注入的概念,本文涉及内容广泛,如果各位读者耐心看完,应该会对自身有一个提升 Spri ...

  7. IOS动画(Core Animation)总结 (参考多方文章)

    一.简介 iOS 动画主要是指Core Animation框架.官方使用文档地址为:Core Animation Guide. Core Animation是IOS和OS X平台上负责图形渲染与动画的 ...

  8. iOS动画效果和实现

    动画效果提供了状态或页面转换时流畅的用户体验,在iOS系统中,咱们不需要自己编写绘制动画的代码,Core Animation提供了丰富的api来实现你需要的动画效果. UIKit只用UIView来展示 ...

  9. 【转】IOS动画的实现,其实很简单

    动画效果提供了状态或页面转换时流畅的用户体验,在iOS系统中,咱们不需要自己编写绘制动画的代码,Core Animation提供了丰富的api来实现你需要的动画效果.UIKit只用UIView来展示动 ...

随机推荐

  1. python 查看目录下所有目录和文件

    python查看目录下所有的子目录和子文件 python递归遍历目录结构 我喜欢第一种 方法1 import json, os def list_dir(path, res): for i in os ...

  2. (笔记)linux 进程和线程的区别

    进程:进程是程序执行时的一个实例,即它是程序已经执行到课中程度的数据结构的汇集.从内核的观点看,进程的目的就是担当分配系统资源(CPU时间.内存等)的基本单位. 线程:线程是进程的一个执行流,是CPU ...

  3. SQL数据查询之——单表查询

    一.SQL数据查询的一般格式 数据查询是数据库的核心操作.SQL提供了SELECT语句进行数据查询,其一般格式为: SELECT [ALL | DISTINCT]<目标列表达式>[,< ...

  4. 第三百二十九节,web爬虫讲解2—urllib库爬虫—ip代理—用户代理和ip代理结合应用

    第三百二十九节,web爬虫讲解2—urllib库爬虫—ip代理 使用IP代理 ProxyHandler()格式化IP,第一个参数,请求目标可能是http或者https,对应设置build_opener ...

  5. 目标跟踪之meanshift---meanshift2

    均值漂移,可以对非刚性物理进行跟踪,是分参数估计,过程是迭代的过程,对光和形态不敏感,缺点是检测目标是固定的,特征不较少,模板背景没有实时更新,没有目标的位置精度预测只是梯度浓聚, 原理: 用文字标书 ...

  6. 多媒体开发之h264中的sps---sps信息提取之帧率

    ------------------------------author:pkf -----------------------------------------time:2015-8-20 --- ...

  7. Oracle表明明存在SQL查询数据提示表不存在异常

    今天同事遇到一个很奇怪的问题,恢复了一个数据库,表明明存在,用PLSQL和sqlplus都试过了,SQL语句select * from 表名,查询数据,却提示表名不存在异常 然而,使用select * ...

  8. LVS 实现负载均衡原理及安装配置详解

    负载均衡集群是 load balance 集群的简写,翻译成中文就是负载均衡集群.常用的负载均衡开源软件有nginx.lvs.haproxy,商业的硬件负载均衡设备F5.Netscale.这里主要是学 ...

  9. APP投资 历史 十万到 十亿元的项目

    马云又投了课程表APP 1亿元.   还能输入106字 http://www.tuicool.com/articles/ARVN3qI#0-qzone-1-41007-d020d2d2a4e8d1a3 ...

  10. JS检查当图片不存在时显示默认图片和键盘大小写键状态

    当图片不存在时显示默认图片 <script type="text/javascript"> var imgs = document.images; for(var i ...