這篇文章不會教大家如何實(shí)現(xiàn)一個(gè)具體的動畫效果暖呕,我會從動畫的本質(zhì)出發(fā)挂签,來說說 iOS 動畫的原理與實(shí)現(xiàn)方式。
什么是動畫
動畫凯力,顧名思義茵瘾,就是能“動”的畫。
人的眼睛對圖像有短暫的記憶效應(yīng)咐鹤,所以當(dāng)眼睛看到多張圖片連續(xù)快速的切換時(shí)拗秘,就會被認(rèn)為是一段連續(xù)播放的動畫了。
比如祈惶,中國古代的“走馬燈”雕旨,就是用的這個(gè)原理扮匠。
有些人還會在一個(gè)本子每頁上手繪一些漫畫,當(dāng)快速翻頁的時(shí)候凡涩,也會看到動畫的效果棒搜,比如:
計(jì)算機(jī)動畫的實(shí)現(xiàn)方式
動畫是由一張張圖片組成的,在計(jì)算機(jī)中活箕,我們稱每一張圖片為 一幀畫面 力麸。
如果我們想實(shí)現(xiàn)這么一個(gè)動畫:一個(gè)水杯放在桌子的左邊,移動到右邊育韩,那么我們實(shí)際操作的克蚂,只是水杯。
所以動畫的實(shí)現(xiàn)筋讨,只是對運(yùn)動變化了的部分的處理陨舱。
逐幀 與 關(guān)鍵幀
類似于上面提到的手繪翻頁方式,我們可以將這個(gè)水杯在每幀畫面中的位置一一找出來版仔,這樣實(shí)現(xiàn)動畫的方式就叫作 逐幀動畫游盲,我們需要處理動畫中的每一幀。
我們一般在計(jì)算機(jī)上用 FPS ( Frames Per Second) 蛮粮,即 每秒的幀數(shù) 來表示動畫的刷新速度益缎,基于屏幕的刷新率等其他原因,在計(jì)算機(jī)上一般采用 60 FPS然想。
如果運(yùn)動變化幅度較緩莺奔,減半到 30 FPS 時(shí),我們?nèi)庋垡彩强山邮艿摹?br> 較低的 FPS 會讓我們有“卡頓”的感覺变泄。
逐幀動畫是最直接的令哟,但要處理的幀數(shù)太多,所以實(shí)現(xiàn)過程是會麻煩妨蛹。
計(jì)算機(jī)的工作就是來完成重復(fù)單調(diào)的工作的屏富,所以,有些工作是可以考慮讓計(jì)算機(jī)來完成的蛙卤。
上面的例子狠半,可以變成一個(gè)涉及數(shù)學(xué)和物理的問題:一個(gè)杯子初始位置在左邊,n秒后勻速運(yùn)動到右邊颤难,那么在每 1/60 秒的時(shí)候神年,這個(gè)杯子的位置顯然是可以計(jì)算出來的了。
所以行嗤,我們其實(shí)只需要指定一些 關(guān)鍵 信息就能讓計(jì)算機(jī)自己計(jì)算出每一幀杯子的位置了:
- 起始位置已日,比如一個(gè)坐標(biāo) (0,0)
- 結(jié)束位置,再比如一個(gè)坐標(biāo) (100,0)
- 動畫總時(shí)間栅屏,比如 0.25 秒
- 勻速運(yùn)動
這種方式就稱之為 關(guān)鍵幀動畫飘千。即我們只需要給定幾個(gè)關(guān)鍵幀的畫面信息堂鲜,關(guān)鍵幀與關(guān)鍵幀之間的過渡幀都將由計(jì)算機(jī)自動生成。
這里說的 關(guān)鍵幀動畫占婉,是指的廣義上的一種動畫制作方式泡嘴,并不僅指
CAKeyframeAnimation
甫恩,CABasicAnimation
的實(shí)現(xiàn)方式也屬于 關(guān)鍵幀動畫
iOS 動畫
說完廣義上的動畫逆济,就可以來說說 iOS 的動畫了。
先來說說動畫的本質(zhì)磺箕。
動畫的本質(zhì)
繼續(xù)用上面的簡單例子:一個(gè) UIView
從 (0,0) 勻速移動到 (100,0)的動畫奖慌,動畫總時(shí)間是0.25秒。
假設(shè)我們基于 60 FPS 來顯示動畫松靡,那么在0.25秒內(nèi)就應(yīng)該有15幀畫面简僧,在每幀畫面中,這個(gè) UIView
的 x坐標(biāo)雕欺,每次應(yīng)移動 100/15 的距離岛马。
如果我們每隔 0.25/15 秒刷新一次UIView
的 x坐標(biāo),那么就能實(shí)現(xiàn)這個(gè)動畫效果了屠列。
對于 x坐標(biāo)而言啦逆,每幀的位置就可以通過一個(gè)基于時(shí)間變化量的函數(shù)來求得:x=f(t) 。
所以笛洛,一個(gè)動畫的本質(zhì)夏志,就是動畫對象(這里是 UIView
)的狀態(tài),基于時(shí)間變化的反應(yīng)了苛让。
簡單說沟蔑,就是給定任意一個(gè)時(shí)刻,如果你都能得到這個(gè)動畫對象的位置和狱杰、形狀等等屬性瘦材,你就能實(shí)現(xiàn)這個(gè)動畫了。
屬性值的變化仿畸,既可能是位置宇色、透明度、旋轉(zhuǎn)角度等的變化颁湖,也包括形狀的改變宣蠕,比如從一條直線變化成一個(gè)圓圈,目標(biāo)就是要得到變化過程中特定時(shí)刻的中間態(tài)甥捺。
動畫的實(shí)現(xiàn)
我們也可將 iOS 的動畫分為兩大類:
- 系統(tǒng)提供的 關(guān)鍵幀動畫 實(shí)現(xiàn)方式抢蚀;用戶指定 關(guān)鍵 信息,系統(tǒng)實(shí)現(xiàn)動畫過程镰禾,對用戶而言操作起來會簡單些皿曲。
- 逐幀動畫 實(shí)現(xiàn)方式唱逢;用戶自己 畫 出每一幀畫面,系統(tǒng)操作方法簡單屋休,但用戶操作的工作量就會大一些坞古。
逐幀動畫實(shí)現(xiàn)方式
簡單的說,要實(shí)現(xiàn)逐幀的方式劫樟,就是需要 周期性 的調(diào)用 繪制 方法痪枫,繪制每幀的動畫對象。
這里說的 繪制叠艳,不光是指覆寫 UIView
的 - drawRect:
的方法來手動重繪視圖奶陈,也包括修改 UIView
它的屬性,比如位置附较、顏色等吃粒。
iOS 的動畫都是基于
CALayer
的,iOS 的UIView
背后都有一個(gè)對應(yīng)的CALayer
拒课。對UIView
的修改實(shí)際上都是對背后CALayer
的修改徐勃。
但如果在逐幀繪制的方法中修改了一個(gè)自建的CALayer
,這個(gè)CALayer
不是對應(yīng)某個(gè)UIView
的早像,需注意系統(tǒng)的 隱式動畫 的影響僻肖,后面會提到這點(diǎn)。
而 周期性扎酷,就需要一個(gè)定時(shí)器來完成了檐涝,即 CADisplayLink
。
CADisplayLink
與 NSTimer
比較類似法挨,可以周期性的調(diào)用指定的方法谁榜。
之所以用 CADisplayLink
,是因?yàn)樗腔谄聊凰⑿侣实姆材桑雌聊幻看嗡⑿聲r(shí)就會觸發(fā)調(diào)用窃植。
iPhone 的屏幕刷新率是 60 FPS。
如果繪制過程過于復(fù)雜荐糜,不能在屏幕刷新一幀的時(shí)間內(nèi)完成巷怜,可以考慮改為每隔一幀繪制,相當(dāng)于是 30 FPS的刷新率暴氏。
不然可能會使動畫不連貫延塑,有卡頓感。
用逐幀方法繪制的原理不是很麻煩答渔,麻煩的是繪制過程关带。
對于一個(gè)復(fù)雜動畫,你可能需要運(yùn)用各種物理沼撕、幾何知識去計(jì)算視圖中間狀態(tài)的信息宋雏。
比如要實(shí)現(xiàn)一條直線卷曲變化為一個(gè)圓的動畫芜飘,你就需要計(jì)算出中間態(tài)的曲線的彎曲程度和位置。
著名的 facebook 的 pop 動畫框架磨总,就是使用 CADisplayLink
這種逐幀繪制的方式實(shí)現(xiàn)的嗦明。
關(guān)鍵幀動畫實(shí)現(xiàn)方式
采用關(guān)鍵幀的方式來實(shí)現(xiàn)動畫,要講的內(nèi)容相對逐幀的方式就多的多了蚪燕。
還是用 UIView
移動的簡單例子娶牌。
這里面有兩個(gè)關(guān)鍵幀,起始幀和結(jié)束幀邻薯,除此之外還有2個(gè)關(guān)鍵信息:
- 起始幀裙戏,變化信息:坐標(biāo)為 (0,0)
- 結(jié)束幀乘凸,變化信息:坐標(biāo)為 (100,0)
- 動畫時(shí)間厕诡,0.25秒
- 勻速運(yùn)動
坐標(biāo) 信息是 UIView
的一個(gè)屬性(實(shí)際是對應(yīng)到 CALayer
的屬性),在動畫實(shí)現(xiàn)里营勤,我們只需要指定起始和結(jié)束的兩個(gè)關(guān)鍵值就夠了灵嫌,中間的過渡值都有系統(tǒng)自動生成。
這里出現(xiàn)了兩種值葛作,一個(gè)是我們設(shè)定的寿羞,一個(gè)是系統(tǒng)生成的,所以要先在這里插入一個(gè) 模型層 和 展現(xiàn)層 的概念了
CALayer
的同一個(gè)屬性值赂蠢,會分別保存在模型層 modelLayer 绪穆,和展現(xiàn)層 presentationLayer 中。當(dāng)我們修改屬性值時(shí)虱岂,是修改的模型層的數(shù)值玖院,動畫時(shí)系統(tǒng)根據(jù)模型層的變化,生成的過渡值第岖,是保存在展現(xiàn)層中的难菌。
在
CALayer
的對象里能直接訪問到這兩層的信息。
而CALayer
的底層實(shí)現(xiàn)實(shí)際不止這兩層蔑滓,但我們現(xiàn)在討論動畫的時(shí)候郊酒,可以只關(guān)心這兩層。
在整個(gè)動畫過程中键袱,呈現(xiàn)出來的過程是這樣的:
- 動畫前燎窘,顯示模型層的當(dāng)前值;
- 動畫開始蹄咖,切換顯示展現(xiàn)層的值褐健;
- 動畫過程中,展現(xiàn)層的值根據(jù)時(shí)間變化,我們看到的實(shí)際是展現(xiàn)層的值在變化卜高;
- 動畫結(jié)束,切換回顯示模型層的值厂榛,此時(shí)模型層的值應(yīng)被修改為動畫結(jié)束時(shí)的值慢叨。
用一段代碼來解釋下動畫過程纽匙。
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
view.backgroundColor = [UIColor redColor];
[self.view addSubview:view];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
animation.fromValue = [NSValue valueWithCGPoint:CGPointMake(50, 0)];
animation.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 0)];
[view.layer addAnimation:animation forKey:nil];
// view.frame = CGRectOffset(view.frame, 100, 0);
你會發(fā)現(xiàn)動畫結(jié)束后,view
又跳回了原來的位置拍谐,這是因?yàn)樽詈笠恍写a注釋了烛缔,而這行代碼的功能就是實(shí)現(xiàn)第4步,將模型層的值修改為動畫結(jié)束時(shí)的值轩拨。
動畫實(shí)現(xiàn)
代碼中的 CABasicAnimation
就是真正的動畫實(shí)現(xiàn)部分践瓷,也就是設(shè)定關(guān)鍵幀信息的地方。
將動畫加入 CALayer
的代碼定義為:
- (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key
接受的類型是 CAAnimation
類型亡蓉,有下面這些子類:
-
CABasicAnimation
晕翠,可設(shè)定起始結(jié)束兩個(gè)關(guān)鍵幀的信息。 -
CAKeyframeAnimation
砍濒,除首尾外淋肾,還可添加多個(gè)中間關(guān)鍵點(diǎn)。 -
CAAnimationGroup
爸邢,可組合多個(gè)動畫樊卓,因?yàn)樯厦鎯煞N動畫一次只能設(shè)置一個(gè)屬性值。 -
CATransition
杠河,圖層過渡動畫碌尔,默認(rèn)是淡入。比如修改一個(gè)CALayer
的背景色時(shí)券敌,是從初始色慢慢淡入過渡到結(jié)束色唾戚。
可修改為新顏色把舊顏色頂出去等效果。還可使用CIFilter
濾鏡做過渡效果陪白,一些開源UIViewController
的過渡動畫使用了這種方式颈走。
動畫中,除了屬性值外咱士,我們還設(shè)置了兩個(gè)和時(shí)間有關(guān)的信息:動畫時(shí)間0.25秒立由,運(yùn)動方式是勻速運(yùn)動。
動畫持續(xù)時(shí)間很簡單序厉,是通過 CAAnimation
遵守的 CAMediaTiming
協(xié)議設(shè)定的锐膜。
勻速運(yùn)動是通過設(shè)置 CAAnimation
的 timingFunction
實(shí)現(xiàn)的,這是一個(gè) CAMediaTimingFunction
類的對象弛房。
之前已經(jīng)說到道盏,動畫過程實(shí)際是一個(gè)時(shí)間的函數(shù),橫坐標(biāo)是時(shí)間的變化值,縱坐標(biāo)是動畫屬性的變化量荷逞。那么我們就可以在一個(gè)直角坐標(biāo)系中媒咳,通過作圖來畫出這個(gè)函數(shù)。比如勻速運(yùn)動的圖形种远,就是一條通過原點(diǎn)的直線涩澡。
所以這個(gè)類的功能就是畫出一條曲線,來表示時(shí)間和屬性變化之間的關(guān)系坠敷。而畫圖的方法妙同,是使用的是畫貝葉斯曲線的方法。
系統(tǒng)提供了幾個(gè)常用的函數(shù)膝迎,比如 kCAMediaTimingFunctionLinear
就是勻速運(yùn)動粥帚;kCAMediaTimingFunctionEaseInEaseOut
就是一般系統(tǒng)動畫的默認(rèn)值,漸入漸出限次,即在動畫開始和結(jié)束的時(shí)候速度稍慢些芒涡。
隱式動畫
上面的過程,我們是 顯式 的向一個(gè) CALayer
添加了一個(gè)動畫掂恕,所以這種方式叫做 顯式動畫拖陆。
對應(yīng)的弛槐,還有 隱式動畫懊亡,即系統(tǒng)自動添加上的動畫。
CALayer *layer = [CALayer layer];
layer.backgroundColor = [UIColor greenColor].CGColor;
layer.frame = CGRectMake(0, 0, 100, 100);
[self.view.layer addSublayer:layer];
layer.frame = CGRectOffset(layer.frame, 100, 0);
這段代碼里乎串,我們沒有添加 CAAnimation
動畫店枣,但 layer 不是直接變化到新的位置,而是有一個(gè)動畫效果叹誉。
這就是 隱式動畫 的效果鸯两。
當(dāng)我們改變 CALayer
的一個(gè)可動畫的屬性值時(shí),就會觸發(fā)系統(tǒng)的隱式動畫长豁。
可動畫的屬性值钧唐,可以在 CALayer
的文檔中找到,屬性說明中標(biāo)有 ** Animatable** 的匠襟,就是可自動添加動畫的屬性钝侠。
但是,有一個(gè)例外酸舍,對于 UIView
背后對應(yīng)的 CALayer
帅韧,系統(tǒng)關(guān)閉了隱式動畫,所以當(dāng)我們直接修改 UIView
或者是其底層的 CALayer
時(shí)啃勉,變化是直接生效的忽舟,沒有動畫效果。
所以當(dāng)我們在逐幀方式生成動畫時(shí),是可以直接修改
UIView
或者是其底層的CALayer
的信息叮阅。
但是如果修改的是一個(gè)自建的單獨(dú)CALayer
時(shí)刁品,幀與幀之間的變化還是會觸發(fā)系統(tǒng)的默認(rèn)隱式動畫,這個(gè)時(shí)候就需要我們來手動關(guān)閉隱式動畫浩姥。
當(dāng)快速動畫的時(shí)候不會察覺到這點(diǎn)哑诊,但這明顯會帶來性能上的浪費(fèi)。
隱式動畫所做的事情和顯示動畫是一樣的及刻,我們設(shè)置的屬性值都是模型層的數(shù)值镀裤,而系統(tǒng)會自動添加屬性對應(yīng)的 CAAnimation
動畫到 CALayer
上。
UIView
有一系列的animateWithDuration
動畫方法缴饭,在這些方法中UIView
會恢復(fù)隱式動畫暑劝,所以在動畫的 block 中修改屬性時(shí),又會觸發(fā)隱式動畫颗搂。
那么系統(tǒng)是如果知道對一個(gè)屬性應(yīng)該添加哪種動畫呢担猛,這就需要讓 CAAction
協(xié)議登場了。
當(dāng)修改一個(gè) CALayer
的屬性時(shí)丢氢,它會通過 - actionForKey:
來查詢這個(gè)屬性對應(yīng)的 action傅联,而 key 就是對應(yīng)的屬性名稱。
CAAnimation
遵守 CAAction
協(xié)議疚察,返回的 action 其實(shí)是個(gè) CAAnimation
動畫蒸走。
也就是說, CALayer
通過 - actionForKey:
來查詢某個(gè)屬性被修改時(shí)貌嫡,需要調(diào)用哪個(gè)動畫去展現(xiàn)這個(gè)變化比驻。
一般默認(rèn)返回的是 CABasicAnimation
,默認(rèn)動畫時(shí)間 0.25秒岛抄,時(shí)間函數(shù)為漸入漸出 kCAMediaTimingFunctionEaseInEaseOut别惦。
- actionForKey:
查詢 action 的步驟有4步,在這個(gè)方法中有詳細(xì)的說明夫椭。
其中一種方式就是通過CALayer
的 delegate 返回 action掸掸。而對于UIView
背后對應(yīng)的CALayer
,其代理就是它對應(yīng)的UIView
蹭秋,UIView
就是用這種方式關(guān)閉了隱式動畫扰付。
動畫事務(wù)
創(chuàng)建動畫事務(wù)的目的是為了操作的原子性,保證動畫的所有修改能同時(shí)生效感凤。
CATransaction
就是動畫事務(wù)的操作類悯周。
在創(chuàng)建隱式動畫的時(shí)候,系統(tǒng)也會隱式的創(chuàng)建一個(gè)動畫事務(wù)陪竿,以保證所有的動畫能同時(shí)進(jìn)行禽翼。
除此之外屠橄,還可以顯式的創(chuàng)建一個(gè)事務(wù)。
顯式事務(wù)中可以定義事務(wù)中所有動畫的運(yùn)行時(shí)間和時(shí)間函數(shù)闰挡,此外锐墙,還有這個(gè)方法 + (void)setDisableActions:(BOOL)flag
能顯式的關(guān)閉這個(gè)事務(wù)中的 action 查詢操作。
關(guān)閉了查詢也就是關(guān)閉了動畫效果长酗,屬性值的變化就會立即生效溪北,而沒有動畫效果了:
[CATransaction begin];
[CATransaction setDisableActions:YES];
///...
layer.frame = CGRectOffset(layer.frame, 100, 0);
///...
[CATransaction commit];
注意別把 CATransaction 和 CATransition 搞混了,一個(gè)單詞是 transaction 事務(wù)夺脾,另一個(gè)是 transition 轉(zhuǎn)變之拨。
對比 總結(jié)
關(guān)鍵幀動畫的實(shí)現(xiàn)方式,只需要修改某個(gè)屬性值就可以了咧叭,簡單方便蚀乔,但涉及的深層次內(nèi)容較多,需要更多的理解和練習(xí)菲茬。
采用逐幀動畫的實(shí)現(xiàn)方式吉挣,實(shí)現(xiàn)原理簡單,但繪制動畫的過程要復(fù)雜婉弹。如果動畫過程處理的事情較多睬魂,也會帶來較大的開銷,就有可能造成動畫幀數(shù)的下降镀赌,出現(xiàn)卡頓的現(xiàn)象氯哮,因此需要較多的測試和調(diào)試。
動畫繪制的過程中佩脊,會要求較多的數(shù)學(xué)蛙粘、物理等知識來計(jì)算中間態(tài)的數(shù)據(jù)。
但這兩種方式也不是絕對分離開的威彰。
關(guān)鍵幀動畫實(shí)現(xiàn)方式,一般只能對系統(tǒng)實(shí)現(xiàn)了可動畫的屬性做動畫處理穴肘,但其實(shí)也是允許實(shí)現(xiàn)自定義屬性的動畫處理的歇盼。
這就需要自己來實(shí)現(xiàn)系統(tǒng)中自動計(jì)算過渡幀的操作了,也就是逐幀實(shí)現(xiàn)動畫的方式了评抚。
實(shí)現(xiàn)自定義屬性的動畫可以參考這篇文章: Layer 中自定義屬性的動畫
對于 iOS 系統(tǒng)提供的動畫方法豹缀,上面只是從整體的角度作了一個(gè)全面的整理,還有很多細(xì)節(jié)內(nèi)容沒有寫出來慨代,比如 CALayer
的三維變換邢笙、CAKeyframeAnimation
的延路徑動畫,CAMediaTiming
的時(shí)間控制侍匙,等等氮惯。感興趣的話,可以再看看這些內(nèi)容:
- 蘋果官方文檔 Core Animation Programming Guide
- iOS Core Animation: Advanced Techniques -- 中文翻譯
- objc中國 動畫專題 中的 動畫解釋