欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

View三大流程分析

程序员文章站 2022-12-02 16:14:28
View的三大流程指的是测量(measure)、布局(layout)和绘制(draw),我们可以通过生活中的一个例子来理解其过程。比如说现在有一个空房子要装修,那么首先要做的事情就是测量一下要往里面放置的家具的尺寸(measure),接着就要画一张设计图来设计一下具体的每个家具应该摆放在什么位置(layout),最后就要将家具摆放到该设计的位置上面了(draw)。然而,View和ViewGroup的三大流程具体的实现又有所不同。测量流程:对于View来说测量过程就是简单的测量自己的尺寸,而ViewGr...

同事件分发一样,View的三大流程也是自定义控件中要重要掌握的知识点。View的三大流程指的是测量(measure)、布局(layout)和绘制(draw),测量流程的作用是度量控件尺寸,布局流程的根据测量流程中度量的尺寸来确定控件的摆放位置,绘制流程最终将控件绘制在屏幕上。

然而,对于View和ViewGroup的三大流程具体的实现又有所不同。

  1. 测量流程:对于View来说测量过程就是简单的测量自己的尺寸,而ViewGroup因为是容器会装有子元素,它的尺寸有可能和子元素尺寸相关(如果给ViewGroup指定为wrap_content),所以ViewGroup的测量流程首先做的是度量所有子元素的尺寸,接着根据子元素的尺寸来确定自己的尺寸。根据这样的分析我们就知道了View的测量流程是它的父容器的测量流程中完成的。
  2. 布局流程:对于View来说布局流程就是确定自己的位置,而ViewGroup的布局流程首先会确定自己的位置,再确定子元素在自己中的位置。因此View的布局流程也是在ViewGroup的布局流程中完成的。
  3. 绘制流程:对于View来说绘制流程就是绘制自己的内容,而ViewGroup是容器,绘制自己没有什么意义,子元素都绘制完毕那么它也就绘制完毕了,所以它的绘制流程就是让子元素去绘制自己。所以说View的绘制流程也是在它的父容器的绘制流程中完成的。

根据以上的分析再结合View树的概念,我们就可以发现这三大流程的共性:子元素的流程都是由父容器传递而来的,这样一层一层的传递,最终完成整个View树的流程,也就是说每一个流程都是一个递归的过程。

接下来分别对这三大流程加以分析:

测量流程

在介绍测量流程前,首先要了解一个View的内部类:MeasureSpec,它在测量流程中起到了相当重要的角色。Android用32位的int值来描述在测量过程中控件长或宽的尺寸,其中高2位代表测量模式(SpecMode),低30位表示尺寸值(SpecSize),而MeasureSpec就是一个用来从int值中获取SpecSize、SpecMode或者根据这两者生成int值的一个工具类(也就是打包和解包),但是在以后的说明中为了方便,提到的MeasureSpec就表示这个int值。在测量流程中,每一个控件都有自己宽度和高度的MeasureSpec。

MeasureSpec代码如下:

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    
    //3种模式
    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;
    
	//根据模式和尺寸生成int值(打包)
    public static int makeMeasureSpec(int size, int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }

	//根据int值获取模式(解包)
    public static int getMode(int measureSpec) {
        //noinspection ResourceType
        return (measureSpec & MODE_MASK);
    }

	//根据int值获取尺寸(解包)
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
    ……
}

由代码可以发现,模式SpecMode有以下3种:

  1. UNSPECIFIED:父容器不对子元素有任何限制,这种情况用于系统内部,在开发中几乎用不到。
  2. EXACTLY:精确模式,这种模式下子元素的测量尺寸就等于SpecSize的值
  3. AT_MOST:最大模式,这种模式下子元素的测量尺寸要分情况讨论,后面会分析到

回想以下在开发中我们是通过布局文件中的android:layout_width和android:layout_height来指定控件的大小的(对应着Java代码中的LayoutParams,也就是布局参数),其值可以设置为match_parent、wrap_content和具体的dp值,而这3种情况就与SpecMode模式相关。在《Android开发艺术探索》中说EXACTLY对应着match_parent和具体数值这两种模式,AT_MOST对应着wrap_content,其实这种说法并不准确,在MeasureSpec的创建过程中就可以看出来,这一点在后面的内容会提到。

