View的MeasureSpec及View的Measure、Layout、Draw三大流程
文章目錄
- View的MeasureSpec及View的Measure、Layout、Draw三大流程
- 前言
- 一、MeasureSpec是什么?
- 1.View整體流程
- 2.理解MeasureSpec
- 3.MeasureSpec模式
- 二、View繪制的三大流程
- 1.ViewRootImpl#performMeasure
- 2.ViewRootImpl#performLayout
- 3.Measure&Layout(Width、height)的區別
- 4.ViewRootImpl#performDraw
- 總結
前言
本文章主要針對MeasureSpec和ViewRootImpl中的performMeasure(測量)、performLayout(布局)、performDraw(繪制)進行描述,
一、MeasureSpec是什么?
1.View整體流程
- ViewRootImpl 是建立 DecorView 和 Window 之間聯系的核心其入口就在performTraversals() 方法中,performTraversals方法的開始經過 measure layout draw 三個程序才能最終的將一個view繪制出來DecorView作為頂級view,
ViewRootImpl.java#performTraversals()
1.private void performTraversals() {
2. ...
3. //測量
4. performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
5. ...
6. //布局
7. performLayout(lp, desiredWindowWidth, desiredWindowHeight);
8. ...
9. //繪制
10. performDraw();
11. ...
12.}
- View 繪制中主要流程分為measure,layout, draw 三個階段,measure:根據父 view 傳遞的 MeasureSpec進行計算大小,layout :根據 measure子View所得到的布局大小和布局引數,將子View放在合適的位置上,draw:把 View 物件繪制到螢屏上,

在開始分析之前,我們需要了解一些概念,如:
View:是所有UI組件的基類,是Android平臺中用戶界面體現的基礎單位,
ViewGroup:是容納UI組件的容器,它本身也是View的子類,
ViewRootImpl:是View的繪制的輔助類,所有View的繪制都離不開ViewRootImpl,
MeasureSpec: View的內部類,主要就是View測量模式的工具類,
2.理解MeasureSpec
- MeasureSpec,“測量規格,MeasureSpec是View定義的一個內部類,MeasureSpec代表一個32位的int,高兩位代表SpecMode,測量模式,低30位代表SpecSize,在某種測量模式下的規格大小,
MeasureSpec提供打包和解包的方法:
可以將一組SpecMode和SpecSize通過makeMeasureSpec方法打包成MeasureSpec,也可以將一個MeasureSpec通過getMode和getSize進行解包獲得對應的值,
MeasureSpec的作用:
在于Measure流程中,系統會將View的LayoutParams根據父容器所施加的規則轉換成對應的MeasureSpec,然后在onMeasure方法中根據這個MeasureSpec來確定View的測量寬高,
SpecMode測量模式有三種,含義如下:
1、AT_MOST:子View的最終大小是父View指定的SpecSize值,并且子View的大小不能大于這個值,即對應wrap_content這種模式,
常量值:-2147483648(0x80000000),
2、EXACTLY:父View已經測量出子Viwe所需要的精確大小,這時候View的最終大小就是SpecSize所指定的值,對應于match_parent和精確數值這兩種模式,
常數值:1073741824(0x40000000),
3、UNSPECIFIED:父View不對子View有任何限制,子View需要多大就多大,
常數值:0(0x00000000),

