View狀態分類
在View視圖中定義了多種和介面效果相關的狀態,比如擁有焦點Focused、按下Pressed等,不同的狀態一般會顯示不同的介面效果,而且檢視狀態會隨著使用者的操作而改變,一般通過xml檔案中selector來申明各種狀態下使用的背景圖;所有的狀態代碼位於StateListDrawable中,常用的狀態代碼包括:
- enable:當前View是否可用,開發人員可以通過setEnable()改變,他完全由開發人員控制;
當狀態為不可用時,View將不會響應任何事件;
- focused:當前View是否正擁有焦點,一個視窗中只能有一個View擁有焦點,一般隨使用者操作而動態改變;
該狀態主要是針對按鍵的,因為所有的按鍵訊息都將派發給focused視圖;
- pressed:當前View是否正被按下,主要是針對觸摸訊息的,一般當使用者按下時視圖會有一個明顯的變化,也是隨使用者操作而動態改變;
- selected:當前View是否已被選中,一個視窗中可以有多個視圖處於選中狀態;開發人員可以通過setSelected()改變,他完全由開發人員控制;
導致View樹重新遍曆的總體誘因
遍曆View樹意味著整個View需要重新對其包含的子視圖分配大小並重繪;一般情況下導致重新遍曆的原因有三個:其一,視圖本身內部狀態發生變化,比如顯示內容由GONE到VISIBLE;其二,ViewGroup中添加或刪除了視圖導致需要重新為子視圖分配位置;其三,視圖本身的大小發生變化,比如TextView中的常值內容變多變少了;
在代碼層面這三種情況最後都會直接或間接調用到View中的三個函數:requestLayout/requestFocus/invalidate;由於是View樹遍曆,所以最後都會執行到最頂級父視圖中的ViewRootImpl.scheduleTraversals();在該方法內,系統會發起一個非同步訊息(老版本中直接通過Handler發,新版本4.1中引入了Choreographer,以及對VSync和三級Buffer支援,讓頁面顯示和操作更流暢,具體可以詳見《Android
Project Butter分析》),然後在非同步訊息執行過程中調用performTraversals()完成具體的View樹遍曆;可以參見:
View中超多的屬性變數如何管理?
在龐大的View類中會涉及到非常多的狀態代碼,比如是否可用、是否處於按下狀態、是否需要重新分配位置、是否需要重繪等等;View樹在遍曆重繪時會根據不同的變數值來進行相應的操作,為此View中引入了bit標示位來管理這些狀態值,分別用mViewFlags和mPrivateFlags變數來管理(隨著狀態代碼的增加,在新版本4.2中還有mPrivateFlags2/mPrivateFlags3變數),他們都是int類型的,也就是說理論上每個變數可以用來標示32個狀態值,當對各個狀態值修改時採用位元運算符&|來完成;
其中mViewFlags變數主要用來儲存和檢視狀態相關的值,比如是否可單擊、是否可雙擊、是否可用、是否擁有焦點等;
mPrivateFlags變數主要用來儲存和內部邏輯相關的屬性,比如是否需要重新分配位置、是否需要重繪、是否重新整理View緩衝等;
注意:這兩個變數之間是有緊密聯絡的,經常會需要兩個變數同時設定某些狀態值,可以參見setFlags(..)方法中的具體內容;
requestLayout()
該方法的執行過程很簡單,因為當View樹進行重新布局時,總是重新給所有的視圖都進行布局,而不像重繪是可以指定只繪製某一個小地區的;
從代碼層面他只是為mPrivateFlags變數添加FORCE_LAYOUT標識而已;然後逐層請求mParent.requestLayout();詳見:
invalidate()
該方法的作用是請求View樹重繪;視圖及其父視圖在介面上是分層先後顯示的,父視圖位於子視圖下面,繪製過程中,首先繪製最底層的根視圖,然後繪製其包含的子視圖,子視圖若是ViewGroup,則繼續繪製其子視圖,如此迭代至沒有子視圖為止;
在具體的重繪過程中,一般不會對所有視圖都進行重繪,而是只繪製那些“需要繪製”的視圖,那如何找出“需要繪製”的地區呢?這就是invalidate方法要完成的功能!
大致的思路是:當View需要重繪時會給mPrivateFlags變數添加DRAWN標識,然後根據所有帶該標識的視圖邊界一起確定最終要重繪的矩形區塊,這裡面會涉及到不同座標體系間的換算,可以參見:
代碼的具體執行過程是:
- View.invalidate()中設定必要的狀態位標識之後,會執行到mParent.invalidateChild(..);
這裡的mParent有兩種情況,一種是有父視圖ViewGroup,另一種是已經到頂層了為ViewRootImpl;
- 若是ViewGroup,會執行完
invalidateChildInParent(…)之後繼續調用mParent.invalidateChildInParent(…);
- 最終調用到ViewRootImpl.invalidateChildInParent(…),進而執行scheduleTraversals();
注意:這裡會提前判斷 mWillDrawSoon局部變數值,若當前已經在執行performTraversals()遍曆重繪了,那就不會調用scheduleTraversals(),也就不會發起重繪的非同步訊息了,但View中設定的各種狀態值仍然是有效,只是會在下次重繪時生效;
scheduleTraversals()
該方法會在多個地方被調用,比如requestLayout()/invalidate()中,而我們又會經常會看到連續調用這兩個方法的情況,那這樣豈不是會發起兩次View樹遍曆重繪請求?其實是不會的,因為在scheduleTraversals()方法內設定了一個局部變數mTraversalScheduled,若先執行了requestLayout(),那此時mTraversalScheduled為false,發起一個非同步訊息請求重繪,並將mTraversalScheduled變數值設為true,這樣接著調用invalidate()時判斷mTraversalScheduled變數值已經不是false了,這樣就確保了只發起一個非同步重繪請求;參見:
performTraversals()
該方法時系統內進行View樹遍曆並進行頁面重繪的核心方法,內部邏輯還是非常複雜的,約800行代碼;老實說偶目前還未完全看懂裡面的細節,中間涉及的關聯變數實在太多了;但大致的主體流程還是清晰的,就是根據之前設定好的各種狀態值,判斷是否需要重新計算視圖大小(Measure)、是否需要重新分配視圖的位置(也叫布局Layout)、以及是否需要重繪視圖(Draw),架構過程參見,其中每項的具體過程詳見後面的具體描述:
以上內容若有轉載,請註明出處,歡迎訪問老唐的專欄http://blog.csdn.net/sfdev