[Disclaimer]: 以下是讀<Android開發藝術探索>Chapter4的筆記
4.1 ViewRoot和DecorView
ViewRoot對應ViewRootImpl類,是連接WindowManager和DecorView的紐帶,View的三大流程都是通過ViewRoot完成的。
WMS調用ViewRootImpl#performTraverals方法開始View的測量、布局、繪制流程。
measure: 測量寬高
layout: 確定位置
draw: 繪制在屏幕上
理解這個圖:
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概念
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中方法解決這個問題:
Activity/View#onWindowFocusChanged
這個方法意思是View已經初始化完畢了。但是這個方法在Activity的窗口得到和失去焦點都會被調用一次。不過這個回調函數里有個參數是boolean的hasFocus,所以可以在它為true的時候獲取寬高。view.post(runnable)
通過post可以將runnable投遞到消息隊列尾部(跟handler一樣),然后等待Looper調用這個Runnable的時候,View已經初始化好了。ViewTreeObserver
ViewTreeObserver observer = view.getViewTreeObserver();
會 return mAttachInfo.mTreeObserver;
然后往observer上add各種listener:
比如onGlobalLayoutListener,會在View Tree狀態改變或是內部可見性改變的時候回調。
- 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