Android View系列(三):View的绘制流程

ViewRoot和DecorView

ViewRoot对应于ViewRootImpl类,他是链接WindowManager和DecorView的纽带,View的三大流程都是通过ViewRoot来完成的,在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl和DecorView建立关联

DecorView作为顶级的View,一般情况下它内部会包括一个竖直方向的LinearLayout,这个LinearLayout里面具有上下俩个部分(具体跟Android的版本和主题有关),上面是标题栏,下面是内容栏,在Activity中setContentView的布局就是被加在了内容栏之中,内容栏的id为content,如何得到content?可以通过findViewById(android.R.id.content)),如何得到我们在Activity中设置的View,可以content.getChildAt(0);同时DecorView其实是一个FrameLayout,View的事件都是通过DecorView传递给我们的View的

在这里插入图片描述

View的三大绘制流程

View的绘制流程是从ViewRootImpl的performTraversals方法开始的,他经过measure,layout,draw才能最终将一个View绘制出来,其中measure用来测量View的宽高,layout用来确定View在父容器中的位置,draw负责将View绘制在屏幕上,大致如图:

在这里插入图片描述
performTraversals方法会依次调用,performMeasure,performLayout,performDraw三个方法,这三个方法分别完成顶级View的measure,layout和draw这三大流程

其中performMeasure方法会调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中则会对所有的子元素进行measure过程,这个时候measure流程就从父元素传入到了子元素中,这样就完成了一次measure过程,接着子元素会重复父元素的measure过程,这样反复就完成View树的遍历,同理performLayout和performDraw的传递流程和performMeasure是类似的,唯一不同的是preformDraw传递过程是在draw方法中通过dispatchDraw来实现的,本质没有区别

  • measure过程决定了View的宽高,measure完以后可以通过getMeasuredWidth和getMeasuredHeight来获取测量宽高
  • layout过程确定了View的四个顶点和实际的View宽高,完成以后可以通过getTop,getLeft,getRight,getBottom来获取View的四个顶点,并通过getWidth和getHeight获取View的最终宽高
  • draw过程则决定了View的显示,只有draw方法完成以后,View的内容才会显示在屏幕上

理解MeasureSpec

MeasureSpec参与了View的measure过程,MeasureSpec很大程度上决定了View的尺寸规格,之所以说很大程度是因为,他还受父View的影响,因为父容器影响了View的MeasureSpec的创建过程

MeasureSpec代表一个32位的int值,高俩位代表SpecMode,低30位代表SpecSize,SpecMode指的是测量模式,SpecSize代表的是某种测量模式下,规格的大小,下面先看一下MeasureSpec的内部常量的一些定义

public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
        public static final int EXACTLY     = 1 << MODE_SHIFT;
        public static final int AT_MOST     = 2 << MODE_SHIFT;

        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
        
        
    @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

 
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多对象内存分配,为了方便操作,其提供了打包和解包的功能,SpecSize和SpecMode也是int值,一个SpecMode和一个SpecSize可以打包成为一个MeasureSpec,而一个MeasureSpec可以解包为一个SpecSize和一个SpecMode

SpecMode有三类,每一类都代表了特殊的含义,如下所示

UNSPECIFIED

父容器不对View有任何限制,要多大给多大,这种情况一般适用于系统内部,表示一种测量状态

EXACTLY

父容器已经检测出了View所需要的精确大小,这个时候View的最终大小就是SpecSize的值,他对应于LayoutParams中的mach_paraent和具体数值俩种模式

AT_MOST

父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现,他对应于LayoutParams中的warp_content

MeasureSpec和LayoutParams

我们给View设置LayoutParams,在View测量的时候,系统将本View设置的LayoutParams和父容器的约束下转换成本View的MeasureSpec,然后根据这个MeasureSpec确定本View的测量后的宽高

另外,对于顶级View(即DecorView)和普通View来说,MeasureSpec的转换过程有所不同,

  • 对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams共同决定
  • 对于普通View,其Measurespec由父容器的MeasureSpec和自身的LayoutParams决定
  • MeasureSpec一旦确定,onMeasure中就可以确定View的测量宽高

对于DecorView来说,在ViewRootImpl中的measureHierarchy方法中有如下一段代码,他展示了DecorView创建MeasureSpec的过程

 			childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