View.java#MeasureSpec()
1.public static class MeasureSpec {
2. /**進位大小為2的30次方(int的大小為32位,所以進位30位就是要使用int的最高位和第二高位也就是32和31位做標志位) */
3. private static final int MODE_SHIFT = 30;
4.
5. /** 運算遮罩,0x3為16進制,10進制為3,二進制為11,3向左進位30,就是11 00000000000(11后跟30個0) .
6. *(遮罩的作用是用1標注需要的值,0標注不要的值,因為1與任何數做與運算都得任何數,0與任何數做與運算都得0).
7. */
8. private static final int MODE_MASK = 0x3 << MODE_SHIFT;
9.
10. /**
11. * UNSPECIFIED 模式:
12. * 父View不對子View有任何限制,子View需要多大就多大
13. */
14. //0向左進位30,就是00 00000000000(00后跟30個0)
15. public static final int UNSPECIFIED = 0 << MODE_SHIFT;
16.
17. /**
18. * EXACTYLY 模式:
19. * 父View已經測量出子Viwe所需要的精確大小,這時候View的最終大小
20. * 就是SpecSize所指定的值,對應于match_parent= -1和精確數值這兩種模式.
21. */
22. //1向左進位30,就是01 00000000000(01后跟30個0)
23. public static final int EXACTLY = 1 << MODE_SHIFT;
24.
25. /**
26. * AT_MOST 模式:
27. * 子View的最終大小是父View指定的SpecSize值,并且子View的大小不能大于這個值,
28. * 即對應wrap_content = -2這種模式
29. */
30. //2向左進位30,就是10 00000000000(10后跟30個0)
31. public static final int AT_MOST = 2 << MODE_SHIFT;
32.
33. /**
34. *將size和mode打包成一個32位的int型數值
35. *高2位表示SpecMode,測量模式,低30位表示SpecSize,某種測量模式下的規格大小
36. */
37. public static int makeMeasureSpec(int size, int mode) {
38. if (sUseBrokenMakeMeasureSpec) {
39. /*measureSpec = size + mode; (注意:二進制的加法,不是十進制的加法!)
40. 這里設計的目的就是使用一個32位的二進制數,32和31位代表了mode的值,后30位代表size的值
41. 例如size=100(4),mode=AT_MOST,則measureSpec=100+10000...00=10000..00100*/
42. return size + mode;
43. } else {
44. /*size &; ~MODE_MASK就是取size 的后30位,mode & MODE_MASK就是取mode的前兩位,最后執行或運算,得出來的數字,前面2位包含代表mode,后面30位代表size*/
45. return (size & ~MODE_MASK) | (mode & MODE_MASK);
46. }
47. }
48.
49. /**將32位的MeasureSpec解包,回傳SpecMode,測量模式*/
50. public static int getMode(int measureSpec) {
51. /*mode = measureSpec & MODE_MASK;
52. * MODE_MASK = 11 00000000000(11后跟30個0),原理是用MODE_MASK后30位的0替換掉measureSpec后30位中的1,再保留32和31位的mode值,
53. * 例如10 00..00100 & 11 00..00(11后跟30個0) = 10 00..00(AT_MOST),這樣就得到了mode的值 */
54.
55. return (measureSpec & MODE_MASK);
56. }
57.
58. /**將32位的MeasureSpec解包,回傳SpecSize,某種測量模式下的規格大小*/
59. public static int getSize(int measureSpec) {
60. /*將MODE_MASK取反,也就是變成了00 111111(00后跟30個1),將32,31替換成0也就是去掉mode,保留后30位的size*/
61. return (measureSpec & ~MODE_MASK);
62. }
63. //...
}
3.MeasureSpec模式
- 每一個View,包括DecorView,都持有一個MeasureSpec,而該MeasureSpec則保存了該View的尺寸規格,在View的測量流程中,通過makeMeasureSpec來保存寬高資訊,在其他流程通過getMode或getSize得到模式和寬高,
ViewRootImpl.java#performTraversals()
1.private void performTraversals() {
2. int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
3. int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
4.
5. //請求host(指的是DecorView)進行測量;
6. performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
7.}
看到這里有同學會問了,DecorView是頂層view了,沒有父容器,那么它的MeasureSpec怎么來的呢?
如上code的getRootMeasureSpec就是lp.width/lp.height就是獲取就是螢屏的尺寸,并把回傳結果賦值childWidthMeasureSpec/childHeightMeasureSpec成員變數,
- 根據不同的模式來設定MeasureSpec,如果LayoutParams.MATCH_PARENT模式,則強制給VIew設定是視窗的大小,WRAP_CONTENT模式則設定最大的size,如過兩個都不是,直接設定MeasureSpec.EXACTLY但是不能超過當前視窗的size(),
ViewRootImpl.java#getRootMeasureSpec()
1.private static int getRootMeasureSpec(int windowSize, int rootDimension) {
2. int measureSpec;
3. switch (rootDimension) {
4.
5. case ViewGroup.LayoutParams.MATCH_PARENT:
6. /*視窗無法調整大小,強制將根View設定成windowSize*/
7. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
8. break;
9. case ViewGroup.LayoutParams.WRAP_CONTENT:
10. /*視窗可以調整大小,設定最大size給根View*/
11. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
12. break;
13. default:
14. /*視窗要精確大小,強制將根View設定為 MeasureSpec.EXACTLY*/
15. measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
16. break;
17. }
18. return measureSpec;
}
二、View繪制的三大流程
1.ViewRootImpl#performMeasure
- DecorView的MeasureSpec,它代表著根View的規格、尺寸,在接下來的measure流程中,就是根據已獲得的根View的MeasureSpec來逐層測量各個子View,
ViewRootImpl.java#performMeasure()
1.private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
2. Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
3. try {
4. //頂級View測量
5. mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
6. } finally {
7. Trace.traceEnd(Trace.TRACE_TAG_VIEW);
8. }
9.}
- performMeasure直接呼叫了measure,我們直接進入measure方法,由于DecorView是繼承FrameLayout,是PhoneWindow的一個內部類,而FrameLayout沒有measure方法,因此呼叫的是父類View的measure方法,
View.java#measure()
1.public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
2. // 判斷View的layoutMode是否為LAYOUT_MODE_OPTICAL_BOUNDS(理解他是一個視覺邊界)
3. boolean optical = isLayoutModeOptical(this);
4. // 子View是LAYOUT_MODE_OPTICAL_BOUNDS,父View不是LAYOUT_MODE_OPTICAL_BOUNDS的情況很少見,不需要去care.
5. if (optical != isLayoutModeOptical(mParent)) {
6. Insets insets = getOpticalInsets();
7. int oWidth = insets.left + insets.right;
8. int oHeight = insets.top + insets.bottom;
9. widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
10. heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
11. }
12.
13. // 生成View寬高的快取key,并且如果快取Map為null,則構建快取Map.
14. long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
15. if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
16. // 若mPrivateFlags中包含PFLAG_FORCE_LAYOUT標記,則強制重新布局
17. // 比如呼叫View.requestLayout()會在mPrivateFlags中加入此標記
18. final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
19.
20. final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
21. || heightMeasureSpec != mOldHeightMeasureSpec;
22. final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
23. && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
24. final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
25. && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
26. final boolean needsLayout = specChanged
27. && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
28. // 判斷是否為強制布局或者寬、高發生了變化
29. if (forceLayout || needsLayout) {
30. //清除PFLAG_MEASURED_DIMENSION_SET標記,表示該View還沒有被測量.
31. mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
32. // 決議從右向左的布局
33. //一般對阿拉伯語、希伯來語等從右到左書寫、布局的語言進行特殊處理
34. resolveRtlPropertiesIfNeeded();
35. //嘗試從緩從中獲取,若forceLayout為true或是快取中不存在或是忽略快取,則呼叫onMeasure()重新進行測量作業
36. int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
37. if (cacheIndex < 0 || sIgnoreMeasureCache) {
38. //View真正測量寬和高的地方
39. onMeasure(widthMeasureSpec, heightMeasureSpec);
40. mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
41. } else {
42. // // 快取命中,獲取快取中的View寬和高,不必再測量
43. long value = mMeasureCache.valueAt(cacheIndex);
44. //long占8個位元組,前4個位元組為寬度,后4個位元組為高度.
45. setMeasuredDimensionRaw((int) (value >> 32), (int) value);
46. mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
47. }
48.
49. // 無論是呼叫onMeasure還是使用快取,都應該設定了PFLAG_MEASURED_DIMENSION_SET標志位.
50. // 沒有設定,則說明測量程序出了問題,因此拋出例外.
51. // 并且,一般出現這種情況一般是子類重寫onMeasure方法,但是最后沒有呼叫setMeasuredDimension.
52. if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
53. throw new IllegalStateException("View with id " + getId() + ": "
54. + getClass().getName() + "#onMeasure() did not set the"
55. + " measured dimension by calling"
56. + " setMeasuredDimension()");
57. }
58.
59. mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
60. }
61.
62. mOldWidthMeasureSpec = widthMeasureSpec;
63. mOldHeightMeasureSpec = heightMeasureSpec;
64. // 記錄View的寬和高,并將其存盤到快取Map里.
65. mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
66. (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
67. }
- 我們可以看到,它在內部呼叫了onMeasure方法,由于DecorView是FrameLayout子類,實際上它呼叫的是DecorView#onMeasure方法,
DecorView#onMeasure主要是進行了一些判斷,這里就不展開來講了,它最后會呼叫到super.onMeasure方法,即FrameLayout#onMeasure方法,下面我們主要說一下FrameLayout#onMeasure方法,
FrameLayout.java#onMeasure()
1. @Override
2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
3. //獲取布局內子View數量
4. int count = getChildCount();
5.
6. final boolean measureMatchParentChildren =
7. MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
8. MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
9. mMatchParentChildren.clear();
10.
11. int maxHeight = 0;
12. int maxWidth = 0;
13. int childState = 0;
14. //遍歷所有子View中可見的View,也就不為GONE的View;
15. for (int i = 0; i < count; i++) {
16. final View child = getChildAt(i);
17. if (mMeasureAllChildren || child.getVisibility() != GONE) {
18. // 測量子view
19. measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
20. // 獲取子view的布局引數
21. final LayoutParams lp = (LayoutParams) child.getLayoutParams();
22. // 記錄子view的最大寬度和高度
23. maxWidth = Math.max(maxWidth,
24. child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
25. maxHeight = Math.max(maxHeight,
26. child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
27. childState = combineMeasuredStates(childState, child.getMeasuredState());
28. // 記錄所有跟父布局有著相同寬或高的子view
29. if (measureMatchParentChildren) {
30. if (lp.width == LayoutParams.MATCH_PARENT ||
31. lp.height == LayoutParams.MATCH_PARENT) {
32. mMatchParentChildren.add(child);
33. }
34. }
35. }
36. }
37.
38. // 子view的最大寬高計算出來后,還要加上父View自身的padding
39. maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
40. maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
41....
42. //保存測量結果
43. setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
44. resolveSizeAndState(maxHeight, heightMeasureSpec,
45. childState << MEASURED_HEIGHT_STATE_SHIFT));
46. //從子View中獲取match_parent的個數
47. count = mMatchParentChildren.size();
48. if (count > 1) {
49. for (int i = 0; i < count; i++) {
50. final View child = mMatchParentChildren.get(i);
51. final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
52.
53. final int childWidthMeasureSpec;
54. // 如果子view的寬是MATCH_PARENT,那么寬度 = 父view的寬 - 父Padding - 子Margin
55. if (lp.width == LayoutParams.MATCH_PARENT) {
56. final int width = Math.max(0, getMeasuredWidth()
57. - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
58. - lp.leftMargin - lp.rightMargin);
59. childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
60. width, MeasureSpec.EXACTLY);
61. } else {
62. childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
63. getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
64. lp.leftMargin + lp.rightMargin,
65. lp.width);
66. }
67.
68....
69. //對于這部分的子View,重新執行measure
70. child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
71. }
72. }
73. }
- 上面code中提到setMeasureDimension方法,該方法用于保存測量結果,該方法的引數接收的是resolveSizeAndState方法的回傳值,當specMode是EXACTLY時,那么直接回傳MeasureSpec里面的寬高規格,作為最終的測量寬高;當specMode時AT_MOST時,那么取MeasureSpec的寬高規格和size的最小值,
注:這里的size,對于FrameLayout來說,是其最大子View的測量寬高,setMeasureDimension方法最侄訓呼叫setMeasureDimensionRaw來保存測量的寬高,
View#setMeasuredDimensionRaw()
1.private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
2. mMeasuredWidth = measuredWidth;
3. mMeasuredHeight = measuredHeight;
4. //增加標志位,表示該View被測量.
5. mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
6.}
- onMeasure中有個measureChildWithMargin方法,它是干什么的呢?它主要作用是測量子View以及父容器的MeasureSpec,就是對子View進行測量,那么我們直接看這個方法,ViewGroup#measureChildWithMargins,
ViewGroup.java#measureChild()
1.protected void measureChild(View child, int parentWidthMeasureSpec,
2. int parentHeightMeasureSpec) {
3. /**
4. * child用來描述當前要測量大小的子view,
5. *parentWidthMeasureSpec和parentHeightMeasureSpec用來描述當前子view可以獲得的最大寬度和高度,
6. * widthUsed和heightUsed用來描述父視窗已經使用了的寬度和高度,
7. * */
8. final LayoutParams lp = child.getLayoutParams();
9.
10. final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
11. mPaddingLeft + mPaddingRight, lp.width);
12. final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
13. mPaddingTop + mPaddingBottom, lp.height);
14.
15. child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
16. }
- ViewGroup類的成員函式measureChildWithMargins必須要綜合考慮上述引數,以及當前正在測量的子view child所設定的大小和Margin值,還有當前視圖容器所設定的Padding值,通過呼叫getChildMeasureSpec來得到正在測量的子視圖child的正確寬度childWidthMeasureSpec和高度childHeightMeasureSpec,
這里的getChildMeasureSpec方法,就是把父容器的MeasureSpec以及自身的layoutParams屬性傳遞進去來獲取子View的MeasureSpec,
ViewGroup.java#getChildMeasureSpec()
1.public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
2. int specMode = MeasureSpec.getMode(spec);
3. int specSize = MeasureSpec.getSize(spec);
4. //size 表示子View可用空間:父容器尺寸減去padding
5. int size = Math.max(0, specSize - padding);
6.
7. int resultSize = 0;
8. int resultMode = 0;
9.
10. switch (specMode) {
11. // 父容器給子View確切的size,(具體數值或MATCH_PARENT)的情況下
12. case MeasureSpec.EXACTLY:
13. if (childDimension >= 0) {
14. resultSize = childDimension;
15. resultMode = MeasureSpec.EXACTLY;
16. } else if (childDimension == LayoutParams.MATCH_PARENT) {
17. // 子view想成為父容器的大小
18. resultSize = size;
19. resultMode = MeasureSpec.EXACTLY;
20. } else if (childDimension == LayoutParams.WRAP_CONTENT) {
21. //子View確定自己的的size,但是不能超過父容器
22. resultSize = size;
23. resultMode = MeasureSpec.AT_MOST;
24. }
25. break;
26.
27. // 父容器對子View施加了最大的限制(即父容器大小賦值為WRAP_CONTENT)的情況下
28. case MeasureSpec.AT_MOST:
29. ...
30. break;
31.
32. // 父容器不限制子View大小,子View需要多大就多
33. case MeasureSpec.UNSPECIFIED:
34. ...
35. break;
36. }
37. //noinspection ResourceType
38. return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
39. }
- 根據不同的父容器的模式及子View的layoutParams來決定子View的規格尺寸模式,通過上面的邏輯,不難看出父容器的MeasureSpec和子View的LayoutParams的組合情況下所出現的不同的子View的MeasureSpec,
接著會呼叫View#measure,child.measure方法,則繼續呼叫onMeasure方法,直到它的所有子view的大小都測量完成為止,這在上面說過了,這里不再贅述,
注:對于不同型別的View,其onMeasure方法是不同的,但是對于不同的View,即使是自定義View,我們在重寫onMeasure方法內,也一定會呼叫View#onMeasure方法,可參考如下示例圖:

- 到這里measure流程就算結束了,
2.ViewRootImpl#performLayout
- ViewRootImpl#performMeasure方法完成后,下面我們就進一步了解performLayout方法,
ViewRootImpl.java#performLayout()
1.private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
2. int desiredWindowHeight) {
3. mScrollMayChange = true;
4. mInLayout = true;
5. //這個host,就是DecorView
6. final View host = mView;
7. if (host == null) {
8. return;
9. }
10. if (DEBUG_ORIENTATION || sDebugLayout) {
11. Log.v(mTag, "Laying out " + host + " to (" +
12. host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
13. }
14.
15. Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
16. try {
17. //四個引數分別為 left,top,bottom,right.
18. host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
19. }
20.}
host.layout(0,0,host.getMeasuredWidth,host.getMeasuredHeight)它們分別代表了一個View的上下左右四個位置,顯然,DecorView的左上位置為0,然后寬高為它的測量寬高,由于View的layout方法是final型別,子類不能重寫,因此我們直接看View#layout方法即可:
View.java#layout()
1. public void layout(int l, int t, int r, int b) {
2. // // 當前視圖的四個頂點
3. int oldL = mLeft;
4. int oldT = mTop;
5. int oldB = mBottom;
6. int oldR = mRight;
7. //isLayoutModeOptical(mParent);//判斷該view布局模式是否有一些特殊的邊界
8. //有特殊邊界則呼叫setOpticalFrame(l, t, r, b)
9. //無特殊邊界則呼叫setFrame(l, t, r, b)
10. boolean changed = isLayoutModeOptical(mParent) ?
11. setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
12. //如果視圖的大小和位置發生變化,會呼叫onLayout()
13. if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
14. // 如果是單一View是沒有子View的,所以onLayout()是一個空實作
15. //onLayout():確定該ViewGroup所有子View在父容器的位置,也可以理解為確定子view位置;
16. onLayout(changed, l, t, r, b);
17....
18.
19. }
}
- setOpticalfram()有特殊邊界呼叫,其內部最終也是呼叫setFrame方法,我們給進代碼看一下;
View.java#setOpticalFrame()
1.private boolean setOpticalFrame(int left, int top, int right, int bottom) {
2. Insets parentInsets = mParent instanceof View ?
3. ((View) mParent).getOpticalInsets() : Insets.NONE;
4. Insets childInsets = getOpticalInsets();
5. //實際上是呼叫setFrame()
6. return setFrame(
7. left + parentInsets.left - childInsets.left,
8. top + parentInsets.top - childInsets.top,
9. right + parentInsets.left + childInsets.right,
10. bottom + parentInsets.top + childInsets.bottom);
11.}
- 這里說一下什么是特殊方法,就是一個Insets實體包含四個整數偏移量,這些偏移量描述了View(長方形)四個邊緣的變化, 按照慣例,設定此值會將邊緣移向矩形的中心,
Insets是不可變的,因此可以將其視為values,這里簡單描述一下什么是insets邊界,我這里用一段代碼和圖(13)來看一下Insets的四個值;
1.<?xml version="1.0" encoding="utf-8"?>
2.<inset xmlns:android="http://schemas.android.com/apk/res/android"
3. android:insetTop="50dp" android:insetLeft="50dp"
4. android:insetRight="50dp" android:insetBottom="50dp"
android:drawable="@color/colorAccent"/>

