简析iOS动画原理及实现——Core Animation

本文转载至 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,如果想进一步了解,推荐学习苹果官方文档

posted @ 2016-06-23 20:47  天牛  阅读(2462)  评论(0编辑  收藏  举报