其中desiredWindowWidth和desiredWindowHeight是指的屏幕尺寸,再看一下getRootMeasureSpec方法的源码

     int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

通过上述代码,DecorView和MeasureSpec的产生过程就很明确了,其遵守如下规则,根据他的LayoutParams中的宽高参数来划分

  • LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小
  • LayoutParams.WRAP_CONTENT:最大模式,大小不确定,但是不能超过窗口的大小
  • 固定大小:精确模式,大小为LayoutParams中指定的大小

对于普通View来说,View的measure过程由ViewGroup传递而来,先看一下ViewGroup的measureChildWithMargins方法源码

   protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

上述方法会调用子View的measure方法,调用子元素的measure方法之前,会先通过getChildMeasureSpec方法来得到子元素的MeasureSpec,从代码来看子元素的MeasureSpec的创建,和父容器的Measure,和自身的LayoutParams有关,此外和View的margin及padding有关,具体情况可以看一下ViewGroup的getChildMeasureSpec方法

 public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

这个方法不难理解,它主要的作用是根据父容器的MeasureSpec同时结合View本身的LayoutParams来确定子元素的MeasureSpec,其中的padding指的是父容器已经占用空间的大小,因此子元素的可用大小为父元素的尺寸减去padding,代码如下:

int size = Math.max(0, specSize - padding);

getChildMeasureSpec方法清楚的展示了普通View 的MeasureSpec的创建规则,为了更清晰的看出getChildMeasureSpec的逻辑,我们提供一个表,对getChildMeasureSpec方法进行梳理
在这里插入图片描述
我们再次分析一下图

  • 当子View采用固定宽高时
    • 不管父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式并且遵循LayoutParams中的大小
  • 当子View的宽高是mach_parent时
    • 父容器是精准模式,子View也是精准模式,并且大小是父容器的剩余空间
    • 父容器是最大模式,子View也是最大模式,并且最大不会超过父容器的剩余空间
  • 当子View的宽高是warp_content
    • 不管父容器是精准还是最大模式,View的模式总是最大模式,并且大小不能超过父容器的剩余大小

我们这里并没有讨论UNSPECIFIED模式,因为这种情况主要用于系统内部多次measure的情形,一般来说我们不需要关注

View的工作流程

View的工作流程主要是指,measure,layout,draw这三大流程,即测量布局和绘制,其中measure确定了View的测量宽高,layout确定了View的最终宽高和四个顶点的位置,而draw是将View绘制到了屏幕上

measure过程

measure过程有俩种情况,如果这是一个原始的View,那么通过measure方法就完成了其测量,如果是一个ViewGoup,除了完成自己的测量外,还会去遍历去调用所有子元素的measure方法,各个子元素再去 执行这个流程,下面针对这俩个流程进行分析

View的measure过程
View的measure过程是通过measure方法完成的,measure方法是一个final类型的方法,不能够子类被重写,在View的measure方法中会调用View的onMeasure方法,我们只需看onMeasure方法的实现

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

setMeasuredDimension方法会设置View宽高的测量值,我们看一下getDefaultSize这个方法

  public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

getDefaultSize这个方法的逻辑很简单,对于我们来说只需要看AT_MOSTEXACTLY这俩种情况,简单理解getDefaultSize返回的就是MeasureSpec中的SpecSize,而这个SpecSize就是View测量后的大小,这里多次提到测量大小,因为View的最终大小是在layout阶段确定的,所以这里加以区分

至于UNSPECIFIED这种情况,一般用于系统内部的测量过程,这种情况下View的大小为getDefaultSize的第一个参数size,即宽高分别为getSuggestedMinimumWidthgetSuggestedMinimumHeight这俩个方法的返回值,看一下他们的源码

 protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

 protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

    }

这里只分析getSuggestedMinimumWidth的实现,从代码中可以看出如果View没有设置背景,那么View的宽度是mMinWidth,而mMinWidth代表android:minWidth这个属性所指定的值,如果不指定这个属性则默认为0,如果View设置了背景,则View的宽度为max(mMinWidth, mBackground.getMinimumWidth()),那么我们研究一下mBackground.getMinimumWidth()是什么

  public int getMinimumHeight() {
        final int intrinsicHeight = getIntrinsicHeight();
        return intrinsicHeight > 0 ? intrinsicHeight : 0;
    }

