Java 设计模式Android开发拾穗Android开发

Android View 渲染流程

2019-06-23  本文已影响7人  a57ecf3aaaf2

一、前言

Android View 显示在屏幕中需经过 measure、layout(ViewGroup 独有)、draw 三个步骤完成,其中 View 的尺寸测量是三个步骤中最为复杂的一个,理解好 View 的 measure 过程,是理解另外两个流程的基础。

View 的整个绘制流程从 ViewRootImpl 类的 performTraversals 方法开始,该类是连接 DecorView 和 WindowManager 的纽带,实现了 ViewParent 接口,负责将整个视图树绘制在 Android 应用程序窗口中。

performTraversals 方法会依次经过 performMeasure、performLayout、performDraw 三个方法,分别完成 View 的测量、布局和绘制流程。

其中,performMeasure 方法中会调用 View 的 measure 方法,而 measure
方法又会调用 onMeasure 方法。以此类推,其他两个流程基本也是这样的调用顺序。

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    if (mView == null) {
        return;
    }
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

实际开发中,我们只需要关心如何实现 onMeasure、onLayout、onDraw 方法,但是实现这三个方法进行自定义 View 的关键还是要理解 View 的整个渲染流程和原理。

二、measure 过程

2.1 MeasureSpec

MeasureSpec 是 View 尺寸测量的基本单位,其通过一个 30 位的 int 值承载了 View 尺寸、规格信息,高 2 位表示 SpecMode,低 30 位表示 SpecSize。

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

    /** @hide */
    @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
    @Retention(RetentionPolicy.SOURCE)
    public @interface MeasureSpecMode {}

    /**
     * Measure specification mode: The parent has not imposed any constraint
     * on the child. It can be whatever size it wants.
     */
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;

    /**
     * Measure specification mode: The parent has determined an exact size
     * for the child. The child is going to be given those bounds regardless
     * of how big it wants to be.
     */
    public static final int EXACTLY     = 1 << MODE_SHIFT;

    /**
     * Measure specification mode: The child can be as large as it wants up
     * to the specified size.
     */
    public static final int AT_MOST     = 2 << MODE_SHIFT;

    /**
     * Creates a measure specification based on the supplied size and mode.
     *
     * The mode must always be one of the following:
     * <ul>
     *  <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li>
     *  <li>{@link android.view.View.MeasureSpec#EXACTLY}</li>
     *  <li>{@link android.view.View.MeasureSpec#AT_MOST}</li>
     * </ul>
     *
     * <p><strong>Note:</strong> On API level 17 and lower, makeMeasureSpec's
     * implementation was such that the order of arguments did not matter
     * and overflow in either value could impact the resulting MeasureSpec.
     * {@link android.widget.RelativeLayout} was affected by this bug.
     * Apps targeting API levels greater than 17 will get the fixed, more strict
     * behavior.</p>
     *
     * @param size the size of the measure specification
     * @param mode the mode of the measure specification
     * @return the measure specification based on size and mode
     */
    public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << View.MeasureSpec.MODE_SHIFT) - 1) int size,
                                      @MeasureSpecMode int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }

    /**
     * Extracts the mode from the supplied measure specification.
     *
     * @param measureSpec the measure specification to extract the mode from
     * @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
     *         {@link android.view.View.MeasureSpec#AT_MOST} or
     *         {@link android.view.View.MeasureSpec#EXACTLY}
     */
    @MeasureSpecMode
    public static int getMode(int measureSpec) {
        //noinspection ResourceType
        return (measureSpec & MODE_MASK);
    }

    /**
     * Extracts the size from the supplied measure specification.
     *
     * @param measureSpec the measure specification to extract the size from
     * @return the size in pixels defined in the supplied measure specification
     */
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
}

可通过该类的 makeMeasureSpec 方法构造一个包含 View 尺寸、规格信息的 int 值,通过 getMode、getSize 方法分别获取指定的 int 值中包含的规格、尺寸信息。

其实,MeasureSpec 没有特别的含义,只是一个将 View 尺寸和规格信息打包和解包成具体 int 值的工具类。

来看一下 SpecMode 包含的三种类型:

