View绘制原理 —— 画多大?

这是Android视图绘制系列文章的第一篇,系列文章目录以下:算法

  1. View绘制原理——画多大?
  2. View绘制原理——画在哪?
  3. View绘制原理——画什么?
  4. 读源码,懂原理,有什么用?写业务代码又用不到?—— 自定义换行容器控件

若是想直接看结论能够移步到第三篇末尾。bash

View绘制就比如画画,先抛开Android概念,若是要画一张图,首先会想到哪几个基本问题:app

  • 画多大?
  • 画在哪?
  • 怎么画?

Android绘制系统也是按照这个思路对View进行绘制,上面这些问题的答案分别藏在less

  • 测量(measure)
  • 定位(layout)
  • 绘制(draw)

这一篇将以源码中的几个关键函数为线索分析“测量(measure)”。ide

View.measure()

“测量”要解决的问题是肯定待绘制View的尺寸,以View.measure()为入口,一探究竟:函数

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    /**
     * <p>
     * This is called to find out how big a view should be. The parent
     * supplies constraint information in the width and height parameters.
     * 这个方法用于决定当前view到底有多大,父亲提供宽高参数起到限制大小的做用
     *
     * The actual measurement work of a view is performed in
     * {@link #onMeasure(int, int)}, called by this method. Therefore, only
     * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
     * 真正的测量工做在onMeasure()中进行
     *
     * @param widthMeasureSpec Horizontal space requirements as imposed by the
     *        parent(父亲施加的宽度要求)
     * @param heightMeasureSpec Vertical space requirements as imposed by the
     *        parent(父亲施加的高度要求)
     *
     * @see #onMeasure(int, int)
     */
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        ...
    }
    
    /**
     * Measure the view and its content to determine the measured width and the
     * measured height. This method is invoked by {@link #measure(int, int)} and
     * should be overridden by subclasses to provide accurate and efficient
     * measurement of their contents.
     * View子类应该重载这个方法以定义本身尺寸
     *
     * <strong>CONTRACT:</strong> When overriding this method, you
     * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
     * measured width and height of this view. Failure to do so will trigger an
     * <code>IllegalStateException</code>, thrown by
     * {@link #measure(int, int)}. Calling the superclass'
     * {@link #onMeasure(int, int)} is a valid use.
     * 重载方法必须调用 setMeasuredDimension()
     */
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
}
复制代码

从注释中得知这么几个信息:布局

  1. 真正的测量工做在onMeasure()中进行,View的子类应该重载这个方法以定义本身尺寸。
  2. onMeasure()中必须调用setMeasuredDimension()
  3. View会经过传入的宽高参数对子View的尺寸施加限制。

顺带便看了一下常见控件如何重载onMeasure(),其实套路都同样,无论是TextView仍是ImageView,在一系列计算得出宽高值后将传入setMeasuredDimension()。因此,整个测量过程的终点是View.setMeasuredDimension()的调用,它表示着视图大小已经有肯定值。post

ViewGroup.onMeasure()

View必然依附于一棵“View树”,那父View是如何对子View的尺寸施加影响的?全局搜索View.measure()被调用的地方,在不少ViewGroup类型的控件中发现相似child.measure()的调用,以最简单的FrameLayout为例:ui

public class FrameLayout extends ViewGroup {
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //得到孩子数量
        int count = getChildCount();
        ...
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            //遍历可见孩子或者强制遍历全部孩子
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                //测量孩子
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                //记忆孩子中最大宽度
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                //记忆孩子中最大高度
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

        ...

        //以最孩子中最大的尺寸做为本身的尺寸
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));

        ...
    }
}
复制代码

FrameLayout会遍历全部可见的孩子记忆其中最大宽度和最大高度,并以此做为本身的宽和高(这是FrameLayout的测量算法,其余的ViewGroup应该也有本身独特的测量算法。)this

ViewGroup.measureChildWithMargins()

父控件在遍历每一个孩子时会调用measureChildWithMargins()来测量孩子:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    /**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding
     * and margins. The child must have MarginLayoutParams The heavy lifting is
     * done in getChildMeasureSpec.
     * 要求孩子本身测量本身(考虑父亲的要求和本身的边距)
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view(来自父亲的宽度要求)
     * @param widthUsed Extra space that has been used up by the parent
     *        horizontally (possibly by other children of the parent)
     * @param parentHeightMeasureSpec The height requirements for this view(来自父亲的高度要求)
     * @param heightUsed Extra space that has been used up by the parent
     *        vertically (possibly by other children of the parent)
     */
    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);
    }
}
复制代码

读到这里应该能够总结出ViewGroup的测量过程: 遍历全部的孩子,经过调用View.measure()触发孩子们测量本身。测量完全部孩子以后,按照自有的测量算法将孩子们的尺寸转换成本身的尺寸并传入View.setMeasuredDimension()

ViewGroup.getChildMeasureSpec()