虽然在布局文件中指定了match_parent或wrap_content,但是系统在显示时依据的长度单位肯定是具体的像素值,而MeasureSpec的目的就在于此,它的功能就是将子元素布局参数中指定的match_parent、wrap_content或具体dp值转化成具体的数值。在测量过程中,父容器会将子元素的布局参数和父容器自己的SpecMode创建成该子元素的MeasureSpec,然后子元素在自己的测量流程中再根据这个MeasureSpec来得到自己的测量尺寸。

介绍完MeasureSpec之后,再来说说测量流程中的两个重要方法:measure(int widthMeasureSpec, int heightMeasureSpec)和onMeasure(int widthMeasureSpec, int heightMeasureSpec)。这两个方法的两个参数都是MeasureSpec,参数的意义可以理解为父容器对它尺寸的建议值,也就是说父容器在将测量流程传给子元素的同时也给了子元素宽高的建议值,子元素到底用不用这个建议值由子元素自己去决定。

不管是View还是ViewGroup,它们的测量流程都是从measure开始的,measure方法做了一些不需要我们关心的操作,随后便调用了onMeasure方法,而onMeasure方法才是我们要重点分析的。对于onMeasure方法,ViewGroup和View的具体实现有所不同,那么接下来分别讨论一下View和ViewGroup的onMeasure方法。

ViewGroup的onMeasure方法

对于ViewGroup来说,因为不同的容器摆放子元素的规则都不一样,所以每个具体的布局容器都要重写onMeasure方法来实现自己的测量原则。虽然如此,不管是什么容器,它们的大致测量流程都分两步:先测量所有子元素的尺寸,之后在根据子元素的尺寸来确定自己的尺寸。

测量子元素所涉及到的方法有以下3个:

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();
    //父容器根据子元素的布局参数和父容器自己的MeasureSpec计算子元素的MeasureSpec
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height);
	//根据计算好的MeasureSpec调用子元素的measure方法进入了子元素的测量流程
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    //父容器根据子元素的布局参数和父容器自己的MeasureSpec计算子元素的MeasureSpec
    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);
    //根据计算好的MeasureSpec调用子元素的measure方法进入了子元素的测量流程
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

第一个方法用于一次性测量所有子元素(其内部通过for循环遍历调用第二个方法实现);第二和第三个方法是测量一个指定的子元素,两者的区别是第三个方法考虑到了子元素的margin,所以如果想让ViewGroup支持子元素的margin,则使用第三个方法,否则直接使用前两个方法即可。观察以下可以发现,它们3个都调用了getChildMeasureSpec方法来创建子元素宽/高的MeasureSpec,最后子元素调用measure方法来进入自己的测量流程。来看一下getChildMeasureSpec方法的实现:

	public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
		//计算父容器去掉自己的padding和子元素的margin后的剩余空间
        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三大流程分析
可以看到,子元素MeasureSpec创建受到两方面的影响:一是子元素自己的布局参数,二是父容器的MeasureSpec(具体来说是SpecMode)。这个图是比较难记忆的,我在网上听过一个有趣的生活中实例去解释它,这样可以方便理解这个创建过程。

子元素MeasureSpec创建规则可以看成一个买房子问题。比如说结婚的时候要买房子,这时候跟爸爸要钱,这时候自己有3种情况,爸爸也有3种情况:
View三大流程分析
在图中可以看到对于match_parent和warp_content,父元素都会将自己的全部剩余空间留给子元素,match_parent这样做好理解但是warp_content为什么也要这样做呢?因为warp_content代表子元素的大小由其内容大小决定,而父容器在创建子元素的MeasureSpec时并不知道子元素到底有多大,所以只能暂时将自己的剩余空间都给子元素了,等到了子元素自己的测量流程时再确定warp_content到底有多大。