模式 说明
EXACTLY 精确模式,表示父视图希望子视图的大小应该由 SpecSize 决定,ViewGroup 默认采用该规则设置子 View 的大小。实际开发中,开发人员可灵活设置 View 的大小,不必依赖系统默认的这一规则。match_parent、固定值(如 20dp)一般对应该模式,当然,前面已经说了,开发人员可以灵活设置 View 大小和规格。也就是说,不管父 View 的大小和模式如何,只要达到开发目的,可以设置子 View 为任何模式和大小。
AT_MOST 至多模式,表示子视图的大小最好不要超过 SpecSize 指定的大小。和 EXACTLY 模式一样,开发人员可自由设置 View 的大小和规格,但是这样可能出现 View 显示不全,具体要结合实际开发过程中实现的功能而定。一般而言,wrap_content对应该模式。
UNSPECIFIED 未指定模式,表示开发人员可设置 View 的任意大小和规格,一般用于系统,实际开发过程中应用场景不多,可以忽略。

2.2 LayoutParams

我们对 LayoutParams 一点也不陌生,但是关于 View 的测量过程,必须要了解一下 LayoutParams 至关重要的作用:

我们知道,一个子视图的宽高不仅取决于其自身的 LayoutParams 属性,也取决于父 view 的大小。
其中,父 View 的大小对应 MeasureSpec 的 int 值。所以,子 View 的 MeasureSpec 则由父 View 的 MeasureSpec 和其自身的 LayoutParams 共同决定。

那么,MeasureSpec 的 int 值第一次是如何创建的呢?其实,是在 ViewRootImpl 的 measureHierarchy 方法中,performMeasure 时创建的:

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

而此时的 DecorView 由于是顶层 View,所以其大小取决于整个应用程序窗口的大小和自身的 LayoutParams。

上面是顶层 View 的 MeasureSpec 创建过程,对于普通 View 而言,ViewGroup 默认测量子 View 的方法 measureChild 中调用了 getChildMeasureSpec 方法,而 get 方法决定了子 View MeasureSpec int 值 的生成过程:

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);
}
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 == ViewGroup.LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == ViewGroup.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 == ViewGroup.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 == ViewGroup.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 == ViewGroup.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 == ViewGroup.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);
}

而最终通过 getChildMeasureSpec 方法生成的 int 值就是子 View 的 onMeasure 方法的两个入参:

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

根据 getChildMeasureSpec 方法可得出 View 的 MeasureSpec 的创建规则:

MeasureSpec 的创建规则

也就是说,该方法反映了父视图的 MeasureSpec 和子视图的 LayoutParams 共同产生子 View 中 onMeasure 方法的两个入参的值的过程。

从中可以看出,当子视图的 LayoutParams 是 wrap 时,Android 默认传递了 parentSize 给了子 View。结合 View 的 onMeasure 方法可知,此时 View 默认的尺寸被设置了父 View 的大小。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
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;
}

所以,当开发人员实现一个继承于 View 的自定义 View 的时候,若直接使用默认的 getDefaultSize 方法,则该自定义 View 的 LayoutParams 是 wrap 时,总是充满了父布局。

2.3 View 测量的灵活性

由此,View 的测量完全可以依赖 Android 系统提供的默认测量规则进行,也可以按照自己的测量规则设置 View 的宽高。

总而言之,一个 View 的大小由 onMeasure 传递进来的两个 MeasureSpec 的 int 值决定的,我们可以根据这个 int 值自由设置 View 的大小,这是第一层的改变。

只不过传递进来的子 View 的 int 值,由父 View 在测量自身尺寸时遍历测量子 View 的过程中通过 getChildMeasureSpec 方法计算得出的。getChildMeasureSpec 方法是默认实现,也是上述“MeasureSpec 的创建规则”表格中的反映。当然,我们完全可以打破这个方法,完全不按照表格中的规则计算子 View 的 MeasureSpec,这是第二层改变。

再者,通常情况下建议使用 getChildMeasureSpec 的默认实现,但是传递进来的尺寸信息可考虑 padding、margin 的情况,灵活取用,这是第三层的改变。

由此可以看出,View 的整个测量过程相对 layout、draw 的流程来说比较复杂。

三、layout 过程

和测量过程一样,layout 方法决定自身的位置,而 onLayout 方法决定子 View 的位置,层层递进。

layout 的过程就是确定 View 的 mTop、mBottom、mLeft、mRight 的值的过程,也进一步为 draw 流程提供了基本的绘制依据。

四、draw 过程

layout 过程中测算出的位置信息为 draw 提供了绘制基础。draw 过程中,通过 onDraw 方法绘制自身,通过 dispatchDraw 方法绘制子 View。

在 dispatchDraw 方法中,ViewGroup 会遍历调用子 View 的 draw 方法:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}

最后会走到 onDraw 方法,由开发人员自行实现 View 的绘制。

// 未完待续...

本文由 Fynn_ 原创,未经许可,不得转载!

上一篇 下一篇

猜你喜欢

热点阅读