掌握 View 繪製流程能對視圖的各個繪製時機有更深入的認識,而且能寫出更好的自定義 View, 反正看源碼(SDK28)就完了。java
1、介紹canvas
2、源碼分析bash
3、總結數據結構
Activity 是經過 Window 與 View系統進行交互,而 Window 則是經過 ViewRootImpl 與 根View(DecorView)交互,View 最關鍵的三個步驟就是測量(measure)、佈局(layout)、繪製(draw), 最開始繪製的入口是 ViewRootImpl 類的 performTravesals 方法,下圖對總體流程作了個概述:異步
MeasureSpec: 這個關鍵對象貫穿在測量流程中,咱們能夠把它理解成一個 View 自身的「測量規格」, 它包含兩個變量一個是 mode(測量模式),另外一個是 size(測量尺寸)。源碼分析
我以爲源碼有一點設計的特別巧妙,但也很難理解,那就是用位操做來表示某個狀態值。這麼作的緣由是能節省更多的內存以及計算更快。MeasureSpec 是一個數據結構,可是它主要是用來製做一個 int 整型的變量,這個變量高 2 位表示測量模式,低 30 位表示測量尺寸,這是根據模式的數量決定的,總共就三種模式,所以用兩位就很夠了,如 01000000000000000000001111010101 粗體即表示模式。兩個變量合併成一個變量了,看到這種方式簡直就像發現新大陸通常。。但不推薦本身寫代碼的時候用這種方式,由於別人不必定看得懂,可讀性差。。佈局
三種模式:post
LayoutParams: 佈局參數。每一個 View 都有自身的佈局參數,最最基礎的就是寬高,咱們平時最多見的就是設置width 和 height 爲 match_parent 或 wrap_content。而後不一樣的 LayoutParams 有不一樣的屬性,如 LinearLayout.LayoutParams 就增長了 margin 相關的屬性。性能
View 自身的 MeasureSpec 是由父視圖的 MeasureSpec 和 自身的 LayoutParams 一塊兒決定的,接着 View 根據自身的 MeasureSpec 來肯定自身測量後的寬/高。優化
從入口 ViewRootImpl.java 的 performTraversals 方法開始看,它調用 performMeasure 以前作了以下操做:
// ViewRootImpl.java
...
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
複製代碼
mWidth, mHeight 表示屏幕的寬高,lp.width, lp.height 表示 DecorView 的寬高屬性,對於 DecorView 來講其 width 和 height 都是 match_parent,所以它的尺寸就是屏幕的尺寸,看下 getRootMeasureSpec 方法作了啥:
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); break; case ViewGroup.LayoutParams.WRAP_CONTENT: // Window can resize. Set max size for root view. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); break; default: // Window wants to be an exact size. Force root view to be that size. measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); break; } return measureSpec; } 複製代碼
若佈局參數中的寬/高是 MATCH_PARENT, 那麼它最終獲得的「測量規格」的 mode 是 EXACTLY, size 是屏幕寬/高,MeasureSpec.makeMeasureSpec 方法就是合併了 mode 和 size, 製做了一個 measureSpec 變量;若佈局參數中的寬或高是 WRAP_CONTENT, 那麼它最終獲得的「測量規格」的 mode 是 AT_MOST, size 是屏幕寬/高,乍一看其實尺寸和 MATCH_PARENT 是同樣的,因此通常系統定義的控件或者咱們自定義 View 都會對 WRAP_CONTENT 進行處理,不然其實它的效果在大部分狀況下和 MATCH_PARENT 並沒有一致;如果其餘值(通常用戶提供了精確的大小),那麼它最終獲得的「測量規格」的 mode 是 EXACTLY, size 是用戶給定的值。
在求出 DecorView 的「測量規格」後,調用 performMeasure 方法,內部主要是調用了 DecorView 的 measure 方法。因爲 measure 方法用 final 修飾了,所以子類沒法重寫此方法,全部的視圖都統一通過 View 中的 measure 這個方法。
// View.java
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// 前半部分代碼主要作了優化,若寬高都不變的狀況下
// 或沒有強制從新佈局的標誌位,那就不從新 measure 了
...
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
複製代碼
能夠把 measure 方法看作是一個統一的測量入口,作了一些通用的事情,真正的測量是在 onMeasure 方法,這個方法是 View 提供給各個子類去實現的,這裏你們能自定義不少測量邏輯,如 LinearLayout 佈局容器就是經過此方法獲取垂直、水平線性佈局時自身的寬/高,反正總之就是一句話, measure 流程就是爲了求出自身測量後的寬/高,並保存下來。如今看下 View 默認的 onMeasure 實現:
// View.java
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
複製代碼
getSuggestedMinimumWidth 方法就是看下是否有背景,若是有就獲取背景的寬度,不然看下是否設置了 minWidth 屬性,getSuggestedMinimumHeight同理。在這裏直接就無視這兩個狀況吧,正常來講這個方法返回值是 0, 看下 getDefaultSize :
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
複製代碼
根據「測量規格」獲取測量模式和測量尺寸, 跳過 UNSPECIFIED 模式,當模式爲 AT_MOST 和 EXACTLY 時,最原始的 View 視圖不管是指定 match_parent 仍是 wrap_content 模式,最後的 size 都是「測量規格」的 size, 因此對於不重寫 onMeasure 方法的 View 來講,這兩個模式沒差異。setMeasuredDimension 也是一個 final 修飾的方法,任何視圖都統一將寬/高保存成全局變量以便以後使用。以上就是 View 默認的測量流程,下面看下 ViewGroup 自定義實現的 onMeasure 方法。
因爲 DecorView 繼承自 FrameLayout,所以接下來的流程其實會調用到 FrameLayout 中的 onMeasure, 不過本文不分析 FrameLayout ,而是分析比較經常使用的 LinearLayout 重寫的 onMeasure 方法,咱們只分析垂直方向的:
// LinearLayout.java
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
......
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
final int childHeight = child.getMeasuredHeight();
......
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
......
}
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState);
}
複製代碼
這裏不分析 weight 屬性,加上這個屬性就有點複雜了。首先遍歷子視圖,讓每一個子視圖都執行自身的 onMeasure 方法,這個過程在 measureChildBeforeLayout 方法內,一下子在分析。測量子 View 以後,child.getMeasuredHeight() 就能得到這一波測量後的高度了,mTotalLength 能夠看作是目前 child 在豎直方向累加的高度(包括padding, margin)。最後調用 setMeasuredDimension 表示此次測量結束,會記錄測量後的寬和高。measureChildBeforeLayout 內部會直接調用 measureChildWithMargins, 此方法是父容器測量子視圖的統一入口:
// ViewGroup.java
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製代碼
是否還記得以前說的 View 的「測量規格」是由父視圖的「測量規格」和自身的佈局參數決定的,這裏 childWidthMeasureSpec 就是經過 父視圖的「測量規格」+ 自身的佈局參數 + padding + margin + 已使用的寬/高 決定的。
// ViewGroup.java
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// 這是父容器的測量模式
int specMode = MeasureSpec.getMode(spec);
// 這是父容器的測量尺寸(寬/高)
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
// 父容器是精確模式 EXACTLY
case MeasureSpec.EXACTLY:
// 子視圖有一個精確的尺寸,那麼它的測量尺寸也就是這個大小,
// 而且指定它的模式爲 EXACTLY
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}
// 子視圖佈局的寬/高是 MATCH_PARENT,那麼它的大小就是父容器的大小,
// 而且指定它的模式爲 EXACTLY,這裏就能看出,通常精確值和 MATCH_PARENT 對應 EXACTLY
else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
}
// 子視圖佈局的寬度是 WRAP_CONTENT,那麼它的大小就是父容器的大小,
// 而且指定它的模式爲 AT_MOST,因此通常來講自定義View要重寫onMeasure。
else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent has imposed a maximum size on us // 父容器是最大模式 AT_MOST case MeasureSpec.AT_MOST: // 這裏的邏輯和父容器爲精確模式時徹底同樣, // 看起來子視圖指定了精確值就不受父容器的約束了 if (childDimension >= 0) { // Child wants a specific size... so be it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } // 和父容器精確模式相比,大小都是父容器的大小, // 測量模式跟隨父容器的模式。 else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size, but our size is not fixed. // Constrain child to not be bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } // 依然和父容器精確模式同樣 else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
......
// 最後製做一個子View自身的「測量規格」
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
複製代碼
上面的註釋寫的比較清晰了,總結下獲取子視圖 MeasureSpec 的過程:若是子 View 佈局參數的尺寸是精確值,那麼父容器的 mode 不會影響到子視圖,子視圖都是 EXACTLY 模式 + 精確值尺寸;若是子 View 的寬/高是 MATCH_PARENT, 那麼子視圖跟隨父容器模式 + 父容器尺寸;若是子 View 的寬/高是 WRAP_CONTENT,那麼子視圖是 AT_MOST 模式 + 父容器尺寸。
在得到子視圖的「測量規格」後直接調用子視圖的 measure 方法讓子視圖根據自身的 MeasureSpec 獲得測量後的寬高,這個流程和以前講解的又是同樣的。
到此爲止 LinearLayout 的 onMeasure 垂直方向大體的流程已經分析完畢。總結下流程:它會先遍歷全部子視圖,經過 LinearLayout 的 MeasureSpec 和子視圖的 LayoutParams 得出子視圖的 MeasureSpec,接着讓子視圖執行 measure 方法 ,計算子視圖測量後的寬/高。經過累加子視圖的高度,若是 LinearLayout 是 EXACTLY 模式那麼高度仍是自身的尺寸,若是 LinearLayout 是 AT_MOST 模式那麼對比子視圖高度總和取較小一方做爲 LinearLayout 的高度。同理,寬度也有這麼一個比較過程。關於 weight 屬性,最關鍵的實際上是它會讓子視圖 measure 兩次,稍微有點耗時。
舉個栗子,如今有一個佈局,LinearLayout 中嵌套一個 TextView 和 View 視圖,如下是圖解:
layout 和 measure 的流程是相似的,直接上源碼:
// ViewRootImpl.java
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
......
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
// 如下主要是對 requestLayout 處理,暫不深究。
......
}
複製代碼
host 就是 DecorView, 直接能夠看到 View.layout 方法,雖然說此方法沒被 final 修飾,但能夠看作統一入口,其餘子類貌似並無重寫此方法:
public void layout(int l, int t, int r, int b) {
.....
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
......
onLayout(changed, l, t, r, b);
......
}
複製代碼
先解釋下前半部分的代碼,這裏的 l, t, r, b 分別表示 自身左邊緣與父容器左邊緣的距離、自身上邊緣與父容器上邊緣的距離、自身右邊緣與父容器左邊緣的距離、自身下邊緣與父容器上邊緣的距離,根據這些值就能得出自身的寬度爲 r - l, 高度爲 b - t, 以及自身的四個頂點。 這裏比較重要的是 setFrame 方法,裏面用全局變量 mLeft, mTop, mRight, mBottom 分別記錄了 l, t, r, b, 這個時候它的寬/高算是真正的定下來了(注意 measure 階段的測量寬高不必定是最終寬高),而且 setFrame 內部調用了, onSizeChanged 方法,因而恍然大悟,怪不得寫自定義 View 的時候要在 onSizeChanged 內拿最終寬高。
接下來解釋下 layout 方法中的 onLayout 方法。View 類並無實現 onLayout,也就是說它徹底去讓子類去實現了,而且 ViewGroup 將此方法設爲抽象方法強制去實現,所以只要是父容器都得實現 onLayout 來控制子視圖的位置,而子視圖沒有特殊需求基本不須要去實現此方法。下面看下 LinearLayout 重寫的 onLayout 方法,一樣只看垂直方向:
void layoutVertical(int left, int top, int right, int bottom) {
......
for (int i = 0; i < count; i++) {
......
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
}
}
複製代碼
依然仍是省略了一堆代碼,只須要解釋關鍵的幾個變量。 childLeft 表示子視圖的左邊緣與父容器的左邊緣的距離,這個變量會被padding, margin, gravity 所影響。childTop 表示子視圖的上邊緣與父容器的上邊緣的距離,受到 padding, 已累加的高度影響(由於是垂直佈局)。childWidth 和 childHeight 分別是子視圖的測量後的寬/高。在 setChildFrame 方法中直接調用了 child.layout, 那麼 layout 事件繼續往子容器傳遞,過程和以前解釋的同樣。
對 layout 作個總結:layout 方法的四個參數決定了自身在父容器內的位置保存爲 mLeft, mTop, mRight, mBottom,此方法真正肯定了自身的最終寬高。而後若是是繼承 ViewGroup 的父容器,那麼會重寫 onLayout 方法對子視圖進行佈局肯定它們的位置,最後會調用到子視圖的 layout 方法,按這種步驟一直傳遞。
依然舉個栗子,,LinearLayout 中嵌套一個 TextView 和 View 視圖,如下是圖解:
performDraw 方法會調到 View 的 draw 方法,重點在於 onDraw 自身的繪製,這也是自定義 View 實現的最關鍵方法,其次是 dispatchDraw, 此方法在 ViewGroup 被重寫主要用來遍歷子視圖並調用它們的 draw 方法傳遞繪製事件:
public void draw(Canvas canvas) {
// 繪製背景
drawBackground(canvas);
// 繪製自身內容
onDraw(canvas);
// 遍歷子視圖讓它們繪製 draw
dispatchDraw(canvas);
// 畫裝飾(前景,滾動條)
onDrawForeground(canvas);
// 繪製默認焦點高亮
drawDefaultFocusHighlight(canvas);
}
複製代碼
draw 調用流程是比較清晰簡單的,但它真正的實現是很複雜的,這一塊是自定義 View 的關鍵部分,須要學不少東西呀。。不過從這裏能看出自定義 View 主要是重寫 onDraw 以及 onMeasure 方法,而自定義 ViewGroup 主要是重寫 onMeasure 以及 onLayout 方法。
用文字的形式表達下整個繪製流程:
整個繪製流程的入口是 ViewRootImpl.performTravesals 方法,繪製的前後順序是 measure, layout, draw.
performMeasure 經過計算得出 DecorView 的 MeasureSpec 而後調用其 measure 方法,此方法是 View 類的統一入口,主要是作了判斷是否要測量和佈局,若是須要則直接調用重寫的 onMeasure 方法(因繼承 ViewGroup 容器的佈局特性所決定的)根據 MeasureSpec 對自身進行測量得出寬/高。父容器會遍歷全部子視圖,根據自身的 MeasureSpec 和 子視圖的 LayoutParams 決定子視圖的 MeasureSpec, 並調用子視圖的 measure 方法傳遞測量事件,直到傳遞到整個 View 樹的葉子爲止。
performLayout 從 View 樹的頂端開始,依次向下調用 layout 方法來確認自身在父容器內的位置,這時最終的寬高被確認,而後調用重寫過的 onLayout 方法(根據佈局特性重寫)來確認全部子視圖的位置。
performDraw 也是按照前面測量和佈局的思路傳遞在整個 View 樹中,onDraw 繪製自身的內容是實現自定義View的最關鍵方法。
View 相關的常見問題:
最後推薦 ConstraintLayout,尚未真正去研究這個約束佈局,但它基本一層就能搞定一個佈局,還管你什麼層級的性能問題嗎?應該是完爆其餘佈局的。