getMinimumHeight方法返回的是Drawadle的原始宽度,前提是有原始宽度,ShapeDrawable无原始宽度,BitmapDrawable有原始宽度

现在我们总结一下getSuggestedMinimumWidth的逻辑

  • 如果View没有设置背景,就返回android:minWidth这个属性值,不指定则默认为0
  • 如果View设置了背景,则返回android:minWidth这个属性值和背景最小宽度的最大值
  • getSuggestedMinimumWidth返回的值就是UNSPECIFIED情况下的测量宽高

从getDefaultSize来看View的宽高由SpecSize决定,我们可以得出如下结论,直接继承View的自定义控件,需要重写onMeasure方法,并设置wrap_content时的自身大小,否则用wrap_content相当于用mach_parent,为什么呢?

从上方的表我们可以知道子View的MeasureSpec,是根据自身的LayoutParams和父容器的MeasureSpec决定的,当子View的LayoutParams是wrap_content时,不管父容器的MeasureSpec是什么,子View的SpecMode都是AT_MOST,这种情况下View的SpecSize是父容器剩余空间的大小,这种情况下的布局和mach_parent是一样的,如何解决这种问题?

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int width = 80;
        int height = 80;
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heigthMode = MeasureSpec.getMode(heightMeasureSpec);
        int heigthSize = MeasureSpec.getSize(heightMeasureSpec);

        if (widthMode == MeasureSpec.AT_MOST && heigthMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(width, height);
        } else if (widthMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(width, heigthSize);
        } else if (heigthMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSize, height);
        }
    }

我们给View指定一个默认的宽高,当设置warp_content的时候直接设置此宽高,对于非warp_content的情况我们需要沿用系统的测量值,至于宽高的默认值大小没有规定依据,TextView和其他控件对warp_content情况均作了特殊处理

VIewGroup的measure过程
对于ViewGroup来说,除了需要执行自己的measure过程,还需要遍历所有的子元素measure方法,各个子元素再去递归执行这个流程,和View不同的是,ViewGroup没有重写View的onMeasure方法,但是它提供了一个measureChildren的方法,源码如下所示:

  protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

 protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

最终就是取出child的LayoutParams,然后在通过getChildMeasureSpec 来创建子元素的MeasureSpec,然后调用子View的measure方法去测量

ViewGroup没有具体的测量过程,因为ViewGroup是一个抽象类,其测量的onMeasure方法需要各个子类去去实现,为什么不能统一实现呢?因为每个ViewGroup的子类都有不同的布局特性,这就导致他们的测量过程有所差异,比如LineraLayout和RelativeLayout的布局特性就不一样,所以无法做统一实现,下面我们分析一下LineraLayout的onMeasure实现

   @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

上面的代码很简单,我们分析一下竖直情况下的代码measureVertical

   // See how tall everyone is. Also remember max width.
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
         ....
                measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);

                final int childHeight = child.getMeasuredHeight();
             
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));

这段代码表示,系统会遍历子元素并且调用measureChildBeforeLayout方法,这个方法内部会调用View的measure方法,这样各个子View都会依次进入measure过程,并且系统会用mTotalLength记录LineraLayout竖直方向的高度,每测量一个子元素,mTotalLength就会增加,当子元素处理完,LineraLayout会测量自己的大小,源码是

 		// Add in our padding
        mTotalLength += mPaddingTop + mPaddingBottom;

        int heightSize = mTotalLength;

        // Check against our minimum height
        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

        // Reconcile our calculated size with the heightMeasureSpec
        int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
        heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
        
		maxWidth += mPaddingLeft + mPaddingRight;

        // Check against our minimum width
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);

上述代码说明,当子元素测量完毕后,LineraLayout会根据子元素的大小测量自己的大小,针对LineraLayout他在水平方向遵循View的测量过程,在竖直方向的测量有所不同,具体来说如果是match_parent或者具体数值,他的测量过程和View一致,即高度为SpecSize,如果他的布局是warp_content,那么高度是所有子元素占用的高度的总和,但是他依然不能超过父元素的剩余空间,当然还要考虑padding,看下源码

 public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);
        final int result;
        switch (specMode) {
            case MeasureSpec.AT_MOST:
                if (specSize < size) {
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    result = size;
                }
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }

View的measure过程是三大流程最复杂的一个,measur完成以后就可以获取测量宽高,需要注意的是有些极端情况下,需要measure多次才可以确定测量宽高,所以尽量不要在onMeasure中拿宽高,在onLayout里面获取会比较准确

