我們知道,iOS的動畫根时,和其對應(yīng)的layer有關(guān)瘦赫。
之前在開發(fā)的過程中碰到一個問題,那就是蛤迎,在一個視圖的動畫過程中确虱,這個視圖view和Layer的frame是怎么變化的?
1 動畫過程中frame的變化
為了研究動畫過程中替裆,view和Layer的frame變化校辩,做了簡單的動畫打印測試,效果如下:
由結(jié)果可知辆童,在UIView動畫的回調(diào)中添加打印宜咒,只會打印一次,且打印的是最終的view和Layer 的位置胸遇,這顯然不是想要的結(jié)果荧呐,所以,在動畫的回調(diào)block中打印是不能實現(xiàn)的;
1.1 添加KVO監(jiān)聽frame的變化
在動畫之前倍阐,對視圖view和layer分別添加kvo監(jiān)聽其frame的變化概疆,看是否能在動畫過程中,視圖的frame發(fā)生變化峰搪,從而打印出對應(yīng)的frame:
如上圖打印結(jié)果岔冀,添加kvo監(jiān)聽,對應(yīng)的view和layer概耻,只監(jiān)聽到了修改view的一次frame的變化使套,添加斷點,監(jiān)聽的回調(diào)也只走一次鞠柄;
侦高??問題:
1.view的frame變化為什么只有一次厌杜?動畫的過程中奉呛,view的frame不變?
2.為什么監(jiān)聽不到layer 的frame的變化夯尽? 最終通過第一次的實驗瞧壮,layer的frame是發(fā)生了變化的啊匙握?
1.2 添加CADisplayLink 打印frame的變化
帶著1.1中的兩個問題咆槽,回想到view動畫的實現(xiàn)原理,動畫的過程其實是一幀一幀顯示的圈纺,所以秦忿,每一幀對應(yīng)的view或者layer的位置是不斷變化的;
所以應(yīng)該嘗試打印在每一幀的顯示時赠堵,view和layer的frame小渊;
而這恰好可以使用系統(tǒng)自帶的CADisplayLink,它的回調(diào)頻率茫叭,和設(shè)備的刷幀是一致的酬屉,添加測試如下:
注意:添加一個dispatch_after,是為了避免一直打印,不方便查看打印結(jié)果揍愁,沒有其他作用呐萨;
打印發(fā)現(xiàn),view和layer的frame還是每次打印的最終位置的frame;
由此莽囤,證明了動畫過程
view的frame只改變的一次谬擦,直接改到了最終的frame;
而動畫的view的layer朽缎,確實是和動畫相關(guān)的惨远,難道它的frame也只改變了一次谜悟?
2 關(guān)于modelLayer和presentationLayer
打開CALayer的頭文件,發(fā)現(xiàn)了其下兩個屬性:
/* Returns a copy of the layer containing all properties as they were
* at the start of the current transaction, with any active animations
* applied. This gives a close approximation to the version of the layer
* that is currently displayed. Returns nil if the layer has not yet
* been committed.
*
* The effect of attempting to modify the returned layer in any way is
* undefined.
*
* The `sublayers', `mask' and `superlayer' properties of the returned
* layer return the presentation versions of these properties. This
* carries through to read-only layer methods. E.g., calling -hitTest:
* on the result of the -presentationLayer will query the presentation
* values of the layer tree. */
/ *在-presentationLayer方法的結(jié)果上調(diào)用時北秽,返回
*具有當前模型值的基礎(chǔ)層葡幸。當調(diào)用
*非表示層,返回接收者贺氓。通話結(jié)果
*產(chǎn)生展示的交易后的此方法
*圖層已完成是不確定的蔚叨。 * /
/ *返回包含所有屬性的圖層的副本
*在當前交易開始時,帶有任何活動的動畫
*適用辙培。這非常接近該圖層的版本
*當前顯示蔑水。如果圖層尚未返回nil
*已承諾。
*
*嘗試以任何方式修改返回的圖層的效果是
*未定義扬蕊。
*
*返回的`sublayers'搀别,`mask'和`superlayer'屬性
*層返回這些屬性的表示形式。這個
*進行只讀層方法厨相。例如领曼,調(diào)用-hitTest:
* -presentationLayer的結(jié)果將查詢演示文稿
*層樹的值。 * /
- (nullable instancetype)presentationLayer;
/* When called on the result of the -presentationLayer method, returns
* the underlying layer with the current model values. When called on a
* non-presentation layer, returns the receiver. The result of calling
* this method after the transaction that produced the presentation
* layer has completed is undefined. */
/ *在-presentationLayer方法的結(jié)果上調(diào)用時蛮穿,返回
*具有當前模型值的基礎(chǔ)層。當調(diào)用
*非表示層毁渗,返回接收者践磅。通話結(jié)果
*產(chǎn)生展示的交易后的此方法
*圖層已完成是不確定的。 * /
- (instancetype)modelLayer;
將回調(diào)中的layer灸异,換成了presentationLayer府适,打印結(jié)果如下:
打印發(fā)現(xiàn)了一開始想要的結(jié)果,原來直接打印view的frame之變化一次是對的肺樟,真正的發(fā)生動畫的是一個叫presentationLayer的東西檐春;
動畫的整個過程其實經(jīng)歷了三個樹狀結(jié)構(gòu),才顯示到了屏幕上:模型樹-->呈現(xiàn)樹-->渲染樹,如圖:
通常么伯,我們操作的是模型樹疟暖;
在重繪周期最后,我們會將模型樹相關(guān)內(nèi)容(層次結(jié)構(gòu)田柔、圖層屬性和動畫)序列化俐巴,通過IPC傳遞給專門負責(zé)屏幕渲染的渲染進程。渲染進程拿到數(shù)據(jù)并反序列化出樹狀結(jié)構(gòu)--呈現(xiàn)樹硬爆。
這個呈現(xiàn)圖層實際上是模型圖層的復(fù)制欣舵,但是它的屬性值代表了在任何指定時刻當前外觀效果。換句話說缀磕,你可以通過呈現(xiàn)圖層的值來獲取當前屏幕上真正顯示出來的值缘圈。
我們可以通過CALayer的presentationLayer方法來訪問對應(yīng)的呈現(xiàn)樹圖層劣光。
注意:呈現(xiàn)圖層僅僅當圖層首次被提交(就是首次第一次在屏幕上顯示)的時候創(chuàng)建,所以在那之前調(diào)用-presentationLayer將會返回nil糟把。
–modelLayer方法: 在呈現(xiàn)圖層上調(diào)用–modelLayer將會返回它正在呈現(xiàn)所依賴的CALayer绢涡。通常在一個圖層上調(diào)用-modelLayer會返回–self(實際上我們已經(jīng)創(chuàng)建的原始圖層就是一種數(shù)據(jù)模型)。
一個移動的圖層是如何通過數(shù)據(jù)模型呈現(xiàn)的:
大多數(shù)情況下糊饱,你不需要直接訪問呈現(xiàn)圖層垂寥,你可以通過和模型圖層的交互,來讓Core Animation更新顯示另锋。
兩種情況下presentationLayer呈現(xiàn)圖層會變得很有用:
- 同步動畫滞项,
- 動畫過程中處理用戶交互。
當模型樹上帶有動畫特征時夭坪,提交到渲染進程后文判,渲染進程會根據(jù)動畫特征,不斷修改呈現(xiàn)樹上的圖層屬性室梅,并同時不斷的在屏幕上渲染出來戏仓,這樣我們就看到了動畫。
2.1 模型樹與呈現(xiàn)樹關(guān)系的比喻
在CALayer內(nèi)部亡鼠,它控制著兩個屬性:presentationLayer(以下稱為P)和modelLayer(以下稱為M)赏殃。
P只負責(zé)顯示,M只負責(zé)數(shù)據(jù)的存儲和獲取间涵。
我們對layer的各種屬性賦值比如frame仁热,實際上是直接對M的屬性賦值;
而P將在每一次屏幕刷新的時候回到M的狀態(tài)勾哩。
比如此時M的狀態(tài)是1抗蠢,P的狀態(tài)也是1,然后我們把M的狀態(tài)改為2思劳,那么此時P還沒有過去迅矛,也就是我們看到的狀態(tài)P還是1,在下一次屏幕刷新的時候P才變?yōu)?潜叛。而我們幾乎感知不到兩次屏幕刷新之間的間隙秽褒,所以感覺就是我們一對M賦值,P就過去了钠导。
P就像是瞎子震嫉,M就像是瘸子,瞎子背著瘸子牡属,瞎子每走一步(也就是每次屏幕刷新的時候)都要去問瘸子應(yīng)該怎樣走(這里的走路就是繪制內(nèi)容到屏幕上)票堵,瘸子沒法走,只能指揮瞎子背著自己走逮栅。
重點: 動畫完成回到原地
可以簡單的理解為:一般情況下悴势,任意時刻P都會回到M的狀態(tài)窗宇。
而當一個CAAnimation(以下稱為A)加到了layer上面后,A就把M從P身上擠下去了特纤。
現(xiàn)在P背著的是A军俊,P同樣在每次屏幕刷新的時候去問他背著的那個家伙,A就指揮它從fromValue到toValue來改變值捧存。而動畫結(jié)束后粪躬,A會自動被移除,這時P沒有了指揮昔穴,就只能大喊“M你在哪”镰官,M說我還在原地沒動呢,于是P就順聲回到M的位置了吗货。
這就是為什么動畫結(jié)束后我們看到這個視圖又回到了原來的位置泳唠,是因為我們看到在移動的是P,而指揮它移動的是A宙搬,M永遠停在原來的位置沒有動笨腥,動畫結(jié)束后A被移除,P就回到了M的懷里勇垛。
動畫結(jié)束后脖母,P會回到M的狀態(tài)(當然這是有前提的,因為動畫已經(jīng)被移除了闲孤,我們可以設(shè)置fillMode來繼續(xù)影響P)镶奉,但是這通常都不是我們動畫想要的效果。我們通常想要的是崭放,動畫結(jié)束后,視圖就停在結(jié)束的地方鸽凶,并且此時我去訪問該視圖的屬性(也就是M的屬性)币砂,也應(yīng)該就是當前看到的那個樣子。按照官方文檔的描述玻侥,我們的CAAnimation動畫都可以通過設(shè)置modelLayer到動畫結(jié)束的狀態(tài)來實現(xiàn)P和M的同步决摧。
2.2 動畫的實現(xiàn)方式
在iOS中,實現(xiàn)動畫的方式主要分兩大類:
- CoreAnimation動畫
- 非CoreAnimation動畫凑兰。
CoreAnimation動畫:
CoreAnimation動畫掌桩,即基于事務(wù)的動畫,是最常見的動畫實現(xiàn)方式姑食。動畫執(zhí)行者是專門負責(zé)渲染的渲染進程波岛,操作的是呈現(xiàn)樹 presentationLayer。我們應(yīng)該盡量使用CoreAnimation來控制動畫音半,因為CoreAnimation是充分優(yōu)化過的:
1则拷、更高效的繪制
基于Layer的繪圖過程中贡蓖,CoreAnimation通過硬件操作位圖(變換、組合等)煌茬,產(chǎn)生動畫的速度比軟件操作的方式快很多斥铺。
基于View的繪圖過程中,view被改動時會觸發(fā)的drawRect:方法來重新繪制位圖坛善,但是這種方式需要CPU在主線程執(zhí)行晾蜘,比較耗時。而CoreAnimation則盡可能的操作硬件中已緩存的位圖眠屎,來實現(xiàn)相同的效果剔交,從而減少了資源損耗。
2组力、更高效的動畫
在動畫過程中省容,CoreAnimation會通過硬件來一幀一幀的繪制。你所做的就是指定動畫的起點和終點燎字,其他的都讓CoreAnimation來做腥椒。當然你也可以自定義動畫參數(shù),否則CoreAnimation會使用合適的默認值候衍。
非CoreAnimation動畫:
非CoreAnimation動畫執(zhí)行者是當前進程笼蛛,操作的是模型樹 modelLayer。常見的有定時器動畫和手勢動畫蛉鹿。定時器動畫是在定時周期觸發(fā)時修改模型樹的圖層屬性滨砍;手勢動畫是手勢事件(比如UIScrollView的didScrollView)觸發(fā)時修改模型樹的圖層屬性。兩者都能達到視圖隨著時間不斷變化的效果妖异,即實現(xiàn)了動畫惋戏。
非CoreAnimation動畫動畫過程中實際上不斷改動的是模型樹,而呈現(xiàn)樹僅僅成了模型樹的復(fù)制品他膳,狀態(tài)與模型樹保持一致响逢。整個過程中,主要是CPU在主線程不斷調(diào)整圖層屬性棕孙、布局計算舔亭、提交數(shù)據(jù),沒有充分利用到CoreAnimation強大的動畫控制功能蟀俊。
以上部分關(guān)于layer的描述摘自文章鏈接
3 動畫過程中的點擊交互處理
如果你想讓你做動畫的圖層響應(yīng)用戶輸入:
你可以使用-hitTest:方法來判斷指定圖層是否被觸摸钦铺,這時候?qū)Τ尸F(xiàn)圖層而不是模型圖層調(diào)用-hitTest:會顯得更有意義,因為呈現(xiàn)圖層代表了用戶當前看到的圖層位置肢预,而不是當前動畫結(jié)束之后的位置矛洞。
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
self.testview = [[UIView alloc]initWithFrame:CGRectMake(0, 100, 50, 30)];
self.testview.backgroundColor = [UIColor orangeColor];
self.testview.userInteractionEnabled = NO;
[self.view addSubview:self.testview];
[UIView animateWithDuration:3.0 animations:^{
self.testview.frame = CGRectMake(300, 100, 50, 30);
}];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self.view];
//方案一
// if (CGRectContainsPoint(self.testview.layer.presentationLayer.frame, point)) {
// [self clickPresntationLayer];
// }
//方案二
if ([self.testview.layer.presentationLayer hitTest:point] != nil) {
[self clickPresntationLayer];
}
}
- (void)clickPresntationLayer{
self.testview.backgroundColor = [UIColor colorWithRed:(arc4random()%255/255.0) green:(arc4random()%255/255.0) blue:(arc4random()%255/255.0) alpha:1.0];
}
方案一:直接使用了CGRectContainsPoint(CGrect rect, CGPoint point);方法,該方法返回一個BOOL值误甚,判斷point是否在rect內(nèi)部缚甩,剛好可以傳染presentationLayer的frame和當前點擊的point谱净;
方案二:直接使用了hitTest:(CGPoint)p;方法,該方法返回一個CALayer對象擅威,如果點擊的point在其內(nèi)部壕探,返回一個layer對象;
另外郊丛,對于CALayer的另一個方法:
- (BOOL)containsPoint:(CGPoint)p;
做了嘗試發(fā)現(xiàn)不管用李请,梳理一下,當前的point在self.view上厉熟,而這個方法的point是在layer內(nèi)部的导盅,所以可能不適用。
之前面試時有碰到問題揍瑟,在回到UIView和CALayer的區(qū)別時:
有回答CALayer不能響應(yīng)點擊事件:但現(xiàn)在來看白翻,通過hitTest方法,確實是可以響應(yīng)點擊事件的绢片。