Android中View的量算、佈局及繪圖機制
為了研究Android中View的佈局及繪圖機制,我建立了一個非常簡單的App,該App只有一個Activity,該Activity對應的layout如下所示:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">
<TextView android:text="@string/hello_world" android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>
該佈局檔案很簡單,RelativeLayout下面就一個TextView。
我們啟動App後,通過Hierarchy Viewer檢視App中的佈局層級,如下所示:
從上圖我們可以看出,App的根結點是PhoneWindow$DecorView
,此處的$表示DecorView是PhoneWindow下面的內部類例項。PhoneWindow$DecorView
下面有三個child,分別是LinearLayout例項、View@49da043和View@44ff410。View@49da043表示的是navigationBarBackground,View@44ff410表示的是statusBarBackground。LinearLayout下面有兩個child,分別是ViewStub例項和FrameLayout例項,其中ViewStub不需要繪製,所以我們在下面的討論中可以直接對其忽略。FrameLayout下有一個child,RelativeLayout例項,該RelativeLayout例項對應的就是佈局檔案activity_main.xml中的RelativeLayout,RelativeLayout下有一個child,即TextView。
以上提到的控制元件都是View的例項,有的則是ViewGroup的例項,ViewGroup繼承自View,PhoneWindow$DecorView
、RelativeLayout、FrameLayout、RelativeLayout都直接或間接繼承自ViewGroup,只有ViewGroup例項才能有子節點。
當我們在onCreate()方法中呼叫setContentView(R.layout.activity_main)
方法後,Android會從layout的樹形結構中自上而下開始對所有的View進行量算、佈局、繪圖,具體來說經過以下過程:
Android自上而下對所有View進行量算,這樣Android就知道了每個View想要的尺寸大小,即寬高資訊
在完成了對所有View的量算工作後,Android會自上而下對所有View進行佈局,Android就知道了每個View在其父控制元件中的位置,即View到其父控制元件四邊的left、right、top、bottom
在完成了對所有View的佈局工作後,Android會自上而下對所有View進行繪圖,這樣Android就將所有的View渲染到螢幕上了
以下是涉及到的相關類的原始碼:
View原始碼
ViewGroup原始碼
ViewRootImpl原始碼
PhoneWindow$DecorView原始碼
LinearLayout原始碼
FrameLayout原始碼
RelativeLayout原始碼
TextView原始碼
量算
關於Measure:
View用measure()方法進行量算,量算的目的是View讓其父節點知道它想要多大的尺寸,所以說量算是後面對View進行佈局以及繪圖的基礎。
View的measure()方法中會執行onMeasure()方法,View類本身的onMeasure()方法不是空方法,其將量算完的結果儲存到View中。View的子類不應該重寫measure()方法,如果需要的話應該重寫onMeasure()方法,ViewGroup的子類都應該重寫onMeasure()方法,比如
PhoneWindow$DecorView
、RelativeLayout、FrameLayout、RelativeLayout都重寫了onMeasure()方法,這些類都在onMeasure()方法中遍歷child,並呼叫child的measure()方法,對child進行量算,縱向遞迴進行,從而實現自上而下對View樹進行量算,直至完成對葉子節點View的量算。量算的起點是ViewRootImpl類,ViewRootImpl是根View,即View樹上面的根結點,嚴格來說ViewRootImpl不屬於View,其實現了ViewParent介面, 其下才是
PhoneWindow$DecorView
。Android在對View樹進行自上而下的量算時,採用的是深度優先演算法,而非廣度優先演算法,即遍歷到某個View時,Android會首先沿著該View一直縱向遍歷並量算到處於葉子節點的View,只有對該View及其所有子孫View(如果存在子孫View的話)完成量算後,才會量算該View的兄弟節點View。
以下是Android對所有View自上而下量算的呼叫過程:
由上我們可以看出,首先ViewRootImpl執行了doTraversal()和performTraversals() 方法,然後執行ViewRootImpl的performMeasure()方法,該方法是Android對所有View進行量算的起點。在該方法中會從ViewRootImpl開始自上而上對View樹進行遍歷,首先ViewRootImpl對
PhoneWindow$DecorView
進行量算,在執行到PhoneWindow$DecorView
的onMeasure()方法時,其遍歷所有的child,對依次它們進行量算,首先對呼叫LinearLayout的measure()方法,對第一個子節點LinearLayout進行量算。LinearLayout在measure()方法中會呼叫onMeasure()方法,在該方法中LinearLayout呼叫了measureVertical()方法,該方法會遍歷其child並對其進行量算,由於其子節點ViewStub不用於渲染,所以此處不對其量算,對其忽略,對另一個child FrameLayout進行量算,呼叫FrameLayout的measure()方法。
FrameLayout在執行measure()方法時會執行onMeasure()方法,在該方法中會遍歷所有的child,並對它們進行量算。其下只有一個child,即RelativeLayout,呼叫RelativeLayout的measure()方法,對其進行量算。
RelativeLayout在measure()方法中會執行onMeasure()方法,在該方法中會遍歷所有的child,並對它們進行量算。其下只有一個child,即TextView,呼叫TextView的measure()方法對其進行量算,在其中會執行onMeasure()方法。
以上完成了對View樹中LinearLayout及其所有子算View的量算工作,之後會對
PhoneWindow$DecorView
中的另外兩個View進行量算,這也體現了Android採用深度優先演算法對View樹進行遍歷量算的過程。View@49da0d3和View@44ff410會依次執行measure()方法和onMeasure()方法。
這樣整個View樹自上而下的量算過程就結束了,經過量算Android知道了各個View想要渲染的尺寸大小,即寬度和高度資訊。
關於量算中measure()和onMeasure()方法的一些細節可參見博文《 原始碼解析Android中View的measure量算過程》。
佈局
關於Layout:
佈局的前提是已經對View進行了量算,View通過呼叫layout()方法進行佈局,佈局的目的是讓Android知道View在其父控制元件中的位置,即距父控制元件四邊的距離left、right、top、bottom。佈局是繪圖的基礎,只有完成了佈局,才能對View進行繪圖。
View的layout()方法中會執行onLayout()方法,View類本身的onLayout()是空方法。View的子類不應該重寫layout()方法,如果需要的話應該重寫其onLayout()方法,ViewGroup的子類都應該重寫onLayout()方法,比如PhoneWindow$DecorView、RelativeLayout、FrameLayout、RelativeLayout都重寫了onLayout()方法,這些類都在onLayout()方法中遍歷child,並呼叫child的layout()方法,對child進行佈局,縱向遞迴進行,從而實現自上而下對View樹進行佈局,直至完成對葉子節點View的佈局。
佈局的起點也是ViewRootImpl類,ViewRootImpl是根View,即View樹上面的根結點,嚴格來說ViewRootImpl不屬於View,其實現了ViewParent介面, 其下才是
PhoneWindow$DecorView
。Android在對View樹進行自上而下的佈局時,採用的是深度優先演算法,而非廣度優先演算法,即遍歷到某個View時,Android會首先沿著該View一直縱向遍歷並佈局到處於葉子節點的View,只有對該View及其所有子孫View(如果存在子孫View的話)完成佈局後,才會佈局該View的兄弟節點View。
Android中的佈局過程與之前上面提到的量算過程很類似,以下是Android對所有View自上而下佈局的呼叫過程:
由上我們可以看出,首先ViewRootImpl執行了doTraversal()和performTraversals() 方法,然後執行ViewRootImpl的performLayout()方法,該方法是Android對所有View進行佈局的起點。在該方法中會從ViewRootImpl開始自上而下對View樹進行遍歷,首先ViewRootImpl執行
PhoneWindow$DecorView
的layout()方法,對其進行佈局。PhoneWindow$DecorView
在其layout()方法中會執行onLayout()方法,PhoneWindow$DecorView
會在onLayout()方法中遍歷其所有的child,並依次呼叫child的layout()方法,實現對child的佈局。首先呼叫其第一個child LinearLayout的layout()方法。LinearLayout在layout()方法中會執行onLayout()方法,在該方法中會呼叫layoutVertical()方法,該方法會遍歷其所有的child並依次呼叫child的layout()方法進行佈局。由於其子節點ViewStub不用於渲染,所以此處不對其進行佈局,對其忽略,對另一個child FrameLayout進行佈局,呼叫FrameLayout的layout()方法。
FrameLayout在layout()方法中會執行onLayout()方法,在該方法中會呼叫layoutChildren()方法,該方法會遍歷其所有的child並依次呼叫child的layout()方法進行佈局。其下只有一個child,即RelativeLayout,執行RelativeLayout的layout()方法,對其進行佈局。
RelativeLayout在layout()方法中會執行onLayout()方法,在該方法中會遍歷所有的child並依次呼叫child的layout()方法進行佈局。其下只有一個child,即TextView,呼叫TextView的layout()方法對其進行佈局,在其中會執行onLayout()方法。
以上完成了對View樹中LinearLayout及其所有子孫View的佈局工作,之後會對
PhoneWindow$DecorView
中的另外兩個View進行佈局,這也體現了Android採用深度優先演算法對View樹進行遍歷佈局的過程。View@49da043和View@44ff410會依次執行layout()方法和onLayout()方法。
這樣整個View樹自上而下的佈局過程就結束了,經過佈局Android知道了各個View在其父控制元件中的位置。
關於佈局layout的細節可參見博文《原始碼解析Android中View的layout佈局過程》。
繪圖
關於Draw:
繪圖的前提是已經對View進行了量算和佈局,View通過呼叫draw()方法進行繪圖,繪圖的目的就是讓View在UI介面上呈現出來。
View的draw()方法中會依次onDraw()和dispatchDraw()方法,View類本身的onDraw()和dispatchDraw()方法都是空方法。View的子類不應該重寫draw()方法,如果需要的話應該按具體情況選擇重寫onDraw()方法或dispatchDraw()方法,具體來說:
- 當我們需要自定義一個View(而非ViewGroup)時,我們需要重寫View的onDraw()方法以實現對自定義View的繪製,即onDraw()用於繪製View自身UI。
- Android中的ViewGroup類重寫了View中的dispatchDraw()方法,ViewGroup.dispatchDraw()方法會遍歷其所有的child,並依次呼叫child的draw()方法,即dispatchDraw()用於繪製ViewGroup的所有子孫View的UI,這是與onDraw()不同的。由於ViewGroup已經具體實現了dispatchDraw()方法,所以大部分情況下ViewGroup的子類無需再對其進行重寫,例如PhoneWindow$DecorView、RelativeLayout、FrameLayout、RelativeLayout都沒有重寫dispatchDraw()方法。只有在極少數情況下,為了實現某些特殊需求,我們才有可能重寫ViewGroup的dispatchDraw()方法,但是即便重寫該方法我們也應該在我們的實現中呼叫super.dispatchDraw()方法以便實現對子孫View進行繪製。
繪圖的起點也是ViewRootImpl類,ViewRootImpl是根View,即View樹上面的根結點,嚴格來說ViewRootImpl不屬於View,其實現了ViewParent介面, 其下才是PhoneWindow$DecorView。
Android在對View樹進行自上而下的繪圖時,採用的也是深度優先演算法,而非廣度優先演算法,即遍歷到某個View時,Android會首先沿著該View一直縱向遍歷並繪圖到處於葉子節點的View,只有對該View及其所有子孫View(如果存在子孫View的話)完成繪圖後,才會渲染該View的兄弟節點View。
Android中的繪圖過程與之前上面提到的量算、佈局過程類似,以下是Android對所有View進行自上而下繪圖的呼叫過程:
由上我們可以看出,首先ViewRootImpl執行了doTraversal()和performTraversals() 方法,然後執行ViewRootImpl的performDraw()方法,該方法是Android對所有View進行繪圖的起點。在該方法中會從ViewRootImpl開始自上而下對View樹進行遍歷,首先ViewRootImpl執行
PhoneWindow$DecorView
的draw()方法,對其繪圖。PhoneWindow$DecorView
在其draw()方法中會依次執行onDraw()方法和dispatchDraw()方法,在dispatchDraw()方法中會遍歷所有的child,呼叫child的draw()方法,對child進行繪圖。首先呼叫其第一個child LinearLayout的draw()方法。LinearLayout在darw()方法中也會依次執行onDraw()方法和dispatchDraw()方法,在dispatchDraw()方法中會遍歷所有的child,呼叫child的draw()方法,對child進行繪圖。由於其子節點ViewStub不用於渲染,所以此處不對其進行繪圖,對其忽略,對另一個child FrameLayout進行繪圖,呼叫FrameLayout的draw()方法。
FrameLayout在draw()方法中也會依次執行onDraw()方法和dispatchDraw()方法,在dispatchDraw()方法中會遍歷所有的child,呼叫child的draw()方法,對child進行繪圖。其下只有一個child,即RelativeLayout,執行RelativeLayout的draw()方法,對其進行繪圖。
RelativeLayout在draw()方法中也會依次執行onDraw()方法和dispatchDraw()方法,在dispatchDraw()方法中會遍歷所有的child,呼叫child的draw()方法,對child進行繪圖。其下只有一個child,即TextView,執行TextView的draw()方法,對其進行繪圖,並在其中執行TextView的onDraw()方法,對TextView進行實際的渲染。
以上完成了對View樹中LinearLayout及其所有子孫View的繪圖工作,之後會對
PhoneWindow$DecorView
中的另外兩個View進行繪圖,這也體現了Android採用深度優先演算法對View樹進行遍歷繪圖的過程。View@49da043和View@44ff410會依次執行draw()方法和onDraw()方法。
總結
當我們在onCreate()方法中呼叫setContentView(R.layout.activity_main)
方法後,Android會從layout的樹形結構中自上而下開始對所有的View進行量算、佈局、繪圖:
量算、佈局、繪圖的起點都是ViewRootImpl
通過呼叫ViewRootImpl的performMeasure() 方法,開始驅動Android自上而下對所有View進行量算,這樣Android就知道了每個View想要的尺寸大小,即寬高資訊
在完成了對所有View的量算工作後,通過呼叫ViewRootImpl的performLayout()方法,開始驅動Android會自上而下對所有View進行佈局,Android就知道了每個View在其父控制元件中的位置,即View到其父控制元件四邊的left、right、top、bottom
在完成了對所有View的佈局工作後,通過呼叫ViewRootImpl的performDraw()方法,開始驅動Android會自上而下對所有View進行繪圖,這樣Android就將所有的View渲染到螢幕上了
希望本文對大家理解Android中View的佈局和繪圖機制有所幫助。
相關閱讀:
《Android相關博文整理彙總》
《原始碼解析Android中View的measure量算過程》
《原始碼解析Android中View的layout佈局過程》
相關文章
- Android中View的測量和佈局過程AndroidView
- Android-繪圖機制總結Android繪圖
- Android GUI之View佈局AndroidGUIView
- 原始碼解析Android中View的layout佈局過程原始碼AndroidView
- Android XML佈局報錯:android/view/View$OnUnhandledKeyEventListenerAndroidXMLView
- 你需要知道的Android View的佈局AndroidView
- Android View 佈局流程(Layout)完全解析AndroidView
- Android自定義View(四)側滑佈局AndroidView
- 原始碼解析Android中View的measure量算過程原始碼AndroidView
- Android中佈局的優化Android優化
- Android測量佈局繪製的起點Android
- self.view.frame的佈局問題View
- Android中View繪製優化二一---- 使用標籤複用佈局檔案AndroidView優化
- 自定義流式佈局:ViewGroup的測量與佈局View
- Android 佈局Android
- Android中常見的佈局和佈局引數Android
- CSS及佈局CSS
- 關於Android中xml佈局檔案之android 入門xml佈局檔案AndroidXML
- Android自定義View實現流式佈局(熱門標籤效果)AndroidView
- CSS > Flex 佈局中的放大和收縮計算CSSFlex
- Avalonia中的佈局
- 通俗理解Android中View的事件分發機制及滑動衝突處理AndroidView事件
- Android的佈局介紹Android
- Android佈局概述Android
- Android xml 佈局AndroidXML
- android佈局------RelativeLayout(相對佈局)詳解Android
- android筆記二(水平佈局與垂直佈局)Android筆記
- 寫給 Android 開發的小程式佈局指南,Flex 佈局!AndroidFlex
- Grid 佈局-子項補充及常用佈局
- Storyboard佈局中的坑
- 將xml佈局轉換成View的幾種方式XMLView
- Android的四個基本佈局Android
- Android學習—— Android佈局Android
- Android 佈局優化Android優化
- android 介面佈局(大概)Android
- Android FoldingLayout 摺疊佈局 原理及實現(一)Android
- Android 實現氣泡佈局/彈窗,可控制氣泡尖角方向及偏移量Android
- css佈局-實現左中右佈局的5種方式CSS