通過兩張圖我們理了解特殊邊界,我們繼續向下一看setFrame()方法,setFrame方法,會把四個位置資訊傳遞進去,這個方法用于確定View的四個頂點的位置,即初始化mLeft,mRight,mTop,mBottom這四個值,在該方法中,會將新舊left、right、top、bottom進行對比,只要不完全相同就說明View的布局發生了變化,則將changed變數設定為true,然后比較View的新舊尺寸是否相同,如果尺寸發生了變化,并將其保存到變數sizeChanged中,如果尺寸發生了變化,那么sizeChanged的值為true
View.java#setFrame()
1. @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
2. protected boolean setFrame(int left, int top, int right, int bottom) {
3. boolean changed = false;
4.
5. if (DBG) {
6. Log.d(VIEW_LOG_TAG, this + " View.setFrame(" + left + "," + top + ","
7. + right + "," + bottom + ")");
8. }
9.
10. if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
11. //將新舊left、right、top、bottom進行對比,只要不完全相對就說明View的布局發生了變化,
12. //則將changed變數設定為true
13. changed = true;
14.
15. //保存一下mPrivateFlags中的PFLAG_DRAWN標簽資訊
16. int drawn = mPrivateFlags & PFLAG_DRAWN;
17. //分別計算View的新舊尺寸
18. int oldWidth = mRight - mLeft;
19. int oldHeight = mBottom - mTop;
20. int newWidth = right - left;
21. int newHeight = bottom - top;
22. //控制元件的大小和位置有沒有改變
23. boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
24.
25. //
26. invalidate(sizeChanged);
27. // 通過以下賦值陳述句記錄下了視圖的位置資訊,即確定View的四個頂點
28. // 即確定了視圖的位置
29. mLeft = left;
30. mTop = top;
31. mRight = right;
32. mBottom = bottom;
33. //mRenderNode.setLeftTopRightBottom()方法會呼叫RenderNode中原生方法的nSetLeftTopRightBottom()方法,
34. //該方法會根據left、top、right、bottom更新用于渲染的顯示串列
35. mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
36. //向mPrivateFlags中增加標簽PFLAG_HAS_BOUNDS,表示當前View具有了明確的邊界范圍
37. mPrivateFlags |= PFLAG_HAS_BOUNDS;
38.
39.
40. if (sizeChanged) {
41. //這里 sizeChange 方法內部呼叫了 onSizeChanged 方法,
42. //所以當控制元件的大小和位置改變的時候會回呼 onSizeChanged 方法
43. sizeChange(newWidth, newHeight, oldWidth, oldHeight);
44. }
45....
46. return changed;
47. }
48.}
- 然后將新的left、top、right、bottom存盤到View的成員變數中保存下來,并執行mRenderNode.setLeftTopRightBottom()方法會,其會呼叫RenderNode中原生方法的nSetLeftTopRightBottom()方法,該方法會根據left、top、right、bottom更新用于渲染的顯示串列,
- 如果View的尺寸和之前相比發生了變化,那么就執行sizeChange()方法,該方法中又會呼叫onSizeChanged()方法,并將View的新舊尺寸傳遞進去,
View.java#sizeChange()
1. private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {
2. onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
3....
4. rebuildOutline();
5. }
6.
7. protected void onSizeChanged(int w, int h, int oldw, int oldh) {
8. }
- 當left、top、right、bottom以上四個值保存了view的位置資訊,所以說這四個值是最終寬高,也就是說,如果要得到View的位置資訊,那么就應該在layout方法完成后呼叫getLeft()、getTop()等方法來取得最終寬高,如果是在此之前呼叫相應的方法,只能得到0的結果,所以一般我們是在onLayout方法中獲取View的寬高資訊,下面我們來看onLayout,發現是一個空實作,
View.java#onLayout()
1.protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
2.}
注:對于單一View來說,由于在layout()中已經對自身View進行了位置計算,所以單一View的layout()就算已經完成了;