前文提到过在《Android开发艺术探索》中有一点说的并不太准确。书中提到EXACTLY对应着match_parent和具体数值这两种情况,但是根据表格可以发现,如果父容器也是AT_MOST模式,那么子元素即使是match_parent它的SpecMode也会被设置成AT_MOST。然而,子元素布局参数如果指定的具体dp值,那么不管父容器的SpecMode是什么,它的SpecMode都是EXACTLY。类似的,子元素布局参数如果指定了wrap_content,那么不管父容器的SpecMode是什么,它的SpecMode都是AT_MOST。也就是说只有match_parent这种情况特殊一点,但是这种情况在开发中基本遇不到。有谁闲的蛋疼呢?将父容器布局参数指定为wrap_content然后将子元素指定为match_parent,这不是没事儿找事儿嘛。所以说,在正常情况下控件的布局参数如果设置成了match_parent或者具体dp值,那么它的SpecMode就为精确模式EXACTLY,若设置成wrap_content那么它的SpecMode就为最大模式AT_MOST。

当父容器测量完所有子元素之后,就该确定自己的尺寸了,所用的方法是setMeasuredDimension,setMeasuredDimension(int measuredWidth, int measuredHeight)方法的两个参数就是确定下来的宽高尺寸的测量值值。这个方法执行完之后,控件的测量尺寸也就确定下来了,我们可以通过getMeasuredWidth/Height方法来获取到这个测量值。

这样一来ViewGroup的测量流程就说完了,接下来看看View的测量流程:

View的onMeasure方法

在上一小节中我们看到父元素在测量所有子元素时先通过getChildMeasureSpec来得到子元素的MeasureSpec,再通过子元素的measure方法进入到子元素的测量流程。之前已经提过了,在子元素的measrue方法又会调用onMeasure方法:

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

可以看到,里面调用了setMeasuredDimension去设置子元素的测量值,因此我们只需要看看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方法就是返回子元素MeasureSpec中的SpecSize,而SpecSize就是测量后的大小,而实际的大小是在布局流程中确定的,但是绝大多数情况下二者是相等的。

从代码中可以看到,子元素SpecMode是AT_MOST和EXACTLY返回值都是SpecSize。这样就会造成一种结果:继承自View的自定义控件在布局参数中若设置了wrap_content,那么效果和march_parent是一样的。为什么呢?结合上面那个买房子的图可以发现,不管是wrap_content还是march_parent,子元素获取到的SpecSize都是父容器的剩余空间,那么到了getDefaultSize又没有分开处理所以效果当然是一样的了。这样的话如果想让wrap_content起作用,就需要我们重写onMeasure方法,在其中判断一下,如果是AT_MOST那么就通过setMeasuredDimension方法给控件设置一个默认值即可。

这样就把View和ViewGroup的测量流程大致说完了,总结一下:不管是View还是ViewGroup,测量流程都是从measure方法开始的,而measure方中又会调用onMeasure方法,这两个方法都有两个参数:int widthMeasureSpec, int heightMeasureSpec。它们都是父容器计算好的尺寸建议值,子元素可以使用这个建议值也可以不用。View和ViewGroup的onMeasure实现方法不同,ViewGroup(这里的ViewGroup指特定的容器,比如线性布局)的onMeasure中先要测量所有子元素,然后再通过setMeasuredDimension确定自己的尺寸;而View的onMeasure方法如果没有被重写,则默认取出建议值中的SpecSize作为测量值。

布局流程

布局流程的作用是确定控件的位置,和布局相关的方法有layout和onLayout。其中layout方法是确定控件自己的位置,而onLayout是ViewGroup的作用是让ViewGroup确定子元素的位置,也就是说onlayout中会调用子元素的layout方法。由于不同的容器排列子元素有不同的特点,所以ViewGroup的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);
				……       
        }
       …… 
    }

上面代码中通过setFrame来确定控件4个顶点的位置,该方法中确定了控件的mLeft、mRight、mTop和mBottom,setFrame执行完毕之后,控件的尺寸就确定下来了,也就是说这时候可以通过getWidth或getHeitht来获取控件真实宽高了。那么通过getWidth/Height方法获得的尺寸和getMeasuredWidth/Height区别是什么呢?区别就在于它们的时机不同。getMeasuredWidth/Height获取到的值是在测量过程中控件的测量尺寸,而getWidth/Height获取到的是布局流程中控件的真实尺寸。一般来说,我们在onLayout方法中首先要通过getMeasuredWidth/Height获取到测量尺寸,然后再根据测量尺寸去调用layout方法确定控件的位置,所以这两类方法获取到的值一般来说是相等的,除非在调用layout方法时不依据测量尺寸,然而这是完全没必要的。