触发孩子测量本身的时候传入了宽高两个参数,它们是经过ViewGroup.getChildMeasureSpec()产生的:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        ...
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
        ...
        switch (specMode) {
        case MeasureSpec.EXACTLY:
            ...
            break;

        case MeasureSpec.AT_MOST:
            ...
            break;

        case MeasureSpec.UNSPECIFIED:
            ...
            break;
        }
        ...
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
}
复制代码

这个函数中有一个陌生的类MeasureSpec,点进去看看:

/**
     * A MeasureSpec encapsulates the layout requirements passed from parent to child.
     * Each MeasureSpec represents a requirement for either the width or the height.
     * A MeasureSpec is comprised of a size and a mode. There are three possible
     * modes:
     * MeasureSpec包装了父亲对孩子的布局要求,它是尺寸和模式的混合,它包含三种模式
     *
     * MeasureSpecs are implemented as ints to reduce object allocation.
     * MeasureSpec被实现成一个int值为了节约空间
     */
    public static class MeasureSpec {
        //前2位是模式,后30位是尺寸
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
            /**
         * 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;
        ...
    }
复制代码

MeasureSpec用于在View测量过程当中描述尺寸,它是一个包含了布局模式和布局尺寸的int值(32位),其中最高的2位表明布局模式,后30位表明布局尺寸。它包含三种布局模式分别是UNSPECIFIEDEXACTLYAT_MOST

结合刚才的ViewGroup.getChildMeasureSpec()来探究下这些模式究竟是什么意思:

/**
     * Does the hard part of measureChildren: figuring out the MeasureSpec to
     * pass to a particular child. This method figures out the right MeasureSpec
     * for one dimension (height or width) of one child view.
     * 得到孩子布局参数(宽或高):混合父亲要求和孩子诉求
     *
     * The goal is to combine information from our MeasureSpec with the
     * LayoutParams of the child to get the best possible results. For example,
     * if the this view knows its size (because its MeasureSpec has a mode of
     * EXACTLY), and the child has indicated in its LayoutParams that it wants
     * to be the same size as the parent, the parent should ask the child to
     * layout given an exact size.
     *
     * @param spec The requirements for this view 父亲要求:要求孩子多宽多高
     * @param padding The padding of this view for the current dimension and
     *        margins, if applicable
     * @param childDimension How big the child wants to be in the current
     *        dimension孩子诉求:想要多宽多高
     * @return a MeasureSpec integer for the child
     */
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        //得到父亲测量模式
        int specMode = MeasureSpec.getMode(spec);
        //得到父亲尺寸
        int specSize = MeasureSpec.getSize(spec);

        //从父亲尺寸中去除padding
        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        //结合父亲要求和孩子诉求计算出孩子尺寸,父亲有三种类型的要求,孩子有三种类型的诉求,孩子尺寸一共有9种结果。
        switch (specMode) {
        // Parent has imposed an exact size on us(父亲有明确尺寸)
        case MeasureSpec.EXACTLY:
            //若是孩子对本身尺寸有明确要求,只能知足它,不考虑padding
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            }
            //若是孩子要求和父亲同样大且父亲有明确尺寸,则孩子尺寸有肯定,考虑padding
            else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            }
            //若是孩子要求彻底显示本身内容,但它不能超过父亲,考虑padding
            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:
            //若是孩子对本身尺寸有明确要求,只能知足它,不考虑padding
            if (childDimension >= 0) {
                // Child wants a specific size... so be it(父亲其实很无奈)
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            }
            //若是孩子要求和父亲同样大,但父亲只有明确最大尺寸,则孩子也能有明确最大尺寸,考虑padding
            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;
            }
            //若是孩子要求彻底显示本身内容,但它不能超过父亲,考虑padding
            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:
            //若是孩子对本身尺寸有明确要求,只能知足它,不考虑padding
            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);
    }
复制代码

这个函数揭示了一个“人间真相”:父亲老是对孩子有要求,但孩子也老是有本身的诉求。最圆满的结局莫过于充分考量两方面的需求并调和之。ViewGroup.getChildMeasureSpec()将3种父亲的要求和3种孩子的诉求进行了调和(详见上述代码及注释)

总结

  • 父控件在测量本身的时候会先遍历全部子控件,并触发它们测量本身。完成孩子测量后,根据孩子的尺寸来肯定本身的尺寸。View绘制中的测量是一个从View树开始自顶向下的递归过程,递表示父控件触发子控件测量本身,归表示子控件完成测量后,父控件测量本身。
  • 父控件会将本身的布局要求和子控件的布局诉求结合成一个MeasureSpec对象传递给子控件以指导子控件测量本身。
  • MeasureSpec用于在View测量过程当中描述尺寸,它是一个包含了布局模式和布局尺寸的int值(32位),其中最高的2位表明布局模式,后30位表明布局尺寸。
  • 整个测量过程的终点是View.setMeasuredDimension()的调用,它表示着视图大小已经有肯定值。
相关文章
相关标签/搜索