- 在 View 類中 onLayout 是一個空實作不難理解,意思很明確該方法在ViewGroup中呼叫,用于確定子View的位置,即在該方法內部,子View會呼叫自身的layout方法來進一步完成自身的布局流程,
- 下面我們看一下ViewGroup方法,發現 ViewGroup 中是一個抽象方法,onLayout()被override標注,所以也是重寫的方法,意思也很明顯了,在控制元件繼承自 ViewGroup 的時候,我們必須重寫 onLayout 方法,
為什么呢?
因為只有viewGroup中才有子 view,如果自己定義view group 則必須實作這個方法,很好理解,你包含子view,測量出來大小了,你得告訴我在具體哪個位置顯示,比如FrameLayout、LinearLayout、RelativeLayout等,他們的布局特性都是不一樣的,需要各自根據自己的特性來進行制定確定子元素位置的規則;
ViewGroup.java#onLayout()
1./**
2. * @param changed 當前View的大小和位置改變了
3. * @param left 父View的左部位置
4. * @param top 父View的頂部位置
5. * @param right 父View的右部位置
6. * @param bottom 父View的底部位置
7. */
8.@Override
9.protected abstract void onLayout(boolean changed,
10. int l, int t, int r, int b);
- 由于不同的布局容器的onMeasure方法均有不同的實作,因此不可能對所有布局方式都說一次,上面講到FrameLayout#onMeasure,那么現在也對FrameLayout#onLayout方法進行介紹:
FrameLayout.java#onLayout()
1.@Override
2.protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
3. //把父容器的位置引數傳遞進去
4. layoutChildren(left, top, right, bottom, false /* no force left gravity */);
5.}
- onLayout方法內部直接呼叫了layoutChildren方法,而具體實作是在layoutChildren方法,我們接著往下看;
FrameLayout.java#layoutChildren()
1. void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
2. final int count = getChildCount();
3. //以下四個值會影響到子View的布局引數
4. //parentLeft由父容器的padding和Foreground決定
5. final int parentLeft = getPaddingLeftWithForeground();
6. //parentRight由父容器的width和padding和Foreground決定
7. final int parentRight = right - left - getPaddingRightWithForeground();
8.
9....
10. for (int i = 0; i < count; i++) {
11. final View child = getChildAt(i);
12. if (child.getVisibility() != GONE) {
13. final LayoutParams lp = (LayoutParams) child.getLayoutParams();
14. //獲取子View的測量寬高
15. final int width = child.getMeasuredWidth();
16. final int height = child.getMeasuredHeight();
17....
18. //當子View設定了水平方向的layout_gravity屬性時,根據不同的屬性設定不同的childLeft
19. //childLeft表示子View的 左上角坐標X值
20. switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
21. /* 水平居中,由于子View要在水平中間的位置顯示,因此,要先計算出以下:
22. * (parentRight - parentLeft -width)/2 此時得出的是父容器減去子View寬度后的
23. * 剩余空間的一半,那么再加上parentLeft后,就是子View初始左上角橫坐標(此時正好位于中間位置),
24. * 假如子View還受到margin約束,由于leftMargin使子View右偏而rightMargin使子View左偏,所以最后
25. * 是 +leftMargin -rightMargin .
26. */
27. case Gravity.CENTER_HORIZONTAL:
28. childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
29. lp.leftMargin - lp.rightMargin;
30. break;
31. //水平居右,子View左上角橫坐標等于 parentRight 減去子View的測量寬度 減去 margin
32. case Gravity.RIGHT:
33. if (!forceLeftGravity) {
34. childLeft = parentRight - width - lp.rightMargin;
35. break;
36. }
37. //如果沒設定水平方向的layout_gravity,那么它默認是水平居左
38. //水平居左,子View的左上角橫坐標等于 parentLeft 加上子View的magin值
39. case Gravity.LEFT:
40. default:
41. childLeft = parentLeft + lp.leftMargin;
42. }
43. //當子View設定了豎直方向的layout_gravity時,根據不同的屬性設定同的childTop
44. //childTop表示子View的 左上角坐標的Y值
45. //分析方法同上
46. switch (verticalGravity) {
47. case Gravity.TOP:
48. childTop = parentTop + lp.topMargin;
49. break;
50....
51. }
52. //對子元素進行布局,左上角坐標為(childLeft,childTop),右下角坐標為(childLeft+width,childTop+height)
53. child.layout(childLeft, childTop, childLeft + width, childTop + height);
54. }
55. }
56. }
- 上面我們可以看出,在onLayout時,會先獲取父容器的padding值,然后遍歷其每一個子View,根據子View的layout_gravity屬性、子View的測量寬高、父容器的padding值、來確定子View的布局引數,然后呼叫child.layout方法,把布局流程從父容器傳遞到子元素,
- 如果子View是一個ViewGroup,那么就會重復以上步驟,如果是一個View,那么會直接呼叫View#layout方法,以上分析,該方法內部會設定view的四個布局引數,接著呼叫onLayout方法,而onLayout是一個空實作,它的主要作用是實作自定義View,并重寫該方法,實作自己想要的布局邏輯,以上onLayout就算講完了;
3.Measure&Layout(Width、height)的區別
- 這里說一下關于getWidth/getHeight與 getMeasuredWidth/ getMeasuredHeigh獲取的寬 (高)有什么區別?
我們來如下圖,便是二者區別,

