主頁 > 移動端開發 > 深入理解Android中View繪制三大流程及MeasureSpec詳解

深入理解Android中View繪制三大流程及MeasureSpec詳解

2021-01-04 11:41:10 移動端開發

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樹完成繪制,

在這里插入圖片描述

總結

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

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/244313.html

標籤:其他

上一篇:Android RecyclerView總結

下一篇:Android開發 設定手機壁紙

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more