iOS動畫涉及圖形學的一些內容,已經忘記的差不多了,關于動畫的筆記準備分兩篇,第一篇總結動畫基礎,第二篇則是完成一個旋轉動畫的實例。初學iOS,沒有太多經驗,總結的若有錯漏,請各位指正。
1 圖層和視圖
在學習動畫之前,需要先明確幾個基本概念,首先是圖層和視圖。視圖是比較熟悉的了,最初學習的時候就會見到有UIViewController,然后控制器會對應一個UIView,這個UIView就是視圖。我們知道視圖是有層級關系的,從UIWindow->UIView->SubView等。而之前學習中一直沒有深究的是,其實每個UIView都有一個CALayer實例的圖層屬性layer。視圖(UIView)的職責就是創建和管理圖層(CALayer),視圖是對圖層的封裝,真正在iPhone屏幕上面顯示和做動畫的其實都是視圖所關聯的圖層。視圖和圖層的關系是一一對應的,如圖1所示為圖層樹結構,Window Layer, View Layer等分別對應視圖中的UIWindow,UIView等。
最初看到這里也很疑惑,為什么要多出來一層封裝呢?看了參考資料1才知道是為了提高復用性,因為蘋果公司除了iOS還有macOS,一個適用于iPhone,一個用于Mac,Mac基于鼠標和觸控板和iPhone基于多點觸控的交互很不相同,因此iPhone里面是UIView,Mac里面則是NSView,它們功能類似,但是實現并不同。可是對于繪圖,布局以及動畫等兩個系統其實有很多可以共用的地方,因此獨立出一個Core Animation框架(CALayer中的CA就是Core Animation的縮寫)用于復用。當然除了視圖層級和圖層樹這兩個層級,還有呈現樹和渲染樹,一共是四個,在動畫執行過程中我們要獲取圖層屬性的話要使用呈現樹presentationLayer,因為我們的圖層樹總是指向動畫結束的最終位置,無法捕獲動畫執行過程中的屬性值。圖2為視圖、圖層樹、呈現樹以及渲染樹的示意圖。
那么CALayer不能做什么,能做什么呢?下面總結一下:
CALayer不能做什么
- 既然是個獨立出來可復用的庫,那么CALayer是不能響應和處理觸控事件的。
CALayer能做什么
- 圖形陰影,邊框,圓角等。
- 仿射變換。
- 3D變換。
- 透明遮罩,多級非線性動畫...
那么既然CALayer可以做這些事情,我們寫個demo測試一下,在一個黃色的UIView對應的圖層CALayer上面添加一個藍色背景的子圖層。
//CALayerDemo1-測試添加子圖層
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *layerView;
@property (strong, nonatomic) CALayer *blueLayer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.blueLayer = [CALayer layer];
self.blueLayer.frame = CGRectMake(25.0f, 25.0f, 50.0f, 50.0f);
self.blueLayer.backgroundColor = [UIColor blueColor].CGColor;
[self.layerView.layer addSublayer:self.blueLayer];
}
@end
注意到外面的黃色UIView的origin為{50,50},大小為200*200,可以發現子圖層的frame坐標是相對于其父圖層坐標系而言的(用addSubview添加子視圖的坐標也是一樣),運行效果如下:
當然還可以設置CALayer的contents屬性為一個Image來設置圖片,設置contentGravity來指定圖層內容的拉伸方式。更多屬性可以參見:iOS核心動畫部分章節
2 坐標系
2.1視圖坐標系和圖層坐標系
關于視圖的坐標系,我在學習筆記一里面已經總結過,這里順便一起看看視圖和圖層的坐標系。可以發現與視圖相比,在圖層中也有frame,bounds,不同的是,圖層沒有視圖中的center,而是多了個position。當然我們可以發現,這兩個值是一樣的。這里我們看到的只是二維的坐標系,在后面我們會看到三維的坐標系。
2.2 錨點
center和position都指定了錨點(anchorPoint)相對于父圖層坐標空間的位置,圖層的錨點通過position來控制圖層的位置,可以把錨點認為是移動圖層的一個把柄。
如圖3為錨點的示意圖,錨點用單位坐標來表示,圖層左上角為{0,0},中心為{0.5,0.5},這也是默認值,右下角為{1,1}。右圖中將錨點設置到了{0,0},可以發現圖層位置向右下發生了移動,注意,圖層frame的值發生了變化,但是position的值并沒有變化。這里可以用之前的例子來繼續測試一下,加入如下代碼在視圖要出現的時候修改錨點的位置,可以發現打印出來的結果是符合我們預期的。
- (void)viewDidLoad {
......
NSLog(@"frame:%@, sublayer frame:%@, position:%@", NSStringFromCGRect(self.layerView.frame), NSStringFromCGRect(self.blueLayer.frame), NSStringFromCGPoint(self.layerView.layer.position));
//output: frame:{{50, 50}, {100, 100}}, sublayer frame:{{25, 25}, {50, 50}}, position:{100, 100}
}
- (void)viewWillAppear:(BOOL)animated {
self.layerView.layer.anchorPoint = CGPointMake(0.0, 0.0);
NSLog(@"frame:%@, sublayer frame:%@, position:%@", NSStringFromCGRect(self.layerView.frame), NSStringFromCGRect(self.blueLayer.frame), NSStringFromCGPoint(self.layerView.layer.position));
//output: frame:{{100, 100}, {100, 100}}, sublayer frame:{{25, 25}, {50, 50}}, position:{100, 100}
}
這里可能會有個疑惑,就是根據錨點如何計算frame的位置,計算公式如下,由于position是錨點在superLayer的位置坐標,是保持不變的,通過修改錨點的值可以導致圖層的frame.origin發生變化,從而導致圖層位置發生變化:
frame.origin.x = position.x - anchorPoint.x * bounds.size.width;
frame.origin.y = position.y - anchorPoint.y * bounds.size.height;
默認情況下,錨點為{0.5,0.5},因此position正好位于圖層中心。當錨點改成{0,0}時,則此時由于position不變,可以看到我們上面例子的frame的origin變成了postion的值,也就是{100,100},運行效果如圖4所示。錨點的用法有個很經典的鬧鐘例子,參見這篇文章。
2.3 三維坐標系
據說平面直角坐標系是笛卡爾在一次生病的時候發明的,而三維坐標系是后人在二維坐標系基礎上發展而來。三維坐標系通常分為兩種:左手坐標系和右手坐標系。iOS用的是左手坐標系(Mac用的是右手坐標系,我們這里不討論)。可以通過左手定則(圖6)來判斷旋轉的方向:使用左手握住拳頭,拇指指向旋轉軸的正方向,四指彎曲方向就是旋轉的正方向。
我們知道iOS坐標中,原點位于左上角,X軸向右,Y軸向下為正方向。圖7給出了iOS中三維坐標中各個軸旋轉方向的示意圖,通過左手定則比劃一下應該就清楚了。
3 變換
在iOS的動畫效果中,變換是很常見的,包括仿射變換和3D變換等。變換的終極原理就是矩陣的乘法運算,到這個時候終于發現以前本科學習矩陣的用處了。
3.1 仿射變換
通過設置UIView的transform屬性可以實現圖層的二維旋轉,縮放以及平移,這一系列的變換歸類為仿射變換,如圖8所示就是多次復合變換,包括了旋轉,縮放,平移。
UIView的transform是一個CGAffineTransform類型的實例,CGAffineTransform是一個可以和二維空間向量(如CGPoint)做乘法的3X3的矩陣。矩陣乘法如下:
注意到我們對CGPoint增加一列,對變換矩陣也增加了[0 0 1]那個第三列,多增加的一列主要是為了復合變換中的矩陣相乘,試想,如果我們不加第三列,那么兩個 3*2的矩陣是不能相乘的。由上面的矩陣計算可以得到變換后的坐標值,如下:
因此我們可以發現,當變換矩陣為圖11這樣時,可以得到新的坐標值如圖12所示,即完成了一次平移操作。
而當變換矩陣為圖13這樣時,則可以完成一次縮放操作,注意縮放的時候center保持不變。
同理,要完成旋轉,則旋轉的變換矩陣如下,相比前面的顯而易見,旋轉的稍微復雜一點,不過你可以畫一個單位圓,然后通過旋轉一個角度a,然后運用下正弦和余弦的幾個定理就可以得到這個公式了。
同樣的,還是用之前的那個實例,即把layerView先縮放,再旋轉然后平移,viewDidLoad中增加代碼如下,運行效果如圖17所示。
......
//創建transform對象
CGAffineTransform transform = CGAffineTransformIdentity;
//縮放為原來大小的50%
transform = CGAffineTransformScale(transform, 0.5, 0.5);
//旋轉30度
transform = CGAffineTransformRotate(transform, M_PI / 180.0 * 30.0);
//X方向平移200
transform = CGAffineTransformTranslate(transform, 200, 0);
//設置transform
self.layerView.layer.affineTransform = transform;
3.2 3D變換
在iOS中使用CATransform3D這個結構體來表示三維的齊次坐標變換矩陣。3D變換涉及到三維透視投影的一些原理知識,具體原理可以參見圖形學的相關書籍,這里只是給出iOS里面的3D變換用法以及基本的結論。關于三維透視投影的一些介紹可以參見參考資料3,4。
CATransform3D結構體在iOS中的定義如下:
struct CATransform3D{
CGFloat m11, m12, m13, m14;
CGFloat m21, m22, m23, m24;
CGFloat m31, m32, m33, m34;
CGFloat m41, m42, m43, m44;
};
iOS的3D變換用的變換矩陣如下所示,注意到坐標是1X4的矩陣,而變換矩陣是4X4的矩陣,這里面的m34這個值是用來設置透視效果的。我們可以通過設置m34為-1.0 / d
來應用透視效果,d代表了想象中視角相機和屏幕之間的距離,以像素為單位。通過設置d的值可以達到近大遠小的效果,也就是我們看到在iOS開發中以坐標軸旋轉圖層時,產生的3D效果。d越大,效果越不明顯,d越小,效果越明顯甚至導致失真。d的一個推薦的值是500-1000之間。
在例子里面加上3D旋轉的代碼如下,這里是沿Y軸旋轉45度:
......
CATransform3D transform = CATransform3DIdentity;
transform.m34 = - 1.0 / 500.0;
transform = CATransform3DRotate(transform, M_PI_4, 0, 1, 0);
self.layerView.layer.transform = transform;
如圖19就是沿Y軸旋轉45度的得到的效果圖。這里設置的d為500,我們可以發現3D效果還算明顯且沒有很夸張。旋轉45度,靠近我們的邊會變大而遠離的邊會縮小,這樣從視覺上產生了3D效果。如果我們設置d為10,這樣會發現3D效果會夸張到失真。而如果設置d為1000000會更大的值,會發現3D效果很不明顯,iOS默認設置的d就是無窮大,因此如果不設置m34的值,我們旋轉是沒有3D效果的。
4 總結
iOS動畫開發涉及內容很多,這里只是摘取了一些我目前了解的基礎知識,后面會寫一篇筆記來做一個動畫的實例。對于3D透視投影這一塊的理論沒有細究,希望后面會有時間研究清楚并補充了。