- getMeasuredWidth/ getMeasuredHeigh的寬高是通過是setMeasuredDimension方法傳過來的,而getWidth/getHeigh是執行onLayout中的setFrame傳遞過來的;
View.java#setMeasuredDimensionRaw(),setFrame()
1. private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
2. mMeasuredWidth = measuredWidth;
3. mMeasuredHeight = measuredHeight;
4.
5. mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
6. }
7.
8.
9.
10. @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
11. protected boolean setFrame(int left, int top, int right, int bottom) {
12....
13. mLeft = left;
14. mTop = top;
15. mRight = right;
16. mBottom = bottom;
17....
18. }
所以說在自定義控制元件的時候在onLayout方法中一般采用getMeasuredWidth來獲得控制元件的寬度,因為getMeasuredWidth在measure后就有了值,而getWidth在layout才有值,因此除了onLayout方法中采用getMeasuredWidth方法外,在其之外的其他地方一般采用getWidth方法來獲取控制元件的寬度;
- View.java#getWidth(),getHeight(),getMeasuredWidth(),getMeasuredHeight()
/**
1. * View最終 寬、高
2. */
3.@ViewDebug.ExportedProperty(category = "layout")
4.public final int getWidth() {
5. //View最終的寬 = 子View的右邊界 - 子View的左邊界
6. return mRight - mLeft;
7.}
8.@ViewDebug.ExportedProperty(category = "layout")
9.public final int getHeight() {
10. //View最終的高 = 子view的下邊界 - 子view的上邊界
11. return mBottom - mTop;
12.}
13.
14./**
15. View測量的 寬、高
16. */
17.public final int getMeasuredWidth() {
18. //Measured程序中回傳的measured width
19. return mMeasuredWidth & MEASURED_SIZE_MASK;
20.}
21.public final int getMeasuredHeight() {
22. //Measured程序中回傳的measured height
23. return mMeasuredHeight & MEASURED_SIZE_MASK;
24.}
這里有個疑問,怎樣才能讓getMeasuredWidth和getWidth方法的回傳值不一樣呢,其時在寫的時候,一般這兩個值都是相等的,為了區分這個值,可以通過下面這個案例,來證明一下;
- 首先自己手動定義View,并繼承ViewGroup,并重寫它的onMeasure和onLayout方法,在onMeasure方法設定它的寬高,onLayout中設定它的上下左右;
MyViewGroup.java
1.public class MyViewGroup extends ViewGroup {
2.
3. public MyViewGroup(Context context) {
4. super(context);
5. }
6.
7. public MyViewGroup(Context context, AttributeSet attrs) {
8. super(context, attrs);
9. }
10.
11. @Override
12. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
13. super.onMeasure(widthMeasureSpec, heightMeasureSpec);
14. View view = getChildAt(0);
15. /**
16. * 設定寬度值為100,MeasureSpec.EXACTLY精確測量模式
17. */
18. measureChild(view, MeasureSpec.EXACTLY + 100, MeasureSpec.EXACTLY + 100);
19. }
20.
21. @Override
22. protected void onLayout(boolean changed, int l, int t, int r, int b) {
23. View childView = getChildAt(0);
24. /**
25. * 設定子View的位置,左上右下
26. */
27. childView.layout(0, 0, 500, 500);
28. }
29.}
- 然后通過xml檔案引入自己定好的myViewGroup,并添加一個button,不然的話,在上部code中獲取不到;
activity_main.xml
1.<?xml version="1.0" encoding="utf-8"?>
2.<com.example.application.MyViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
3. xmlns:app="http://schemas.android.com/apk/res-auto"
4. xmlns:tools="http://schemas.android.com/tools"
5. android:layout_width="match_parent"
6. android:layout_height="match_parent"
7. tools:context=".Main2Activity">
8. <Button
9. android:id="@+id/bt_1"
10. android:layout_width="wrap_content"
11. android:layout_height="wrap_content">
12. </Button>
13.
14.</com.example.recyclerviewapplication.MyViewGroup>
- 最后一步,我們來看一下Main2Activity中的實作,并輸出log及運行效果圖
MainActivity.java
1.public class MainActivity extends AppCompatActivity {
2.
3. private Button bt;
4.
5. @Override
6. protected void onCreate(Bundle savedInstanceState) {
7. super.onCreate(savedInstanceState);
8. setContentView(R.layout.activity_main);
9. bt=(Button)findViewById(R.id.bt_1);
10. }
11.
12. @Override
13. public void onWindowFocusChanged(boolean hasFocus) {
14. super.onWindowFocusChanged(hasFocus);
15. android.util.Log.d("MainActivity"," "+bt.getWidth()+","+bt.getHeight()+" ,"+bt.getMeasuredWidth()+" ,"+bt.getMeasuredHeight());
16. }
17.}
log輸出:
9231 D MainActivity : 500,500,100,100

- 通過對比,發現是不是不一樣,這里不過是人為設定的,正常一般都是相同值的,這樣設定其實沒有什么意義,但是證明了View的最終寬 / 高和測量寬 / 高大100px是可以不一樣,注意:olayout獲得的(寬 、 高)與measure獲取的(寬 、高)在非人為設定的情況下,永遠是相等的,
總結如下三點:
①getMeasuredWidth方法獲得的值是setMeasuredDimension方法設定的值,它的值在measure方法運行后就會確定
②getWidth方法獲得是layout方法中傳遞的四個引數中的mRight-mLeft,它的值是在layout方法運行后確定的
③一般情況下在onLayout方法中使用getMeasuredWidth方法,而在除onLayout方法之外的地方用getWidth方法,

