深入理解Android中View绘制三大流程及MeasureSpec详解

View的MeasureSpec及View的Measure、Layout、Draw三大流程


前言

本文章主要针对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,至此就算结束了。
  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值