接下来在代码中又调用了onLayout方法:

	protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

可以看到是一个空的方法,这样的话,如果调用layout方法的是View,在setFrame确定自己的位置之后就结束了;而调用layout方法的是一个具体的布局容器,那么调用的onLayout就是该容器自己重写的onLayout方法了。总结一下,容器在layout中的setFrame方法确定自己的位置之后会调用onLayout方法遍历确定它的所有子元素的位置,也就是说onLayout方法中又会调用子元素的layout方法,而子元素如果还是容器,那么子元素中的layout方法又会调用onLayou方法,这样一层一层递归调用完成整个View树位置的确定。

绘制流程

绘制流程负责将控件绘制到屏幕上面,首先来看看View的draw方法:

	public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        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;

        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
            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;
        }

代码中的原生注释写的很清楚,一共分为6个步骤,不过步骤2和步骤5一般用不到,实际上也就分为了4步:

  1. 绘制背景:drawBackground
  2. 绘制自己:onDraw
  3. 绘制子元素:dispatchDraw
  4. 绘制装饰:onDrawForeground

View的dispatchDraw方法是空的,也就是说如果是View的话,画完自己就结束了;ViewGroup本身没有onDraw方法,它的onDraw方法是从View继承而来的,而View的onDraw方法又是空的,这也就说明:ViewGroup只是容器,它把子元素画完之后(ViewGroup的dispatchDraw方法将绘制流程传给子元素)自己也就画完了(不需要刻意的去画自己)。那么View的onDraw方法为什么是空的呢?因为不同的控件绘制当然是不同的。所以说,我们在自定义View的时候需要覆盖onDraw方法而自定义ViewGroup则不需要。

总结

在了解了三大流程之后我们可以发现一个规律,每一个流程都有一个带on的方法和不带on的方法,对它们总结如下:

方法/类 View ViewGroup
measure 测量自己 测量自己
onMeasure 测量自己的具体实现 先测子元素,再测自己
layout 确定自己在父容器中的位置 确定自己在父容器中的位置
onLayout 空方法 确定子元素在自己中的位置
draw 画自己 画子元素
onDraw 画自己的具体实现 空方法

根据这个表格和之前的分析,我们得出了几个在自定义控件时的结论:

  1. 自定义控件从大的方面可以分为两类:自定义View(继承View)和自定义ViewGroup(继承ViewGroup)。自定义View时一般要重写onMeasure和onDraw方法,自定义ViewGoup时要重写onMeasure和onLayout方法。
  2. 自定义View时要重写onMeasure方法来处理wrap_content的情况,否则效果和match_parent一样。
  3. getMeasuredWidh/Height方法一定要在setMeasuredDimension完成之后调用获得的值才有意义,同理,getWidth/Height方法要在layout方法(setFrame就是在layout中调用的)完成之后调用获得的值才有意义。而这两类方法获得的值一般来说是相等的,除非你不按测量尺寸去调用layout方法,然而这样的做法是没有意义的。
  4. 如果有必要的话,自定义ViewGroup时要在onLayout方法中处理自己的padding和子元素的margin,否则在布局文件中指定这些边距将不会生效。

这样一来View的3大流程就说完了,然而这是远远不够的。因为在ViewGroup的onMeasure和onLayout中只说了它的流程,而对于具体的ViewGroup都有具体的实现。那么在下一篇文章中我们通过手写一个自定义ViewGroup的方式来深刻理解一下这几大流程。为什么不自定义View呢?因为自定义View的侧重点在于onDraw方法的实现,而onDraw方法涉及到的东西实在太多了:画笔、画布、画矩形、画圆、画路径、画贝塞尔曲线等等,可以说一个自定义View是否能花里胡哨的就看开发者对onDraw方法实现怎么样了,然而这些东西跟我们讨论的3大流程关系并不大,而通过自定义ViewGroup的方式我们能更好的理解这几个流程的传递流程。如果想写出很炫酷的控件,这里我推荐启舰大佬的自定义控件三部曲系列文章

本文地址:https://blog.csdn.net/weixin_44965650/article/details/107538940