簡介
我們知道,在 Android 中,View 繪制主要包含 3 大流程:
measure(測量):主要用于確定 View 的測量寬/高。
layout(布局):主要用于確定 View 在父容器中的放置位置。
draw(繪制):結合前面兩步結果,將 View 真正繪制到屏幕上。
Android 中,主要有兩種視圖:View
和ViewGroup
,其中:
-
View
:就是一個獨立的視圖 -
ViewGroup
:一個容器組件,該容器可容納多個子視圖,即ViewGroup
可容納多個View
或ViewGroup
,且支持嵌套。
雖然ViewGroup
繼承于View
,但是在 View 繪制三大流程中,某些流程需要區分View
和ViewGroup
,它們之間的操作并不完全相同,比如:
-
View
和ViewGroup
都需要進行 measure,確定各自的測量寬/高。View
只需直接測量自身即可,而ViewGroup
通常都必須先測量所有子View,最后才能測量自己 - 通常
ViewGroup
先定位自己的位置(layout
),然后再定位其子View 位置(onLayout
) -
View
需要進行 draw 過程,而ViewGroup
通常不需要(當然也可以進行繪制),因為ViewGroup
更多作為容器存在,起存儲放置功能
measure 流程
對 View 進行測量,主要包含兩個步驟:
- 求取 View 的測量規格
MeasureSpec
。 - 依據上一步求得的
MeasureSpec
,對 View 進行測量,求取得到 View 的最終測量寬/高。
MeasureSpec
對于第一個步驟,即求取 View 的MeasureSpec
,首先我們來看下MeasureSpec
的源碼定義:
// frameworks/base/core/java/android/view/View.java
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
// 生成測量規格
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
// 獲取測量模式
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
// 獲取測量大小
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
...
}
MeasureSpec
是View
的一個公有靜態內部類,它是一個 32 位的int
值,高 2 位表示 SpecMode(測量模式),低 30 位表示 SpecSize(測量尺寸/測量大小)。
MeasureSpec
將兩個數據打包到一個int
值上,可以減少對象內存分配,并且其提供了相應的工具方法可以很方便地讓我們從一個int
值中抽取出 View 的 SpecMode 和 SpecSize。
一個MeasureSpec
表達的是:該 View 在該種測量模式(SpecMode)下對應的測量尺寸(SpecSize)。其中,SpecMode 有三種類型:
UNSPECIFIED
:表示父容器對子View 未施加任何限制,子View 尺寸想多大就多大。EXACTLY
:如果子View 的模式為EXACTLY
,則表示子View 已設置了確切的測量尺寸,或者父容器已檢測出子View 所需要的確切大小。
這種模式對應于LayoutParams.MATCH_PARENT
和子View 設置具體數值兩種情況。AT_MOST
:表示自適應內容,在該種模式下,View 的最大尺寸不能超過父容器的 SpecSize,因此也稱這種模式為 最大值模式。
這種模式對應于LayoutParams.WRAP_CONTENT
。
LayoutParams
對 View 進行測量,最關鍵的一步就是計算得到 View 的MeasureSpec
,子View 在創建時,可以指定不同的LayoutParams
(布局參數),LayoutParams
的源碼主要內容如下所示:
// frameworks/base/core/java/android/view/ViewGroup.java
public static class LayoutParams {
...
/**
* Special value for the height or width requested by a View.
* MATCH_PARENT means that the view wants to be as big as its parent,
* minus the parent's padding, if any. Introduced in API Level 8.
*/
public static final int MATCH_PARENT = -1;
/**
* Special value for the height or width requested by a View.
* WRAP_CONTENT means that the view wants to be just large enough to fit
* its own internal content, taking its own padding into account.
*/
public static final int WRAP_CONTENT = -2;
/**
* Information about how wide the view wants to be. Can be one of the
* constants FILL_PARENT (replaced by MATCH_PARENT
* in API Level 8) or WRAP_CONTENT, or an exact size.
*/
public int width;
/**
* Information about how tall the view wants to be. Can be one of the
* constants FILL_PARENT (replaced by MATCH_PARENT
* in API Level 8) or WRAP_CONTENT, or an exact size.
*/
public int height;
...
}
其中:
-
LayoutParams.MATCH_PARENT
:表示子View 的尺寸與父容器一樣大(注:需要減去父容器padding
部分空間,讓父容器padding
生效) -
LayoutParams.WRAP_CONTENT
:表示子View 的尺寸自適應其內容大小(注:需要包含子View 本身的padding
空間) -
width
/height
:表示 View 的設置寬/高,即layout_width
和layout_height
設置的值,其值有三種選擇:LayoutParams.MATCH_PARENT
、LayoutParams.WRAP_CONTENT
和 具體數值。
LayoutParams
會受到父容器的MeasureSpec
的影響,測量過程會依據兩者之間的相互約束最終生成子View 的MeasureSpec
,完成 View 的測量規格。
簡而言之,View 的MeasureSpec
受自身的LayoutParams
和父容器的MeasureSpec
共同決定(DecorView
的MeasureSpec
是由自身的LayoutParams
和屏幕尺寸共同決定,參考后文)。也因此,如果要求取子View 的MeasureSpec
,那么首先就需要知道父容器的MeasureSpec
,層層逆推而上,即最終就是需要知道頂層View(即DecorView
)的MeasureSpec
,這樣才能一層層傳遞下來,這整個過程需要結合Activity
的啟動過程進行分析。
Activity 視圖基本結構
我們知道,在 Android 中,Activity
是作為視圖組件存在,主要就是在手機上顯示視圖界面,可以供用戶操作,Activity
就是 Andorid 中與用戶直接交互最多的系統組件。
Activity
的基本視圖層次結構如下所示:
Activity
中,實際承載視圖的組件是Window
(更具體來說為PhoneWindow
),頂層View 是DecorView
,它是一個FrameLayout
,DecorView
內部是一個LinearLayout
,該LinearLayout
由兩部分組成(不同 Android 版本或主題稍有差異):TitleView
和ContentView
,其中,TitleView
就是標題欄,也就是我們常說的TitleBar
或ActionBar
,ContentView
就是內容欄,它也是一個FrameLayout
,主要用于承載我們的自定義根布局,即當我們調用setContentView(...)
時,其實就是把我們自定義的布局設置到該ContentView
中。
當Activity
啟動完成后,最終就會渲染出上述層次結構的視圖。
DecorView 測量規格
因此,如果我們要求取得到子View 的MeasureSpec
,那么第一步就是求取得到頂層View(即DecorView
)的MeasureSpec
。大致過程如下所示:
-
在
Activity
啟動過程中,會調用到ActivityThread.handleResumeActivity(...)
,該方法就是 View 視圖繪制的起始之處:// frameworks/base/core/java/android/app/ActivityThread.java final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) { ... ActivityClientRecord r = performResumeActivity(token, clearHide); ... // 此處的 window 為與 Activity 綁定的 PhoneWindow,即 Activity.mWindow r.window = r.activity.getWindow(); // PhoneWindow 綁定的頂層視圖:DecorView View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); // 獲取與 Activity 綁定的 WindowManager,實際上是 PhoneWindow 的 WindowManager ViewManager wm = a.getWindowManager(); WindowManager.LayoutParams l = r.window.getAttributes(); ... // 添加 DecorView 到 PhoneWindow 上(相當于設置 Activity 根視圖) wm.addView(decor, l); ... }
其中,
r.window.getDecorView()
實際調用的是PhoneWindow.getDecorView()
,其會返回頂層DecorView
(不存在時會自動實例化):// frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java public class PhoneWindow extends Window implements MenuBuilder.Callback { // This is the top-level view of the window, containing the window decor. private DecorView mDecor; ... @Override public final View getDecorView() { if (mDecor == null) { installDecor(); } return mDecor; } private void installDecor() { if (mDecor == null) { mDecor = generateDecor(); ... } ... } protected DecorView generateDecor() { // 實例化 DecorView return new DecorView(getContext(), -1); } ... }
然后,
r.window.getAttributes()
實際調用的是Window.getAttributes()
:// frameworks/base/core/java/android/view/Window.java public abstract class Window { private final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams(); ... public final WindowManager.LayoutParams getAttributes() { return mWindowAttributes; } } // frameworks/base/core/java/android/view/WindowManager.java public interface WindowManager extends ViewManager { ... public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable { public LayoutParams() { // DecorView 的布局參數為 MATCH_PARENT super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); ... } } }
這里可以看到,此處
r.window.getAttributes()
返回的是一個WindowManager.LayoutParams
實例,對應的最終寬/高布局參數為LayoutParams.MATCH_PARENT
,最后通過wm.addView(decor,l)
將DecorView
添加到WindowManager
上(最終其實是設置到ViewRootImpl
上),所以DecorView
的布局參數為MATCH_PARENT
。 -
View 的繪制流程真正開始的地方為
ViewRootImpl.performTraversals()
,在其中,有如下代碼片段:// frameworks/base/core/java/android/view/ViewRootImpl.java private void performTraversals() { ... int desiredWindowWidth; int desiredWindowHeight; ... // Ask host how big it wants to be windowSizeMayChange |= measureHierarchy(host, lp, res, desiredWindowWidth, desiredWindowHeight); ... } private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp, final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) { ... childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); ... }
此處的
desiredWindowWidth
和desiredWindowHeight
是屏幕的尺寸,內部最終會調用到ViewRootImpl.getRootMeasureSpec(...)
,其源碼如下所示:// frameworks/base/core/java/android/view/ViewRootImpl.java 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; }
ViewRootImpl.getRootMeasureSpec(...)
見名知意,其實就是用來獲取頂層View(即DecorView
)的MeasureSpec
,其邏輯如下:- 當
DecorView
的LayoutParams
為MATCH_PARENT
時,說明DecorView
的大小與屏幕一樣大,而又由于屏幕大小是確定的,因此,其 SpecMode 為EXACTLY
,SpecSize 為windowSize
,; - 當
DecorView
的LayoutParams
為WRAP_CONTENT
時,說明DecorView
自適應內容大小,因此它的大小不確定,但是最大不能超過屏幕大小,故其 SpecMode 為AT_MOST
,SpecSize 為windowSize
; - 其余情況為
DecorView
設置了具體數值大小或UNSPECIFIED
,故以DecorView
為主,其 SpecMode 為EXACTLY
,SpecSize 就是自己設置的值,即rootDimension
;
結合我們上面的分析,由于
DecorView
的LayoutParams
為MATCH_PARENT
,因此,DecorView
的MeasureSpec
最終為:MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY)
,即DecorView
的 SpecMode 為EXACTLY
,SpecSize 為屏幕大小。 - 當
默認測量(measure)
經過上述步驟求取得到 View 的MeasureSpec
后,接下來就可以真正對 View 進行測量,求取 View 的最終測量寬/高:
Android 內部對視圖進行測量的過程是由View#measure(int, int)
方法負責的,但是對于View
和ViewGroup
,其具體測量過程有所差異。
因此,對于測量過程,我們分別對View
和ViewGroup
進行分析:
-
View
測量:View
的測量過程由View.measure(...)
方法負責,其源碼如下所示:// frameworks/base/core/java/android/view/View.java public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ... // measure ourselves, this should set the measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec); ... }
View#measure(int, int)
中參數widthMeasureSpec
和heightMeasureSpec
是由父容器傳遞進來的,具體的測量過程請參考后文內容。需要注意的是,
View#measure(int, int)
是一個final
方法,因此其不可被覆寫,實際真正測量 View 自身使用的是View#onMeasure(int, int)
方法,如下所示:// frameworks/base/core/java/android/view/View.java protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
onMeasure(...)
主要做了三件事:-
首先通過
getSuggestedMinimumWidth()
/getSuggestedMinimumHeight()
方法獲取得到 View 的推薦最小測量寬/高:// frameworks/base/core/java/android/view/View.java protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); } protected int getSuggestedMinimumHeight() { return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight()); }
這兩個方法的實現原理是一致的,這里就只分析
getSuggestedMinimumWidth()
方法實現,該方法內部是一個三目運算符,可以很清晰看出,當 View 沒有設置背景時,它的寬度就為mMinWidth
,mMinWidth
就是android:minWidth
這個屬性對應設置的值(未設置android:minWidth
時,其值默認為0
),當 View 設置了背景時,它的寬度就是mMinWidth
和mBackground.getMinimumWidth()
之中的較大值,其中,mBackground.getMinimumWidth()
源碼如下:// frameworks/base/graphics/java/android/graphics/drawable/Drawable.java /* * @return The minimum width suggested by this Drawable. If this Drawable * doesn't have a suggested minimum width, 0 is returned. */ public int getMinimumWidth() { final int intrinsicWidth = getIntrinsicWidth(); return intrinsicWidth > 0 ? intrinsicWidth : 0; } // 不同子類可實現具體大小 public int getIntrinsicWidth() { return -1; }
Drawable.getMinimumWidth()
就是返回 Drawable 的原始寬度,如果該 Drawable 未設置寬度,則返回0
。綜上,
getSuggestedMinimumWidth()
/getSuggestedMinimumHeight()
其實就是用于獲取 View 的最小測量寬/高,其具體邏輯為:當 View 沒有設置背景時,其最小寬/高為android:minWidth
/android:mMinHeight
所指定的值,當 View 設置了背景時,其最小測量寬/高為android:minWidth
/android:minHeight
與其背景圖片寬/高的較大值。簡而言之,View 的最小測量寬/高為
android:minWidth
/android:minHeight
和其背景寬/高之間的較大值。 -
通過
getDefaultSize(...)
獲取到 View 的默認測量寬/高,具體獲取過程如下所示:// frameworks/base/core/java/android/view/View.java 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; }
此處的
size
是通過getSuggestedMinimumWidth()
/getSuggestedMinimumHeight()
方法獲取得到系統建議 View 的最小測量寬/高。參數
measureSpec
是經由View.measure(...)
->View.onMeasure(...)
->View.getDefaultSize(...)
調用鏈傳遞進來的,表示的是當前 View 的MeasureSpec
。getDefaultSize(...)
內部首先會獲取 View 的測量模式和測量大小,然后當 View 的測量模式為UNSPECIFIED
時,也即未限制 View 的大小,因此此時 View 的大小就是其原生大小(也即android:minWidth
或背景圖片大小),當 View 的測量模式為AT_MOST
或EXACTLY
時,此時不對這兩種模式進行區分,一律將 View 的大小設置為測量大小(即 SpecSize)。
注:實際上,這里可以看到,默認情況下,View 不區分AT_MOST
和EXACTLY
,也即,當自定義 View 時,LayoutParams.WRAP_CONTENT
和LayoutParams.MATCH_PARENT
效果是一樣的,均為MATCH_PARENT
的效果,原因是 子View 的MeasureSpec
是由父容器傳遞進來的,父容器是通過ViewGroup#getChildMeasureSpec(...)
方法獲取得到 子View 的MeasureSpec
,在該方法內部,子View 的測量模式無論是AT_MOST
或是EXACTLY
,其測量大小都為父容器大小(確定的說,是父容器剩余空間大小),因此其效果就等同于MATCH_PARENT
,具體源碼詳情分析請參考后文。總之,一般自定義 View 時,都需要覆寫
onMeasure(...)
,并為其LayoutParams.WRAP_CONTENT
設置一個默認大小,如下所示:@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 先進行默認測量 super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 默認大小依據自己靈活配置,這里為 400px int defaultSize = 400; // 獲取默認測量寬/高 int width = this.getMeasuredWidth(); int height = this.getMeasuredHeight(); // 獲取 View 的布局參數 ViewGroup.LayoutParams lp = this.getLayoutParams(); // 寬度為自適應,則設置一個默認大小 if(lp.width == ViewGroup.LayoutParams.WARP_CONTENT) { width = defaultSize; } // 高度為自適應,則設置一個默認大小 if(lp.height == ViewGroup.LayoutParams.WARP_CONTENT) { height = defaultSize; } this.setMeasuredDimension(width, height); }
-
獲取到 View 的測量寬/高后,通過
setMeasuredDimension(...)
記錄 View 的測量寬/高:// frameworks/base/core/java/android/view/View.java protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { ... setMeasuredDimensionRaw(measuredWidth, measuredHeight); } // 記錄測量寬/高 private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; }
setMeasuredDimension(...)
其實就是將 View 的最終測量寬/高設置到View.mMeasuredWidth
/View.mMeasuredHeight
屬性中,完成測量過程。
-
-
ViewGroup
測量:ViewGroup
是一個抽象類,其繼承于View
:public abstract class ViewGroup extends View implements ViewParent, ViewManager {...}
ViewGroup
的測量過程也是由View.measure(...)
負責,因此實際負責測量的是ViewGroup.onMeasure(...)
方法,但是由于ViewGroup
的作用是用于容納子View,如果想測量ViewGroup
,則必須先測量其子View,而又由于不同的ViewGroup
有不同的布局特性,因此無法抽象出一套標準的測量流程,所以ViewGroup
本身沒有覆寫onMeasure(...)
方法(交由具體自定義ViewGroup
覆寫),但是它提供了一些測量子View 的輔助方法,比如:measureChildren(...)
、measureChildrenWithMargins(...)
、measureChild(...)
、getChildMeasureSpec(...)
等等,自定義ViewGroup
可借助這些輔助方法,在onMeasure(...)
中完成子View 的測量,然后最終才能完成自己的測量。我們隨便選擇一個輔助方法,比如
ViewGroup#measureChildWithMargins(...)
,查看其源碼:// android/view/ViewGroup.java protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { // 獲取 子View 的 LayoutParams final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); // 獲取 子View 的 MeasureSpec // 父容器已使用的空間為:自身已使用空間 + 自身的 padding + 子View的 margin final int childWidthMeasureSpec = 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); // 測量子View child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
代碼非常簡潔易懂,其核心就是先獲取得到 子View 的
MeasureSpec
(getChildMeasureSpec(...)
),然后就可以對 子View 進行測量(child.measure(...)
)。View#measure(...)
的測量詳情上述我們已經介紹過了,這里我們主要來看下ViewGroup#getChildMeasureSpec(...)
獲取 子View 測量規格的具體過程:// android/view/ViewGroup.java /** * * @param spec 父容器的 MeasureSpec * @param padding 父容器已使用的空間(比如:父View自身的 padding + 子View的 margin) * @param childDimension 子View的 LayoutParams * @return 子View 的 MeasureSpec */ public static int getChildMeasureSpec(int spec, int padding, int childDimension) { // 當前View(即父容器)的測量模式 int specMode = MeasureSpec.getMode(spec); // 父容器的測量大小 int specSize = MeasureSpec.getSize(spec); // 父容器剩余可用空間 int size = Math.max(0, specSize - padding); // 子View 最終測量大小 int resultSize = 0; // 子View 最終測量模式 int resultMode = 0; switch (specMode) { // Parent has imposed an exact size on us // 父容器大小已確定 case MeasureSpec.EXACTLY: if (childDimension >= 0) { // 子View 設置了具體大小(精確數值) resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // 子View 大小撐滿父容器 // Child wants to be our size. So be it. resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // 子View 自適應內容大小 // 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 // 父容器自適應內容大小 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; // Parent asked to see how big we want to be // 父容器大小無限制 case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // Child wants a specific size... let him have it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size... find out how big it should // be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } // 子View 的最終測量規格 return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
getChildMeasureSpec(...)
其實就是ViewGroup
對其內部 子View 的默認測量過程,其核心邏輯為:-
如果父容器的測量模式為
EXACTLY
:即父容器測量大小是確切的,且其剩余空間精確為size
,此時:-
如果 子View 的
LayoutParams
為具體數值:則表示 子View 已明確設置了具體大小,因此,此時 子View 的測量大小即為自己設置的值,即childDimension
,測量模式為EXACTLY
。 -
如果 子View 的
LayoutParams
為MATCH_PARENT
:表示 子View 的大小撐滿父容器,由于父容器是EXACTLY
,即大小已知,因此,子View 也是大小已知,故其測量模式為EXACTLY
,且其測量大小就是父容器剩余空間大小,具體為size
。 -
如果 子View 的
LayoutParams
為WRAP_CONTENT
:表示 子View 自適應內容大小,但是其尺寸最大不能超過父容器剩余空間,因此其測量模式為AT_MOST
,測量大小為父容器剩余空間size
。
-
如果 子View 的
-
如果父容器的測量模式為
AT_MOST
:即父容器自適應其內容大小,也即父容器大小不確定,此時:-
如果 子View 的
LayoutParams
為具體數值:則表示 子View 已明確設置了具體大小,因此,此時 子View 的測量大小即為自己設置的值,即childDimension
,測量模式為EXACTLY
。 -
如果 子View 的
LayoutParams
為MATCH_PARENT
:表示 子View 的大小撐滿父容器,由于父容器是AT_MOST
,即大小未知,因此,子View 也是大小未知,即其測量模式為AT_MOST
,且其測量大小不超過父容器剩余空間大小size
。 -
如果 子View 的
LayoutParams
為WRAP_CONTENT
:表示 子View 自適應內容大小,但是其尺寸最大不能超過父容器剩余空間,因此其測量模式為AT_MOST
,測量大小為父容器剩余空間size
。
-
如果 子View 的
-
如果父容器的測量模式為
UNSPECIFIED
:即父容器大小無限制,此時:-
如果 子View 的
LayoutParams
為具體數值:則表示 子View 已明確設置了具體大小,因此,此時 子View 的測量大小即為自己設置的值,即childDimension
,測量模式為EXACTLY
。 -
如果 子View 的
LayoutParams
為MATCH_PARENT
:表示 子View 的大小撐滿父容器,由于父容器大小無限制,因此,子View 的大小也是無限制的,所以,子View 的測量模式為UNSPECIFIED
,測量大小未知,通常設置為0
,表示無限。 -
如果 子View 的
LayoutParams
為WRAP_CONTENT
:表示 子View 自適應內容大小,由于父容器大小無限制,因此,子View 的測量大小也是無限制的,所以其模式為UNSPECIFIED
,測量大小無限,通常使用0
進行表示。
-
如果 子View 的
上述的邏輯總結如下圖所示:(注:圖片來源于互聯網,侵刪)
ViewGroup#getChildMeasureSpec注:前面我們一直強調:子View 的
MeasureSpec
是由其LayoutParams
和父容器的MeasureSpec
共同約束構造而成,其實這部分邏輯就是ViewGroup#getChildMeasureSpec(...)
方法負責的,可以很清晰看到,子View 的MeasureSpec
就是在父容器MeasureSpec
約束下,與其自身LayoutParams
共同協商決定的。 -
綜上,無論是對View
的測量還是ViewGroup
的測量,都是由View#measure(int widthMeasureSpec, int heightMeasureSpec)
方法負責,然后真正執行 View 測量的是 View 的onMeasure(int widthMeasureSpec, int heightMeasureSpec)
方法。
具體來說,View
直接在onMeasure(...)
中測量并設置自己的最終測量寬/高。在默認測量情況下,View
的測量寬/高由其父容器的MeasureSpec
和自身的LayoutParams
共同決定,當View
自身的測量模式為LayoutParams.UNSPECIFIED
時,其測量寬/高為android:minWidth
/android:minHeight
和其背景寬/高之間的較大值,其余情況皆為自身MeasureSpec
指定的測量尺寸。
而對于ViewGroup
來說,由于布局特性的豐富性,只能自己手動覆寫onMeasure(...)
方法,實現自定義測量過程,但是總的思想都是先測量 子View 大小,最終才能確定自己的測量大小。
layout 流程
當確定了 View 的測量大小后,接下來就可以來確定 View 的布局位置了,也即將 View 放置到屏幕具體哪個位置。
View layout
View 的布局過程由View#layout(...)
負責,其源碼如下:
// android/view/View.java
/**
* @param l Left position, relative to parent
* @param t Top position, relative to parent
* @param r Right position, relative to parent
* @param b Bottom position, relative to parent
*/
@SuppressWarnings({"unchecked"})
public void layout(int l, int t, int r, int b) {
...
setFrame(l, t, r, b);
...
onLayout(changed, l, t, r, b);
...
}
View#layout(...)
主要就做了兩件事:
-
setFrame(...)
:首先通過View#setFrame(...)
來確定自己的布局位置,其源碼如下:// android/view/View.java protected boolean setFrame(int left, int top, int right, int bottom) { ... // Invalidate our old position invalidate(sizeChanged); mLeft = left; mTop = top; mRight = right; mBottom = bottom; }
setFrame(...)
其實就是更新記錄 View 的四個頂點位置,這樣 View 在父容器中的坐標位置就確定了。 -
onLayout(...)
:setFrame(...)
是用于確定 View 自身的布局位置,而onLayout(...)
主要用于確定 子View 的布局位置:protected void onLayout(boolean changed, int left, int top, int right, int bottom) { }
由于 View 不包含子組件,因此其
onLayout
是一個空實現。
ViewGroup layout
ViewGroup 的布局流程由ViewGroup#layout(...)
負責,其源碼如下:
// android/view/ViewGroup.java
@UiThread
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
...
@Override
public final void layout(int l, int t, int r, int b) {
...
super.layout(l, t, r, b);
...
}
可以看到,ViewGroup#layout(...)
最終也是通過View#layout(...)
完成自身的布局過程,一個注意的點是,ViewGroup#layout(...)
是一個final
方法,因此子類無法覆寫該方法,主要是ViewGroup#layout(...)
方法內部對子視圖動畫效果進行了相關設置。
由于ViewGroup#layout(...)
內部最終調用的還是View#layout(...)
,因此,ViewGroup#onLayout(...)
就會得到回調,用于處理 子View 的布局放置,其源碼如下:
// android/view/ViewGroup.java
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
由于不同的ViewGroup
,其布局特性不同,因此ViewGroup#onLayout(...)
是一個抽象方法,交由ViewGroup
子類依據自己的布局特性,擺放其 子View 的位置。
draw 流程
當 View 的測量大小,布局位置都確定后,就可以最終將該 View 繪制到屏幕上了。
View 的繪制過程由View#draw(...)
方法負責,其源碼如下:
// android/view/View.java
public void draw(Canvas canvas) {
...
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
drawBackground(canvas);
// skip step 2 & 5 if possible (common case)
...
// Step 2, save the canvas' layers
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
if (drawBottom) {
canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
}
if (drawLeft) {
canvas.saveLayer(left, top, left + length, bottom, null, flags);
}
if (drawRight) {
canvas.saveLayer(right - length, top, right, bottom, null, flags);
}
...
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
...
if (drawTop) {
...
canvas.drawRect(left, top, right, top + length, p);
}
if (drawBottom) {
...
canvas.drawRect(left, bottom - length, right, bottom, p);
}
if (drawLeft) {
...
canvas.drawRect(left, top, left + length, bottom, p);
}
if (drawRight) {
...
canvas.drawRect(right - length, top, right, bottom, p);
}
...
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
}
其實注釋已經寫的很清楚了,View#draw(...)
主要做了以下 6 件事:
繪制背景:
drawBackground(...)
如果有必要的話,保存畫布圖層:
Canvas.saveLayer(...)
-
繪制自己:
onDraw(...)
,其源碼如下:// android/view/View.java protected void onDraw(Canvas canvas) { }
View#onDraw(...)
是一個空方法,因為每個 View 的繪制都是不同的,自定義 View 時,通常會覆寫該方法,手動繪制該 View 內容。 -
繪制子View:
dispatchDraw(...)
,其源碼如下:// android/view/View.java protected void dispatchDraw(Canvas canvas) { }
由于 View 沒有子元素,因此其
dispatchDraw
是一個空實現。查看下
ViewGroup#dispatchDraw(...)
,其源碼如下:// android/view/ViewGroup.java @Override protected void dispatchDraw(Canvas canvas) { ... final int childrenCount = mChildrenCount; final View[] children = mChildren; ... for (int i = 0; i < childrenCount; i++) { ... more |= drawChild(canvas, child, drawingTime); ... } ... } protected boolean drawChild(Canvas canvas, View child, long drawingTime) { return child.draw(canvas, this, drawingTime); }
可以看到,其內部主要就是遍歷子View,最后通過
child.draw(...)
讓子View自己進行繪制。 如果有必要的話,繪制淡化效果并恢復圖層:
Canvas.drawRect(...)
-
繪制裝飾:
onDrawForeground(...)
,其源碼如下:// android/view/View.java public void onDrawForeground(Canvas canvas) { onDrawScrollIndicators(canvas); onDrawScrollBars(canvas); final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null; ... foreground.draw(canvas); } }
其實主要就是繪制滾動條,前景圖片等視圖相關的裝飾。
繪制起始流程
我們知道,在Activity
啟動過程中,會調用到ActivityThread.handleResumeActivity(...)
,該方法就是 View 視圖繪制的起始之處:
// frameworks/base/core/java/android/app/ActivityThread.java
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume) {
...
// 回調 Activity.onResume() 方法
ActivityClientRecord r = performResumeActivity(token, clearHide);
...
// 獲取當前 Activity 實例
final Activity a = r.activity;
...
// 此處的 window 為與 Activity 綁定的 PhoneWindow,即 Activity.mWindow
r.window = r.activity.getWindow();
// PhoneWindow 綁定的頂層視圖:DecorView
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
// 獲取與 Activity 綁定的 WindowManager,實際上是 PhoneWindow 的 WindowManager
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
...
// 添加 DecorView 到 PhoneWindow 上(相當于設置 Activity 根視圖)
wm.addView(decor, l);
...
}
可以看到,ActivityThread.handleResumeActivity(...)
主要就是獲取到當前Activity
綁定的ViewManager
,最后調用ViewManager.addView(...)
方法將DecorView
設置到PhoneWindow
上,也即設置到當前Activity
上。ViewManager
是一個接口,WindowManager
繼承ViewManager
,而WindowManagerImpl
實現了接口WindowManager
,此處的ViewManager.addView(...)
實際上調用的是WindowManagerImpl.addView(...)
,源碼如下所示:
// frameworks/base/core/java/android/view/WindowManagerImpl.java
public final class WindowManagerImpl implements WindowManager {
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
...
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
...
}
WindowManagerImpl.addView(...)
內部轉發到WindowManagerGlobal.addView(...)
:
// frameworks/base/core/java/android/view/WindowManagerGlobal.java
public final class WindowManagerGlobal {
...
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
ViewRootImpl root;
...
// 實例化一個 ViewRootImpl
root = new ViewRootImpl(view.getContext(), display);
...
// 將 ViewRootImpl 與 DecorView 關聯到一起
root.setView(view, wparams, panelParentView);
...
}
...
}
在WindowManagerGlobal.addView(...)
內部,會創建一個ViewRootImpl
實例,然后調用ViewRootImpl.setView(...)
將ViewRootImpl
與DecorView
關聯到一起:
// frameworks/base/core/java/android/view/ViewRootImpl.java
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
...
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
...
// 將 DecorView 綁定到 ViewRootImpl.mView 屬性上
mView = view;
...
mWindowAttributes.copyFrom(attrs);
...
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();
...
}
...
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
// 檢查是否處于主線程
checkThread();
...
scheduleTraversals();
}
}
...
}
ViewRootImpl.setView(...)
內部首先關聯了傳遞過來的DecorView
(通過屬性mView
指向DecorView
即可建立關聯),然后最終調用requestLayout()
,而requestLayout()
內部又會調用方法scheduleTraversals()
:
// frameworks/base/core/java/android/view/ViewRootImpl.java
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
...
Choreographer mChoreographer;
...
final class TraversalRunnable implements Runnable {
@Override
public void run() {
// 開始執行繪制
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
...
void scheduleTraversals() {
if (!mTraversalScheduled) { // 同一幀內不會多次調用遍歷
mTraversalScheduled = true;
// 發送一個同步屏障
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 將 UI 繪制任務發送到 Choreographer,回調觸發 mTraversalRunnable,執行繪制操作
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
...
}
}
...
void doTraversal() {
...
performTraversals();
...
}
...
}
ViewRootImpl.scheduleTraversals()
內部主要做了兩件事:
- 調用
MessageQueue.postSyncBarrier()
方法發送一個同步屏障,同步屏障可以攔截Looper
對同步消息的獲取與分發,即加入同步屏障后,此時Looper
只會獲取和處理異步消息,如果沒有異步消息,則進入阻塞狀態。 - 通過
Choreographer.postCallback(...)
發送一個Choreographer.CALLBACK_TRAVERSAL
的異步視圖渲染消息。因為前面已經發送了一個同步屏障,因此此處的視圖繪制渲染消息會優先被處理。
Choreographer.postCallback(...)
會申請一次 VSYNC 中斷信號,當 VSYNC 信號到達時,便會回調Choreographer.doFrame(...)
方法,內部會觸發已經添加的回調任務,Choreographer
的回調任務有以下四種類型:
// 回調 INPUT 任務
doCallbacks(Choreographer.CALLBACK_INPUT, mframeTimeNanos);
// 回調 ANIMATION
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
// 回調 View 繪制任務 TRAVERSAL
doCallbacks(Choreographer,CALLBACK_TRAVERSAL, frameTimeNanos);
// API Level 23 新增,COMMIT
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
因此,ViewRootImpl.scheduleTraversals(...)
內部通過mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null)
發送的異步視圖渲染消息就會得到回調,即回調mTraversalRunnable.run()
方法,最終會執行doTraversal()
方法,而doTraversal()
內部又會調用performTraversals()
方法,該方法才是真正開始執行 View 繪制流程的地方,其源碼如下所示:
// frameworks/base/core/java/android/view/ViewRootImpl.java
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
...
private void performTraversals() {
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
...
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...
performDraw();
...
}
...
}
綜上,performTraversals()
會依次調用performMeasure(...)
、performLayout(...)
和performDraw()
三個方法,這三個方法會依次完成頂層View(即DecorView
)的測量(measure
)、布局(layout
)和繪制(draw
)流程,具體詳情請參考后文。
到此,我們才真正進入 View 繪制流程,總結一下上述流程,如下圖所示:
performMeasure
書接前文,我們知道,真正開始 View 繪制流程是ViewRootImpl.performTraversals()
,該方法內部首先進行的是performMeasure(...)
流程:
// frameworks/base/core/java/android/view/ViewRootImpl.java
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
// 調用 DecorView.measure(...)
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
此處的mView
其實就是DecorView
,其賦值指向在ViewRootImpl.setView(...)
中進行,可以看到,performMeasure(...)
實際調用的是DecorView.measure(...)
,所以最終會回調DecorView#onMeasure(...)
方法,其源碼如下:
// frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder.Callback {
...
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
...
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
...
}
可以看到,DecorView#onMeasure(...)
內部將測量過程交由其父類,即FrameLayout
進行處理,那我們看下FrameLayout#onMeasure(...)
源碼:
// frameworks/base/core/java/android/widget/FrameLayout.java
public class FrameLayout extends ViewGroup {
...
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 獲取 子View 數量
int count = getChildCount();
...
// 最大高度
int maxHeight = 0;
// 最大寬度
int maxWidth = 0;
int childState = 0;
for (int i = 0; i < count; i++) {
// 獲取 子View
final View child = getChildAt(i);
// 只對可見的 子View 進行測量
if (mMeasureAllChildren || child.getVisibility() != GONE) {
// 測量子View
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
// 獲取 子View 的布局參數
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 獲取當前子View的寬度,包含其外邊距,記錄子View的最大寬度
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
// 記錄子View的最大高度
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
...
}
}
// Account for padding too
// 最大寬度包含前景偏移量:padding
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
// 最大高度包含前景偏移量:padding
maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
// Check against our minimum height and width
// 比較子View 和 系統建議的 子View 最小高度,獲取兩者中的較大值
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
// 比較子View 和 系統建議的 子View 最小寬度,獲取兩者中的較大值
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// Check against our foreground's minimum height and width
final Drawable drawable = getForeground();
if (drawable != null) {
// 子View 高度和 前景圖片高度比較,記錄其中較大值
maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
// 子View 高度和 前景圖片寬度比較,記錄其中較大值
maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
}
// 記錄測量結果
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
...
}
...
}
FrameLayout
的布局特性為:所有 子View 層疊在一起,所以FrameLayout
的測量寬/高就是其所有 子View 中最大的寬和高,因此FrameLayout#onMeasure(...)
的核心邏輯就是遍歷其所有子View,然后通過measureChildWithMargins(...)
(該方法前面內容已詳細介紹)測量子View,然后就可以獲取 子View 的寬/高,記錄其中最大的寬/高值,作為自己的測量寬/高。
經過以上步驟,DecorView
的測量就已經完成了。
綜上,ViewRootImpl#performMeasure(...)
其實就是對DecorView
的測量過程(DecorView#measure(...)
),DecorView
是一個FrameLayout
,其測量過程主要由FrameLayout#onMeasure(...)
負責,內部主要測量邏輯是先遍歷所有子View,讓 子View 先自己進行測量(child.measure(...)
),然后就可以獲取 子View 的測量大小,記錄所有 子View 中占比最大的測量寬/高,作為自己的最終測量大小。
performLayout
ViewRootImpl#performMeasure(...)
完成對DecorView
的測量后,接下來執行的是ViewRootImpl#performLayout(...)
,其源碼如下:
// frameworks/base/core/java/android/view/ViewRootImpl.java
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
...
// cache mView since it is used so much below...
final View host = mView;
...
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
}
其中,參數lp
的width
和height
均為MATCH_PARENT
,desiredWindowWidth
和desiredWindowHeight
為屏幕寬/高,mView
為DecorView
。
所以,performLayout(...)
內部其實就是調用DecorView#layout(...)
,前面 layout 流程中介紹過,ViewGroup#layout(...)
內部最終會通過View#layout(...)
進行布局,而View#layout(...)
內部最終通過View#setFrame(...)
方法記錄四個頂點位置,這樣DecorView
自己的布局位置就已確定了,即host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight())
。
確定了DecorView
自身的布局位置后,接下來就是要布局其 子View 了,因此,這里最終回調的是DecorView#onLayout(...)
方法,其源碼如下所示:
// frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder.Callback {
...
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
...
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
...
}
...
}
DecorView#onLayout(...)
內部轉交給FrameLayout#onLayout(...)
進行 子View 布局操作,其源碼如下:
// frameworks/base/core/java/android/widget/FrameLayout.java
public class FrameLayout extends ViewGroup {
...
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// 布局子View
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
void layoutChildren(int left, int top, int right, int bottom,
boolean forceLeftGravity) {
// 獲取 子View 數量
final int count = getChildCount();
// 左邊可放置起始點坐標
final int parentLeft = getPaddingLeftWithForeground();
// 右邊可放置終點坐標
final int parentRight = right - left - getPaddingRightWithForeground();
// 頂部可放置起始點坐標
final int parentTop = getPaddingTopWithForeground();
// 底部可放置終點坐標
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
// 遍歷 子View
for (int i = 0; i < count; i++) {
// 獲取 子View
final View child = getChildAt(i);
// 不放置狀態為 GONE 的子View
if (child.getVisibility() != GONE) {
// 獲取 子View 布局參數
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 獲取 子View 測量寬/高
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
// 當前 子View 的布局左邊界
int childLeft;
// 當前 子View 的布局右邊界
int childTop;
...
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
...
}
FrameLayout#onLayout(...)
內部是通過FrameLayout#layoutChildren(...)
進行 子View 的布局操作,其主要邏輯就是遍歷所有 子View,計算得到 子View 的四個頂點位置坐標,最后將結果傳遞給child.layout(...)
,讓 子View 記錄自己在父容器中的布局位置,完成 子View 的布局過程。
綜上,ViewRootImpl#performLayout(...)
就是對DecorView
的布局過程,此過程會遞歸計算各個 子View 的布局位置,調用 子View 的布局方法,完成各個 子View 的布局。
performDraw
完成了performMeasure(...)
和performLayout(...)
后,最后一步就是performDraw(...)
過程,其源碼如下:
// frameworks/base/core/java/android/view/ViewRootImpl.java
private void performDraw() {
...
draw(fullRedrawNeeded);
...
}
private void draw(boolean fullRedrawNeeded) {
...
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
...
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
...
mView.draw(canvas);
...
}
可以看到,ViewRootImpl#performDraw()
內部會經由ViewRootImpl#draw(...)
、ViewRootImpl#drawSoftware(...)
,最終執行的還是DecorView#draw(...)
過程,其源碼如下:
// frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder.Callback {
...
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if (mMenuBackground != null) {
mMenuBackground.draw(canvas);
}
}
...
}
由于FrameLayout
沒有覆寫draw(...)
方法,因此,super.draw(...)
最終調用的是View#draw(...)
方法,所以DecorView
默認采用的就是 View 的繪制方法,具體繪制詳情上文已介紹過了,主要就是對DecorView
的背景、內容、子View、滾動條等裝飾視圖進行繪制。
至此,View 繪制的整個流程已基本介紹完畢。
總結
View 的繪制主要有以下一些核心內容:
-
三大流程:View 繪制主要包含如下三大流程:
-
measure:測量流程,主要負責對 View 進行測量,其核心邏輯位于
View#measure(...)
,真正的測量處理由View#onMeasure(...)
負責。默認的測量規則為:如果 View 的布局參數為LayoutParams.WRAP_CONTENT
或LayoutParams.MATCH_PARENT
,那么其測量大小為 SpecSize;如果其布局參數為LayoutParams.UNSPECIFIED
,那么其測量大小為android:minWidth
/android:minHeight
和其背景之間的較大值。
自定義View 通常覆寫
onMeasure(...)
方法,在其內一般會對WRAP_CONTENT
預設一個默認值,區分WARP_CONTENT
和MATCH_PARENT
效果,最終完成自己的測量寬/高。而ViewGroup
在onMeasure(...)
方法中,通常都是先測量子View,收集到相應數據后,才能最終測量自己。-
layout:布局流程,主要完成對 View 的位置放置,其核心邏輯位于
View#layout(...)
,該方法內部主要通過View#setFrame(...)
記錄自己的四個頂點坐標(記錄與對應成員變量中即可),完成自己的位置放置,最后會回調View#onLayout(...)
方法,在其內完成對 子View 的布局放置。注:不同于 measure 流程首先對 子View 進行測量,最后才測量自己,layout 流程首先是先定位自己的布局位置,然后才處理放置 子View 的布局位置。
-
draw:繪制流程,就是將 View 繪制到屏幕上,其核心邏輯位于
View#draw(...)
,主要就是對 背景、自身內容(onDraw(...)
)、子View(dispatchDraw(...)
)、裝飾(滾動條、前景等) 進行繪制。注:通常自定義View 覆寫
onDraw(...)
方法,完成自己的繪制即可,ViewGroup 一般充當容器使用,因此通常無需覆寫onDraw(...)
。
-
measure:測量流程,主要負責對 View 進行測量,其核心邏輯位于
Activity 的根視圖(即
DecorView
)最終是綁定到ViewRootImpl
,具體是由ViewRootImpl#setView(...)
進行綁定關聯的,后續 View 繪制的三大流程都是均有ViewRootImpl
負責執行的。對 View 的測量流程中,最關鍵的一步是求取 View 的
MeasureSpec
,View 的MeasureSpec
是在其父容器MeasureSpec
的約束下,結合自己的LayoutParams
共同測量得到的,具體的測量邏輯由ViewGroup#getChildMeasureSpec(...)
負責。
DecorView
的MeasureSpec
取決于自己的LayoutParams
和屏幕尺寸,具體的測量邏輯位于ViewRootImpl#getRootMeasureSpec(...)
。
最后,稍微總結一下 View 繪制的整個流程:
-
首先,當 Activity 啟動時,會觸發調用到
ActivityThread#handleResumeActivity(..)
,其內部會經歷一系列過程,生成DecorView
和ViewRootImpl
等實例,最后通過ViewRootImpl#setView(decor,MATCH_PARENT)
設置 Activity 根View。注:
ViewRootImpl#setView(...)
內容通過將其成員屬性ViewRootImpl#mView
指向DecorView
,完成兩者之間的關聯。 ViewRootImpl
成功關聯DecorView
后,其內部會設置同步屏障并發送一個CALLBACK_TRAVERSAL
異步渲染消息,在下一次 VSYNC 信號到來時,CALLBACK_TRAVERSAL
就會得到響應,從而最終觸發執行ViewRootImpl#performTraversals(...)
,真正開始執行 View 繪制流程。-
ViewRootImpl#performTraversals(...)
內部會依次調用ViewRootImpl#performMeasure(...)
、ViewRootImpl#performLayout(...)
和ViewRootImpl#performDraw(...)
三大繪制流程,其中:performMeasure(..)
:內部主要就是對DecorView
執行測量流程:DecorView#measure(...)
。DecorView
是一個FrameLayout
,其布局特性是層疊布局,所占的空間就是其 子View 占比最大的寬/高,因此其測量邏輯(onMeasure(...)
)是先對所有 子View 進行測量,具體是通過ViewGroup#measureChildWithMargins(...)
方法對 子View 進行測量,子View 測量完成后,記錄最大的寬/高,設置為自己的測量大小(通過View#setMeasuredDimension(...)
),如此便完成了DecorView
的測量流程。performLayout(...)
:內部其實就是調用DecorView#layout(...)
,如此便完成了DecorView
的布局位置,最后會回調DecorView#onLayout(...)
,負責 子View 的布局放置,核心邏輯就是計算出各個 子View 的坐標位置,最后通過child.layout(...)
完成 子View 布局。performDraw()
:內部最終調用到的是DecorView#draw(...)
,該方法內部并未對繪制流程做任何修改,因此最終執行的是View#draw(...)
,所以主要就是依次完成對DecorView
的 背景、子View(dispatchDraw(...)
) 和 視圖裝飾(滾動條、前景等) 的繪制。