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