上周,為了實(shí)現(xiàn)在代碼中動(dòng)態(tài)移動(dòng)View的位置的需求吁峻,遇到難以處理的問(wèn)題。
先看看需求
在XML布局文件中在张,擺放好View的位置用含。接下來(lái)以此布局位置播放動(dòng)畫,當(dāng)播放到一個(gè)結(jié)點(diǎn)的時(shí)候帮匾,要讓一個(gè)ImageView啄骇、兩個(gè)TextView出現(xiàn)在另外一個(gè)位置上,而且會(huì)有一些變化瘟斜。除了基本的XY的左邊變化了之外缸夹,ICON的圖標(biāo)的大小,TextView的TextSize螺句、TextColor虽惭、Lines、maxWidth都產(chǎn)生了變化蛇尚。本來(lái)用兩個(gè)View芽唇,分別布局在對(duì)應(yīng)的位置上,控制他們的Visibility就可以取劫,但因?yàn)橐恍┨厥庑源殷裕荒苣敲醋鲅新拢荒茏孷iew在播放動(dòng)畫的同時(shí)進(jìn)行View位置的移動(dòng)。
解決方案
在布局文件中擺放好不顯示的替身View炮捧,然后當(dāng)變化的時(shí)候庶诡,將替身的XY值傳遞給要變化的View。
理論上是可以實(shí)現(xiàn)的咆课,代碼如下:
實(shí)際上確實(shí)困難重重末誓。大小可以變,但是位置就是不能對(duì)上傀蚌。
最后總結(jié)出問(wèn)題出在requestLayout()上基显。
淺析requestLayout()
1.requestLayout()和invalidate()的區(qū)別
requestLayout():view調(diào)用這個(gè)方法要求parent view重新進(jìn)行一次測(cè)量、布局善炫、繪制這三個(gè)流程來(lái)更新自己位置撩幽。
invalidate():view調(diào)用這個(gè)方法迫使view進(jìn)行重新繪制。
一句話箩艺,requestLayout()的效果是重新布局自己在父布局中的位置窜醉,invalidate()的效果是強(qiáng)制調(diào)用自己的onDraw()方法。
2.分析requestLayout()
我們先看View的requestLayout()方法的源碼:
在requestLayout方法中艺谆,首先先為當(dāng)前View設(shè)置上PFLAG_FORCE_LAYOUT的標(biāo)記位榨惰,表示當(dāng)前的View需要重新繪制。
接下來(lái)會(huì)判斷當(dāng)前View樹是否正在布局流程静汤,這會(huì)調(diào)用ViewRootImpl的isLayoutRequested()方法琅催,如下:
當(dāng)父布局已經(jīng)開始重新布局的時(shí)候,不會(huì)繼續(xù)傳遞重新布局的請(qǐng)求虫给,而是帶著FORCE_LAYOUT的標(biāo)記等待重新繪制的流程走到這里藤抡。
當(dāng)并沒(méi)有已經(jīng)在重新布局的時(shí)候,接著調(diào)用mParent.requestLayout方法抹估,為父容器添加PFLAG_FORCE_LAYOUT標(biāo)記位缠黍,而父容器又會(huì)調(diào)用它的父容器的requestLayout方法.
requestLayout的事件會(huì)層層上傳,直到DecorView药蜻,即根View瓷式,而根View又會(huì)傳遞給ViewRootImpl。
即是說(shuō)任何一個(gè)View的requestLayout事件语泽,最終會(huì)被ViewRootImpl接收并得到處理贸典。
接下來(lái)看一下ViewRootImpl的requestLayout方法:
在這里,調(diào)用了scheduleTraversals方法踱卵,這個(gè)方法是一個(gè)異步方法瓤漏,post了一個(gè)Runnable,我們繼續(xù)跟進(jìn),看一下這個(gè)TraversalRunnable接口蔬充。
這個(gè)Runnable只調(diào)用了一個(gè)方法,doTraversal()班利,如下:
最終我們看到饥漫,會(huì)調(diào)用到performTraversals方法,這也是View工作流程的核心方法罗标。這個(gè)方法從1282行庸队,一直到2082行,一共800行代碼闯割,太長(zhǎng)了彻消,就不展示了。
這個(gè)方法有多重要呢宙拉?
整個(gè)Android的UI繪制機(jī)制是從哪里開始的即入口在哪里呢宾尚?答案就是ViewRootImpl類的performTraversals()方法。在這個(gè)方法內(nèi)部谢澈,分別調(diào)用measure煌贴、layout、draw方法來(lái)進(jìn)行View的三大工作流程锥忿。
至此牛郑,我們就能明白了,requestLayout()會(huì)牽動(dòng)出整個(gè)Android繪制機(jī)制重新走一遍流程敬鬓。
接下來(lái)淹朋,繼續(xù)看一下View的measure()方法:
首先是判斷一下標(biāo)記位,如果當(dāng)前View的標(biāo)記位為PFLAG_FORCE_LAYOUT钉答,那么就會(huì)進(jìn)行測(cè)量流程础芍,調(diào)用onMeasure,對(duì)該View進(jìn)行測(cè)量希痴,接著最后為標(biāo)記位設(shè)置為PFLAG_LAYOUT_REQUIRED,這個(gè)標(biāo)記位的作用就是在View的layout流程中者甲,如果當(dāng)前View設(shè)置了該標(biāo)記位,則會(huì)進(jìn)行布局流程砌创。
到目前為止虏缸,requestLayout的流程便完成了。
隨著流程的梳理完嫩实,導(dǎo)致我們出問(wèn)題的原因已經(jīng)找到刽辙,再看一眼出問(wèn)題的代碼:
我們先看一下,TextView的setTextSize方法甲献,
我們會(huì)發(fā)現(xiàn)宰缤,其中調(diào)用了requestLayout()方法和invalidate()方法。(requestLayout()方法的目的是重新調(diào)整TextView的擺放位置,invalidate()方法的目的是重新繪制一遍文字內(nèi)容)
當(dāng)這里已經(jīng)調(diào)用過(guò)requestLayout()方法的時(shí)候慨灭,由于ViewRootImpl對(duì)象是唯一的朦乏,所以其他的requestLayout()方法的調(diào)用都會(huì)被擱置等待。而這個(gè)方法又是異步的氧骤,所以如果我們同步的去變更調(diào)整TextView的位置呻疹,都是根據(jù)的舊布局取到的XY坐標(biāo)。所以解決方法如下:
不需要主動(dòng)調(diào)用requestLayout()方法了筹陵,在同步的變化所有的View的屬性之后刽锤,拋一個(gè)消息等待requestLayout()結(jié)束之后,再賦值朦佩,就可以在新布局的基礎(chǔ)上變化了并思,于是就按照預(yù)期的產(chǎn)生位置變動(dòng)了。