《View的工作原理》筆記


[Disclaimer]: 以下是讀<Android開發藝術探索>Chapter4的筆記

4.1 ViewRoot和DecorView

ViewRoot對應ViewRootImpl類,是連接WindowManagerDecorView的紐帶,View的三大流程都是通過ViewRoot完成的。

WMS調用ViewRootImpl#performTraverals方法開始View的測量、布局、繪制流程。

measure: 測量寬高
layout: 確定位置
draw: 繪制在屏幕上

performTraversals的工作流程

理解這個圖:
WMS調用ViewRootImpl#performTraverals來執行ViewGroup的
performMeasure->measure->onMeasure, 然后ViewGroup的onMeasure里會調用子View的measure,重復下去。layout和draw的流程也類似。

好了,下面說下結果。
measure結束后可以通過getMeasuredWidth和getMeasureHeight獲取View測量后的寬高,一般就是View的最終寬高。

layout完成后可以調用getTop等方法獲取四個角的位置。

draw完成后View才會顯示在屏幕上。


關于DecorView:DecorView是一個FrameLayout,不多說了,本文前面的圖吧。

4.2 理解MeasureSpec

4.2.1 MeasureSpec概念

Documentation MeasureSpec in View

MeasureSpec是View的靜態內部類。
是一個32 bit的int值,高2位代表SpecMode,低30位為代表SpecSize。
MeasureSpec囊括了從Parent到child的layout requirements。
每個MeasureSpec代表一個寬或高的需求。
一個MeasureSpec由size和mode組成。有三種可能的modes:
UNSPECIFIED
parent沒有給child施加任何限制。它可以是任何size。
(一般用于系統內部,表示測量狀態)

EXACTLY
parent為child決定了精確的size。
(對應于LayoutParams的match_parent和具體數值)

AT_MOST
child可以任何大小,最大不超過指定size。
((對應于LayoutParams的wrap_content)

MeasureSpec被是現成ints來減少對象分配。這個類用來pack和unpack<size, mode>這樣的tuple到int型。

4.2.2 MeasureSpec和LayoutParams的關系

首先,系統根據MeasureSpec來進行Measure過程。
MeasureSpec怎么來?
LayoutParams決定著MeasureSpec。但MeasureSpec不是僅僅由LayoutParams決定的,需要和父容器一起才能決定View的MeasureSpec。

View Measure的時候,系統將LayoutParams在parent約束下轉換成MeasureSpec,再根據MeasureSpec確定測量后的寬/高。

子元素的MeasureSpec的創建與父容器的MeasureSpec和View的padding、margin有關。

4.3 View的工作流程

4.3.1 Measure的工作過程

這邊分成兩個部分,View和ViewGroup的Measure。前者只是Measure一次,后者要遞歸Measure。具體細節不講了。
提一點:ViewGroup是抽象類,所以onMeasure是由子類實現的。
比如LinearLayout跟RelativeLayout的onMeasure是就是不同的,LinearLayout要計算totalLength嘛。


Measure完成后可通過getMeasuredHeight/Widht獲取寬高,極端情況下系統需要多次Measure才能確定寬高,所以最好在onLayout中去獲取寬高。

下面說常見的一個問題,就是在Activity啟動的時候就測量寬高,但是在onCreate, onStart, onResume里都是無法獲取寬高的,具體我前面也分析過了,ActivityThread#handleResumeActivity才會執行onResume,WMS才會調用ViewRootImpl#performTraversals。而且View的measure過程和Activity的生命周期不是同步執行的。如果View還沒有測量完,那獲得的寬高就是0。有4中方法解決這個問題:

  1. Activity/View#onWindowFocusChanged
    這個方法意思是View已經初始化完畢了。但是這個方法在Activity的窗口得到和失去焦點都會被調用一次。不過這個回調函數里有個參數是boolean的hasFocus,所以可以在它為true的時候獲取寬高。

  2. view.post(runnable)
    通過post可以將runnable投遞到消息隊列尾部(跟handler一樣),然后等待Looper調用這個Runnable的時候,View已經初始化好了。

  3. ViewTreeObserver

ViewTreeObserver observer = view.getViewTreeObserver();

會 return mAttachInfo.mTreeObserver;
然后往observer上add各種listener:


ViewTreeObserver#add

比如onGlobalLayoutListener,會在View Tree狀態改變或是內部可見性改變的時候回調。

  1. view.measure(int WidthMeasureSpec, int HeightMeasureSpec)
    手動對View進行measure,比較復雜,需要根據LayoutParams分情況討論。

4.3.2 layout過程

Layout的作用是ViewGroup確定子元素位置。先是確定ViewGroup的位置,然后會在onLayout里遍歷調用子元素的layout,然后onLayout又被調用。layout確定view本身位置,onLayout確定子元素位置。

步驟大概如下:
setFrame確定4個頂點的位置,然后調用ViewGroup#onLayout。同樣是沒有實現的,跟具體布局有關。

getWidth(Height)和getMeasuredWidth(Height)的區別
姑且把前者叫作最終寬,后者叫作測量寬。它們的區別只是后者是在measure過程中賦值的,前者是在layout過程中賦值的。比如getWidth其實就是return mRight - mLeft。
除非覆寫onLayout然后搞些賦值(這種操作沒什么意義),否則一般來講測量寬和最終寬的值是一樣的。

4.3.3 draw(canvas)過程

將View繪制到圖片上。遵循如下幾步:
(1) background.draw(canvas) 繪制背景
(2) onDraw 繪制自己
(3) dispatchDraw 繪制children
(4) onDrawScrollBars 繪制裝飾


by DrunkPiano

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容