Layout过程

layout确定了View的四个顶点的位置和最终的宽高,如果是ViewGroup,当ViewGroup的位置确定后,他在onLayout遍历子元素调用子元素的layout方法,在layout方法中onLayout又会被调用,如此循环,layout方法确定View本身的位置,而onLayout方法确定所有子元素的位置,先看一下View的layout方法

  public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);

            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }

            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
            }

layout的大致流程如下,首先会通过setFrame方法来设定View的四个顶点的值,及初始化mLeft,mTop,mRight,mBottom的值,View的四个顶点一旦确定,View在父容器的位置就确定了,接着会调用onLayout方法,这个方法是为了父容器确定子元素的位置,onLayout的具体实现和具体的布局有关,View和ViewGroup均没有真正实现onLayout方法,我们看一下LineraLayout的onLayout方法的实现

  protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }

这里选择layoutVertical进行分析

 void layoutVertical(int left, int top, int right, int bottom) {
   ...

        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();

                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();

               ...
               
                if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }

                childTop += lp.topMargin;
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }
        }
    }

可以看到此方法会遍历子元素并调用setChildFrame方法为子元素指定相应的位置,其中childTop会不断的变大,这就意味着后面的子元素会放在靠下的位置,这刚好符合LineraLayout竖直的属性,setChildFrame只是调用了子元素的layout方法,父元素通过layout确定自己的位置后,通过onLayout调用子元素的layout方法,如此循环,完成整个View树的layout,setChildFrame的源码:

 private void setChildFrame(View child, int left, int top, int width, int height) {
        child.layout(left, top, left + width, top + height);
    }

我们注意到setChildFrame中的width和height就是View的测量宽高,如下代码可以看出

   				final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),childWidth, childHeight);

而在layout中会通过setFrame方法去设置四个顶点的宽高,如下代码

  			mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;

View的测量宽高和最终宽高有什么区别

我们看一下getWidth和getMeasureWidth有什么区别,我们先看一下getWidth和getHeight的源码

    public final int getWidth() {
        return mRight - mLeft;
    }

    public final int getHeight() {
        return mBottom - mTop;
    }

通过上面源码我们可以知道,其实View默认情况下测量宽高和最终宽高大小是一样的,只不过测量宽高是measure过程形成的,而最终宽高是在layout过程形成的,赋值的时机不同,measure比较早,在实际的开发中,我们可以认为测量宽高等于实际的宽高,但是某些情况下他们不同,比如:

重写View的layout方法

@Override
    public void layout(int l, int t, int r, int b) {
        super.layout(l, t, r+100, b+100);
    }

这样就会导致测量宽高比实际宽高小100px,这样会导致View显示不正常没有实际的意义,但是也证明了有不相等的情况

draw过程

draw过程比较简单,他的作用是把View绘制到屏幕上,draw过程会遵循如下几步

  • 绘制背景( drawBackground(canvas);)
  • 绘制自己(onDraw(canvas);)
  • 绘制children( dispatchDraw(canvas);)
  • 绘制装饰( onDrawForeground(canvas);)
  public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

        // Step 1, draw the background, if needed
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(canvas);

            drawAutofilledHighlight(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);

            if (debugDraw()) {
                debugDrawFocus(canvas);
            }

            // we're done...
            return;
        }

View绘制过程是通过dispatchDraw方法向子View 传递的,dispatchDraw会遍历所有的子View调用子View的draw方法,这样draw时间就一层一层传了下去,View有一个特殊的方法setWillNotDraw

  /**
     * If this view doesn't do any drawing on its own, set this flag to
     * allow further optimizations. By default, this flag is not set on
     * View, but could be set on some View subclasses such as ViewGroup.
     *
     * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
     * you should clear this flag.
     *
     * @param willNotDraw whether or not this View draw on its own
     */
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }

如果一个View不需要绘制任何内容,那么设置这个标记为true后,系统会进行相应的优化,默认情况下View没有启用这个标志位,但是ViewGroup会默认启用这个标志位,当我们我们的自定义控件继承自ViewGroup时本身不具备绘制功能,就可以开启这个标志位,如果需要通过onDraw绘制,那么就要关闭这个标志位

发布了100 篇原创文章 · 获赞 5 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_34760508/article/details/98479386