iOS動(dòng)畫涉及圖形學(xué)的一些內(nèi)容汹族,已經(jīng)忘記的差不多了萧求,關(guān)于動(dòng)畫的筆記準(zhǔn)備分兩篇,第一篇總結(jié)動(dòng)畫基礎(chǔ)顶瞒,第二篇?jiǎng)t是完成一個(gè)旋轉(zhuǎn)動(dòng)畫的實(shí)例夸政。初學(xué)iOS,沒(méi)有太多經(jīng)驗(yàn)榴徐,總結(jié)的若有錯(cuò)漏守问,請(qǐng)各位指正。
1 圖層和視圖
在學(xué)習(xí)動(dòng)畫之前坑资,需要先明確幾個(gè)基本概念耗帕,首先是圖層和視圖袱贮。視圖是比較熟悉的了仿便,最初學(xué)習(xí)的時(shí)候就會(huì)見(jiàn)到有UIViewController,然后控制器會(huì)對(duì)應(yīng)一個(gè)UIView攒巍,這個(gè)UIView就是視圖嗽仪。我們知道視圖是有層級(jí)關(guān)系的,從UIWindow->UIView->SubView等柒莉。而之前學(xué)習(xí)中一直沒(méi)有深究的是闻坚,其實(shí)每個(gè)UIView都有一個(gè)CALayer實(shí)例的圖層屬性layer。視圖(UIView)的職責(zé)就是創(chuàng)建和管理圖層(CALayer)兢孝,視圖是對(duì)圖層的封裝窿凤,真正在iPhone屏幕上面顯示和做動(dòng)畫的其實(shí)都是視圖所關(guān)聯(lián)的圖層搀擂。視圖和圖層的關(guān)系是一一對(duì)應(yīng)的,如圖1所示為圖層樹結(jié)構(gòu)卷玉,Window Layer, View Layer等分別對(duì)應(yīng)視圖中的UIWindow哨颂,UIView等。
最初看到這里也很疑惑相种,為什么要多出來(lái)一層封裝呢威恼?看了參考資料1才知道是為了提高復(fù)用性,因?yàn)樘O果公司除了iOS還有macOS寝并,一個(gè)適用于iPhone箫措,一個(gè)用于Mac,Mac基于鼠標(biāo)和觸控板和iPhone基于多點(diǎn)觸控的交互很不相同衬潦,因此iPhone里面是UIView斤蔓,Mac里面則是NSView,它們功能類似镀岛,但是實(shí)現(xiàn)并不同弦牡。可是對(duì)于繪圖漂羊,布局以及動(dòng)畫等兩個(gè)系統(tǒng)其實(shí)有很多可以共用的地方驾锰,因此獨(dú)立出一個(gè)Core Animation框架(CALayer中的CA就是Core Animation的縮寫)用于復(fù)用。當(dāng)然除了視圖層級(jí)和圖層樹這兩個(gè)層級(jí)走越,還有呈現(xiàn)樹和渲染樹椭豫,一共是四個(gè),在動(dòng)畫執(zhí)行過(guò)程中我們要獲取圖層屬性的話要使用呈現(xiàn)樹presentationLayer旨指,因?yàn)槲覀兊膱D層樹總是指向動(dòng)畫結(jié)束的最終位置赏酥,無(wú)法捕獲動(dòng)畫執(zhí)行過(guò)程中的屬性值。圖2為視圖谆构、圖層樹裸扶、呈現(xiàn)樹以及渲染樹的示意圖。
那么CALayer不能做什么姓言,能做什么呢瞬项?下面總結(jié)一下:
CALayer不能做什么
- 既然是個(gè)獨(dú)立出來(lái)可復(fù)用的庫(kù)蔗蹋,那么CALayer是不能響應(yīng)和處理觸控事件的。
CALayer能做什么
- 圖形陰影囱淋,邊框猪杭,圓角等。
- 仿射變換妥衣。
- 3D變換皂吮。
- 透明遮罩戒傻,多級(jí)非線性動(dòng)畫...
那么既然CALayer可以做這些事情,我們寫個(gè)demo測(cè)試一下蜂筹,在一個(gè)黃色的UIView對(duì)應(yīng)的圖層CALayer上面添加一個(gè)藍(lán)色背景的子圖層需纳。
//CALayerDemo1-測(cè)試添加子圖層
@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,可以發(fā)現(xiàn)子圖層的frame坐標(biāo)是相對(duì)于其父圖層坐標(biāo)系而言的(用addSubview添加子視圖的坐標(biāo)也是一樣)艺挪,運(yùn)行效果如下:
當(dāng)然還可以設(shè)置CALayer的contents屬性為一個(gè)Image來(lái)設(shè)置圖片不翩,設(shè)置contentGravity來(lái)指定圖層內(nèi)容的拉伸方式。更多屬性可以參見(jiàn):iOS核心動(dòng)畫部分章節(jié)
2 坐標(biāo)系
2.1視圖坐標(biāo)系和圖層坐標(biāo)系
關(guān)于視圖的坐標(biāo)系麻裳,我在學(xué)習(xí)筆記一里面已經(jīng)總結(jié)過(guò)口蝠,這里順便一起看看視圖和圖層的坐標(biāo)系〗蚩樱可以發(fā)現(xiàn)與視圖相比妙蔗,在圖層中也有frame与帆,bounds夹孔,不同的是,圖層沒(méi)有視圖中的center猎提,而是多了個(gè)position穆役。當(dāng)然我們可以發(fā)現(xiàn)禁漓,這兩個(gè)值是一樣的。這里我們看到的只是二維的坐標(biāo)系孵睬,在后面我們會(huì)看到三維的坐標(biāo)系播歼。
2.2 錨點(diǎn)
center和position都指定了錨點(diǎn)(anchorPoint)相對(duì)于父圖層坐標(biāo)空間的位置,圖層的錨點(diǎn)通過(guò)position來(lái)控制圖層的位置掰读,可以把錨點(diǎn)認(rèn)為是移動(dòng)圖層的一個(gè)把柄秘狞。
如圖3為錨點(diǎn)的示意圖,錨點(diǎn)用單位坐標(biāo)來(lái)表示蹈集,圖層左上角為{0,0},中心為{0.5,0.5}烁试,這也是默認(rèn)值,右下角為{1,1}拢肆。右圖中將錨點(diǎn)設(shè)置到了{(lán)0,0}减响,可以發(fā)現(xiàn)圖層位置向右下發(fā)生了移動(dòng),注意郭怪,圖層frame的值發(fā)生了變化支示,但是position的值并沒(méi)有變化。這里可以用之前的例子來(lái)繼續(xù)測(cè)試一下鄙才,加入如下代碼在視圖要出現(xiàn)的時(shí)候修改錨點(diǎn)的位置颂鸿,可以發(fā)現(xiàn)打印出來(lái)的結(jié)果是符合我們預(yù)期的。
- (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}
}
這里可能會(huì)有個(gè)疑惑攒庵,就是根據(jù)錨點(diǎn)如何計(jì)算frame的位置嘴纺,計(jì)算公式如下败晴,由于position是錨點(diǎn)在superLayer的位置坐標(biāo),是保持不變的栽渴,通過(guò)修改錨點(diǎn)的值可以導(dǎo)致圖層的frame.origin發(fā)生變化尖坤,從而導(dǎo)致圖層位置發(fā)生變化:
frame.origin.x = position.x - anchorPoint.x * bounds.size.width;
frame.origin.y = position.y - anchorPoint.y * bounds.size.height闲擦;
默認(rèn)情況下糖驴,錨點(diǎn)為{0.5,0.5},因此position正好位于圖層中心佛致。當(dāng)錨點(diǎn)改成{0,0}時(shí)贮缕,則此時(shí)由于position不變,可以看到我們上面例子的frame的origin變成了postion的值俺榆,也就是{100,100}感昼,運(yùn)行效果如圖4所示。錨點(diǎn)的用法有個(gè)很經(jīng)典的鬧鐘例子罐脊,參見(jiàn)這篇文章定嗓。
2.3 三維坐標(biāo)系
據(jù)說(shuō)平面直角坐標(biāo)系是笛卡爾在一次生病的時(shí)候發(fā)明的,而三維坐標(biāo)系是后人在二維坐標(biāo)系基礎(chǔ)上發(fā)展而來(lái)萍桌。三維坐標(biāo)系通常分為兩種:左手坐標(biāo)系和右手坐標(biāo)系宵溅。iOS用的是左手坐標(biāo)系(Mac用的是右手坐標(biāo)系,我們這里不討論)上炎∈崖撸可以通過(guò)左手定則(圖6)來(lái)判斷旋轉(zhuǎn)的方向:使用左手握住拳頭,拇指指向旋轉(zhuǎn)軸的正方向藕施,四指彎曲方向就是旋轉(zhuǎn)的正方向寇损。
我們知道iOS坐標(biāo)中,原點(diǎn)位于左上角裳食,X軸向右矛市,Y軸向下為正方向。圖7給出了iOS中三維坐標(biāo)中各個(gè)軸旋轉(zhuǎn)方向的示意圖诲祸,通過(guò)左手定則比劃一下應(yīng)該就清楚了浊吏。
3 變換
在iOS的動(dòng)畫效果中,變換是很常見(jiàn)的救氯,包括仿射變換和3D變換等找田。變換的終極原理就是矩陣的乘法運(yùn)算,到這個(gè)時(shí)候終于發(fā)現(xiàn)以前本科學(xué)習(xí)矩陣的用處了径密。
3.1 仿射變換
通過(guò)設(shè)置UIView的transform屬性可以實(shí)現(xiàn)圖層的二維旋轉(zhuǎn)午阵,縮放以及平移躺孝,這一系列的變換歸類為仿射變換享扔,如圖8所示就是多次復(fù)合變換底桂,包括了旋轉(zhuǎn),縮放惧眠,平移籽懦。
UIView的transform是一個(gè)CGAffineTransform類型的實(shí)例,CGAffineTransform是一個(gè)可以和二維空間向量(如CGPoint)做乘法的3X3的矩陣氛魁。矩陣乘法如下:
注意到我們對(duì)CGPoint增加一列暮顺,對(duì)變換矩陣也增加了[0 0 1]那個(gè)第三列,多增加的一列主要是為了復(fù)合變換中的矩陣相乘秀存,試想捶码,如果我們不加第三列,那么兩個(gè) 3*2的矩陣是不能相乘的或链。由上面的矩陣計(jì)算可以得到變換后的坐標(biāo)值惫恼,如下:
因此我們可以發(fā)現(xiàn),當(dāng)變換矩陣為圖11這樣時(shí)澳盐,可以得到新的坐標(biāo)值如圖12所示祈纯,即完成了一次平移操作。
而當(dāng)變換矩陣為圖13這樣時(shí)叼耙,則可以完成一次縮放操作腕窥,注意縮放的時(shí)候center保持不變。
同理筛婉,要完成旋轉(zhuǎn)簇爆,則旋轉(zhuǎn)的變換矩陣如下,相比前面的顯而易見(jiàn)爽撒,旋轉(zhuǎn)的稍微復(fù)雜一點(diǎn)冕碟,不過(guò)你可以畫一個(gè)單位圓,然后通過(guò)旋轉(zhuǎn)一個(gè)角度a匆浙,然后運(yùn)用下正弦和余弦的幾個(gè)定理就可以得到這個(gè)公式了安寺。
同樣的,還是用之前的那個(gè)實(shí)例首尼,即把layerView先縮放挑庶,再旋轉(zhuǎn)然后平移,viewDidLoad中增加代碼如下软能,運(yùn)行效果如圖17所示迎捺。
......
//創(chuàng)建transform對(duì)象
CGAffineTransform transform = CGAffineTransformIdentity;
//縮放為原來(lái)大小的50%
transform = CGAffineTransformScale(transform, 0.5, 0.5);
//旋轉(zhuǎn)30度
transform = CGAffineTransformRotate(transform, M_PI / 180.0 * 30.0);
//X方向平移200
transform = CGAffineTransformTranslate(transform, 200, 0);
//設(shè)置transform
self.layerView.layer.affineTransform = transform;
3.2 3D變換
在iOS中使用CATransform3D這個(gè)結(jié)構(gòu)體來(lái)表示三維的齊次坐標(biāo)變換矩陣。3D變換涉及到三維透視投影的一些原理知識(shí)查排,具體原理可以參見(jiàn)圖形學(xué)的相關(guān)書籍凳枝,這里只是給出iOS里面的3D變換用法以及基本的結(jié)論。關(guān)于三維透視投影的一些介紹可以參見(jiàn)參考資料3,4岖瑰。
CATransform3D結(jié)構(gòu)體在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變換用的變換矩陣如下所示叛买,注意到坐標(biāo)是1X4的矩陣,而變換矩陣是4X4的矩陣蹋订,這里面的m34這個(gè)值是用來(lái)設(shè)置透視效果的率挣。我們可以通過(guò)設(shè)置m34為-1.0 / d
來(lái)應(yīng)用透視效果,d代表了想象中視角相機(jī)和屏幕之間的距離露戒,以像素為單位椒功。通過(guò)設(shè)置d的值可以達(dá)到近大遠(yuǎn)小的效果,也就是我們看到在iOS開發(fā)中以坐標(biāo)軸旋轉(zhuǎn)圖層時(shí)智什,產(chǎn)生的3D效果动漾。d越大,效果越不明顯荠锭,d越小谦炬,效果越明顯甚至導(dǎo)致失真。d的一個(gè)推薦的值是500-1000之間节沦。
在例子里面加上3D旋轉(zhuǎn)的代碼如下键思,這里是沿Y軸旋轉(zhuǎn)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軸旋轉(zhuǎn)45度的得到的效果圖。這里設(shè)置的d為500甫贯,我們可以發(fā)現(xiàn)3D效果還算明顯且沒(méi)有很夸張吼鳞。旋轉(zhuǎn)45度,靠近我們的邊會(huì)變大而遠(yuǎn)離的邊會(huì)縮小叫搁,這樣從視覺(jué)上產(chǎn)生了3D效果赔桌。如果我們?cè)O(shè)置d為10,這樣會(huì)發(fā)現(xiàn)3D效果會(huì)夸張到失真渴逻。而如果設(shè)置d為1000000會(huì)更大的值疾党,會(huì)發(fā)現(xiàn)3D效果很不明顯,iOS默認(rèn)設(shè)置的d就是無(wú)窮大惨奕,因此如果不設(shè)置m34的值雪位,我們旋轉(zhuǎn)是沒(méi)有3D效果的。
4 總結(jié)
iOS動(dòng)畫開發(fā)涉及內(nèi)容很多梨撞,這里只是摘取了一些我目前了解的基礎(chǔ)知識(shí)雹洗,后面會(huì)寫一篇筆記來(lái)做一個(gè)動(dòng)畫的實(shí)例。對(duì)于3D透視投影這一塊的理論沒(méi)有細(xì)究卧波,希望后面會(huì)有時(shí)間研究清楚并補(bǔ)充了时肿。