4.ViewRootImpl#performDraw
- 上面兩篇已經詳細的介紹了 Measure 以及 Layout 程序,下面我們解紹 Draw 繪制程序,Draw 其實也不是很復雜,但是想要徹底掌味訓制的技巧就需要了解 Canvas 的使用了,這里就不展開講了,下面只描performDraw述核心代碼;
ViewRootImpl.java#performDraw()
1. private void performDraw() {
2....
3. /**
4. * mFullRedrawNeeded 表示是否需要繪制當前視窗全部區域
5. * mReportNextDraw 表示是當前視窗區域是否繪制完成
6. * */
7. final boolean fullRedrawNeeded = mFullRedrawNeeded || mReportNextDraw;
8....
9. //實作分發draw的作業
10. boolean canUseAsync = draw(fullRedrawNeeded);
11....
12.}
- Draw方法中做了很多處理,大概總結一下就是view的滾動設定和ViewTreeObserver的dispatchOnDraw開始分發draw開始繪制的監聽,還有硬體加速功能繪畫等,
ViewRootImpl.java#draw()
1. private boolean draw(boolean fullRedrawNeeded) {
2....
3. ///呼叫內部實作方法,來實作分發繪畫的作業
4. scrollToRectOrFocus(null, false);
5. //如果界面發生了滾動,就分發滾動監聽
6. if (mAttachInfo.mViewScrollChanged) {
7. mAttachInfo.mViewScrollChanged = false;
8. mAttachInfo.mTreeObserver.dispatchOnScrollChanged();
9. }
10. //computeScrollOffset 判斷是否要滑動影片,
11. //如果需要執行影片,則呼叫DeocView的onRootViewScrollYChanged,進行Y軸上的影片執行
12. boolean animating = mScroller != null && mScroller.computeScrollOffset();
13. final int curScrollY;
14....
15.
16. final float appScale = mAttachInfo.mApplicationScale;
17. final boolean scalingRequired = mAttachInfo.mScalingRequired;
18. //獲取mDirty,該值表示需要重繪的區域
19. final Rect dirty = mDirty;
20....
21. //如果fullRedrawNeeded為真,怎dirty區域至為整個螢屏,表示整個view都需要繪制
22. //第一次繪制,需要繪制所有view
23. if (fullRedrawNeeded) {
24. dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
25. }
26....
27. //如果有注冊TreeObserver下的監聽,在呼叫onDraw之前會觸發
28. mAttachInfo.mTreeObserver.dispatchOnDraw();
29....
30. if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
31. //是否開啟硬體加速,如果是,View 的繪制流程都是一樣的,區別就是 Canvas 不同
32. if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
33....
34. //開啟了硬體加速,則執行該方法,內部最侄訓是會執行到view 的 draw 方法
35. mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
36. } ...
37. //未開啟硬體加速,則執行該方法,執行drawSOftWare呼叫view的draw方法,整個繪畫流程就跑起來了
38. if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
39. scalingRequired, dirty, surfaceInsets)) {
40. return false;
41. }
42. }
43....
44. return useAsyncReport;
45. }
- 這些我們都不用關心,這里我們只看關鍵代碼,首先是先獲取了mDirty值,該值保存了需要重繪的區域的資訊,接著根據fullRedrawNeeded來判斷是否需要重置dirty區域,最后呼叫了ViewRootImpl#drawSoftware方法,并把相關引數傳遞進去,包括dirty區域,我們接著看該方法的原始碼,
ViewRootImpl.java#drawSoftware()
1.private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
2. boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
3. //鎖定canvas區域,由dirty區域決定
4. //獲取一塊畫布,這塊畫布會傳遞到各個onDraw方法中
5. canvas = mSurface.lockCanvas(dirty);
6...
7. //正式開始繪制
8. mView.draw(canvas);
9....
10. return true;
11. }
- ViewGroup沒有重寫draw方法,因此所有的View都是呼叫View#draw方法,這個方法很重要,我們要顯示的內容都是在這個方法中實作的,沒有實作這個方法的邏輯,就是前面的 Measure 和 Layout 邏輯處理的在漂亮,也不能呈現,因此,我們直接進View看它的原始碼,
View.java#draw()
1.@CallSuper
2. public void draw(Canvas canvas) {
3. final int privateFlags = mPrivateFlags;
4. mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
5.
6. /*
7. * Draw traversal performs several drawing steps which must be executed
8. * in the appropriate order:繪制流程
9. * 繪制背景
10. * 1. Draw the background
11. * 如果需要,會保存畫布已繪制的背景(可省略)
12. * 2. If necessary, save the canvas' layers to prepare for fading
13. * 繪制 View 的內容
14. * 3. Draw view's content
15. * 繪制子 View,子 View 的繪制也是按照這個流程進行
16. * 4. Draw children
17. * 如果需要,繪制邊框
18. * 5. If necessary, draw the fading edges and restore layers
19. * 繪制裝飾,如滾動條等
20. * 6. Draw decorations (scrollbars for instance)
21. * 如有必要,繪制默認的焦點突出顯示
22. * 7. If necessary, draw the default focus highlight
23. */
24.
25. // Step 1, draw the background, if needed
26. //繪制背景
27. int saveCount;
28.
29. drawBackground(canvas);
30. // 通常情況下,會跳過第 2 步和第 5 步
31. // skip step 2 & 5 if possible (common case)
32. Fading Edge是View 設定邊框漸變的效果
33. final int viewFlags = mViewFlags;
34. boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
35. boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
36. if (!verticalEdges && !horizontalEdges) {
37. //繪制 View 的內容,需要子類具體實作,View 的 onDraw 是一個空實作
38. //因為 View 并不是一個具體的 View ,不知道要繪制的內容,所以要繪制的內容留給具體的子類去具體實作
39. // Step 3, draw the content
40. onDraw(canvas);
41. //繪制內部包含的子 View,這個方法 View 也沒有實作,具體的實作是在 ViewGroup 中.
42. //呼叫ViewGroup的dispatchDraw方法,讓ViewGroup遍歷并呼叫所有的onDraw方法,整個view繪畫流程被激活
43. // Step 4, draw the children
44. dispatchDraw(canvas);
45.
46. drawAutofilledHighlight(canvas);
47.
48. // Overlay is part of the content and draws beneath Foreground
49. // 如果設定了 Overlay ,就呼叫并繪制 Overlay
50. if (mOverlay != null && !mOverlay.isEmpty()) {
51. mOverlay.getOverlayView().dispatchDraw(canvas);
52. }
53.
54. // 繪制前景色的drawble,繪制到canvas上
55. // 繪制scrollbars
56. // Step 6, draw decorations (foreground, scrollbars)
57. onDrawForeground(canvas);
58.
59. // Step 7, draw the default focus highlight
60. //繪制默認焦點高亮
61. drawDefaultFocusHighlight(canvas);
62.
63. if (isShowingLayoutBounds()) {
64. debugDrawFocus(canvas);
65. }
66.
67. // we're done...
68. return;
69. }
70. /*
71. * 后邊是對于Fading Edge效果的設定,這次就不再分析了,有興趣的朋友可以自己看一下這個效果;
72. *
73. */
- 下面,我們繼續分析在draw()中呼叫的drawBackground(),
View.java#drawBackground()
1.private void drawBackground(Canvas canvas) {
2. // 獲取背景 drawable
3. final Drawable background = mBackground;
4. if (background == null) {
5. return;
6. }
7. // 根據在 layout 程序中獲取的 View 的位置引數,來設定背景的邊界
8. setBackgroundBounds();
9. ...
10. // 獲取 mScrollX 和 mScrollY值
11. final int scrollX = mScrollX;
12. final int scrollY = mScrollY;
13. if ((scrollX | scrollY) == 0) {
14. background.draw(canvas);
15. } else {
16. // 如果 mScrollX 和 mScrollY 有值,則對 canvas 的坐標進行偏移
17. canvas.translate(scrollX, scrollY);
18. // 呼叫 Drawable 的 draw 方法繪制背景
19. background.draw(canvas);
20. canvas.translate(-scrollX, -scrollY);
21. }
22. }
- 這里呼叫了View#onDraw方法,View中該方法是一個空實作,因為不同的View有著不同的內容,這需要我們自己去實作,當我們自定義控制元件繼承 View 的時候,需要重寫 onDraw 方法,通過 Canvas 和 Paint 來進行內容的繪制,
View#onDraw()
1.protected void onDraw(Canvas canvas) {
2.}
- 如果當前的View是一個ViewGroup型別,那么就需要繪制它的子View,這里呼叫了dispatchDraw,而View中該方法是空實作,因為單獨一個 View 本身是沒有子元素的,不需要繪制 children ,
View.java#dispatchDraw()
1.protected void dispatchDraw(Canvas canvas) {
2.
3.}
- 實際是ViewGroup重寫了這個方法,那么我們來看看,ViewGroup#dispatchDraw:
ViewGroup#dispatchDraw()
1. @Override
2. protected void dispatchDraw(Canvas canvas) {
3....
4. // 這里會對畫布進行剪切,切掉Padding值
5. if (clipToPadding) {
6. clipSaveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
7. canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
8. mScrollX + mRight - mLeft - mPaddingRight,
9. mScrollY + mBottom - mTop - mPaddingBottom);
10. }
11.
12. ...
13. // 遍歷子View
14. for (int i = 0; i < childrenCount; i++) {
15. while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
16. ...
17. }
18. }
19....
20. if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
21. //呼叫 drawChild 方法,進行繪制子view
22. more |= drawChild(canvas, child, drawingTime);
23. }
24. }
- 這里面主要遍歷了所以子View,就不展開講了,每個子View都呼叫了drawChild這個方法,我們找到這個方法,ViewGroup#drawChild
ViewGroup#drawChild()
1./**
2. *Draw 一個該view組的child, 此方法負責使canvas處于正確的狀態, 這包括剪切,平移以使子view的滾動原點位于0、0,并應用任何影片轉換,
3. * @param canvas 在其上繪制子項的畫布
4. * @param child 畫誰
5. * @param drawingTime draw 發生的時間
6. * @return 如果是 invalidate() 回傳 true;
7. */
8.protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
9. return child.draw(canvas, this, drawingTime);
10.}
- 這里呼叫的是 View 這個類的多載方法,來看一下
View.java#draw()
1. /**
2. * ViewGroup.drawChild()呼叫此方法以繪制每個子View,
3. * 這是View專門基于圖層型別和硬體加速的渲染行為的地方,
4. */
5. boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
6. final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
7.
8. //是否支持硬體加速
9. boolean drawingWithRenderNode = mAttachInfo != null
10. && mAttachInfo.mHardwareAccelerated
11. && hardwareAcceleratedCanvas;
12....
13.
14. if (layerType == LAYER_TYPE_SOFTWARE || !drawingWithRenderNode) {
15. if (layerType != LAYER_TYPE_NONE) {
16. //未開啟硬體加速
17. //呼叫 View 的 buildDrawingCache 方法
18. layerType = LAYER_TYPE_SOFTWARE;
19. buildDrawingCache(true);
20. }
21. cache = getDrawingCache(true);
22. }
23. if (drawingWithRenderNode) {
24. //延遲獲取顯示串列,直到獲得影片驅動的alpha值為止,設定并傳遞給View
25. renderNode = updateDisplayListIfDirty();
26. if (!renderNode.hasDisplayList()) {
27. renderNode = null;
28. drawingWithRenderNode = false;
29. }
30. }
31.
32. int sx = 0;
33. int sy = 0;
34. if (!drawingWithRenderNode) {
35. //內部是一個空實作,用于我們在自定義滑動控制元件時,重寫該方法,并設定mScrollX和mScrollY
36. computeScroll();
37. sx = mScrollX;
38. sy = mScrollY;
39. }
40....
41. //根據mScrollX和mScrollY移影片布的坐標系
42. if (offsetForScroll) {
43. canvas.translate(mLeft - sx, mTop - sy);
44. } ...
45. //設定畫布透明度的操作,省略
46. ...
47.
48. if (!drawingWithDrawingCache) {// 不用快取,直接畫
49. if (drawingWithRenderNode) { // 是否硬體加速
50. mPrivateFlags &= ~PFLAG_DIRTY_MASK;
51. //開啟硬體加速掉用drawRenderNode
52. ((RecordingCanvas) canvas).drawRenderNode(renderNode);
53. } else {
54. //是否已經繪制過一次了,如果沒有,則會呼叫draw(canvas)方法,如果繪制了,則繼續執行dispatchDraw,
55. if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
56. mPrivateFlags &= ~PFLAG_DIRTY_MASK;
57. dispatchDraw(canvas); 繪制子view的子view
58. } else {
59. draw(canvas);// 繪制子view自身
60. }
61. }
62. } else if (cache != null) {// 有快取的話,繪制bitmap,view的快取是做為bitmap保存的.
63. mPrivateFlags &= ~PFLAG_DIRTY_MASK;
64. if (layerType == LAYER_TYPE_NONE || mLayerPaint == null) {
65. ...
66. canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
67. } ...
68.
69.
70. return more;
71. }
- 這一步也可以歸納為ViewGroup繪制程序,它對子View進行了繪制,而子View又會呼叫自身的draw方法來繪制自身,這樣不斷遍歷子View及子View的不斷對自身的繪制,從而使得View樹完成繪制,

總結
- performMeasure 通過計算得出 DecorView 的 MeasureSpec 然后呼叫其 measure 方法,此方法是 View 類的統一入口,主要是做了判斷是否要測量和布局,如果需要則直接呼叫重寫的 onMeasure 方法,ViewGruop會遍歷所有子view,根據自身的 MeasureSpec 和 子view的 LayoutParams 決定子view的 MeasureSpec, 并呼叫子view的 measure 方法傳遞測量事件,直到傳遞到整個 View 樹的葉子為止,
- performLayout 從 View 樹的頂端開始,依次向下呼叫 layout 方法來確認自身在父容器內的位置,這時最終的寬高被確認,然后呼叫重寫過的 onLayout 方法(根據布局特性重寫)來確認所有子view的位置,
- performDraw 也是按照前面測量和布局的思路傳遞在整個 View 樹中,onDraw 繪制自身的內容是實作自定義View的最關鍵方法,
Android整個繪制流程是 通過ViewRootImpl#performTravesals 方法,繪制的先后順序是 measure, layout, draw,至此就算結束了,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/244313.html
標籤:其他
下一篇:Android開發 設定手機壁紙
