簡析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,如果想進一步了解,推薦學習蘋果官方文檔


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM