首页 > 代码库 > 对于Android View绘制的一些思考

对于Android View绘制的一些思考

 
AT_MOST
表示最大是多大.
UNSPECIFIED
不确定是多大, 你想多大就多大,我尽量满足你.
EXACTLY
就这么大 已经指定了大小
MATCH_PARENT 为什么说MATCH_PARENT 因为 MATCH_PARENT 就是间接说 我要占据父控件剩下的那部分了。
这就相当于指定了确定的宽或高。
一个view 的绘制需要三个阶段 measure -> layout -> draw
view的测量阶段
measure-> onMeasure() 我们就看单纯一个view的测量 测量自身的大小
它需要有两个参数
widthMeasureSpec
heightMeasureSpec
这两个参数 是由他们所在的父容器传给他们的,父控件是要传递多大呢? 这是一个给view自己测量时候的
参考值,肯定不能乱给。我的问题是 父容器会传多少?为什么会传递这么大?先记住这个问题(1)
在这里面我需要调用下面的方法,来设置测量的值,调用这个方法,就表示该view 在测量这一阶段完成了。
setMeasuredDimension(width, height)
注意在基类 View onMeasure() 方法调用是这样的:
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
这里面 View 是一个通俗的实现,就是默认值,意思要么使用父控件指定的( xxxMeasureSpec),要么自己测量(getSuggestedMininumxxx)。
然后,我们就看了一下getDefaultSize的源码,看一下是如何选择选择,这个源码很简单。
 /**
     * Utility to return a default size. Uses the supplied size if the
     * MeasureSpec imposed no constraints. Will get larger if allowed
     * by the MeasureSpec.
     *
     * @param size Default size for this view
     * @param measureSpec Constraints imposed by the parent
     * @return The size this view should be.
     */
    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() 的实现。 我们可以得到这么个结论。
除了unspecified 模式之外 其他模式(exactly, at_most)都用了父容器传递过来的值。也就是父容器指定的值。原来
父容器给的参考值这么重要。
话又说回来 什么叫父指定的值,为什么要指定值,自己测量自己不就行了么?那么多事.这个问题也很好理解:
同志们都知道 view(比如 常用的TextView等) 一般情况下 在xml 文件中定义中view 必然是有父布局的
并且view的宽高也是必须指定(wrap_content、
match_parent 、固定值) 不指定的话编译都过不了。而传递的参考值就是在这里设定的值。于是,对于问题1
我基本是想通了。so 现在想一下,我们是如何动态创建一个view,并将其放到一个布局里面的,代码如下
LinearLayout ll = (LinearLayout)findViewById(R.id.ll);
TextView text_view = new TextView(this);
ll.add(text_view);
TextView text_view = new TextView(this);
text_view.setText("计划好了再娶吧");
text_view.setBackgroundColor(Color.RED);
ll.addView(text_view);
此时你看到了所显示的。不要激动!
注意对于LinearLayout 来说如果是VERTICAL 那么view默认生成的 LayoutParams 宽度是match_parent.
这里为什么要说下动态生成与添加view,在这里我就是想说这个LayoutParams,我们可以为我们的
view 设置LayoutParams 也可以用系统默认的,系统默认的就是上层父控件默认的,这里不设置就是用我们LinearLayout 默认给子view生成的。
不管怎么实现,我们的父控件 会拿到了我们的LayoutParams 里面设置的各种参数。
然后 呢 ?--requestLayout 继续三个过程,并且是在父控件(我们的LinearLayout)的3个过程measure -> layout -> draw
于是我们就可以 开始 measure ,但是父控件调用measure 必然是为了测量自身,这是必须明白的。 measure->onMeasure
真正的测量在onMeasure中开始了。在这里面会测量每一个子view 的大小。
protected void measureChildWithMargins(View child,
                       int parentWidthMeasureSpec,
                                    int widthUsed,
                      int parentHeightMeasureSpec,
                                   int heightUsed)
必须得看一下源码(因为不是太长)
 /**
     * 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);
    }
通过源码我们可以看出这个方法所做的事情
1、得到子view 的MarginLayoutParams
2、确定子view 长宽的参考值,调用getChildMeasureSpec(...)
3、调用子view 的 measure 并传入参考值 child.measure(childWidthMeasureSpec, childHeightMeasureSpec) 进入子view测量阶段

看一下getChildMeasureSpec(...)的源码(稍微有点长,但是人的好奇心促使我们继续前行,因为我们必须知道给子view的参考值是怎么来的,注释好好看,我都加粗了):
  /**
     * 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);
        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);
    }
public static int getChildMeasureSpec(int spec, int padding, int childDimension)
spec: 父控件的测量值,主要是为得到 父控件相关的规格(mode)
padding : 父控件中已经被使用的部分
childDimension 我们通过LayoutParams 设定的值(match_parent、wrap_content 或者固定值) 注意仅仅是尺寸
这里并没有包括相关的mode值,因我就是为了得到子view 的mode值
1、计算剩余空间 int size = Math.max(0, specSize - padding);
2、拿到父控件的 specMode 与 specSize
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
对于 接下来代码,就是通过父控件的specMode 和 子view 自身的LayoutParams值来确定 子view大小的参考值和规格
但是源码中的几句英文注释,对于我们来说理解分析at_most exactly unspecified 不同有很大关系。
我理解是这样的:
每一种mode都有 一个size 与之对应 mode 就是说明了size这个参数表示的意义
at_most    at_most this size
exactly   exactly this size
unspecified    unspecified this size
现在我们测量完成之后 每一个 子view 的大小,得到所需要参考值(mode + size)。
然后测量完所有子view之后,我们就可以调用 child.getMeasuredWidth() 方法得到每一个子view宽度。
通过计算各个子view 的宽度,和自身的padding 我们就得到了我们父容器的测量值。于是我们就可以大胆的
setMeasuredDimension 来设置自己(LinearLayout)的测量的高度。但这仅仅是测量大小,还有layout 呢!
layout 是如何开始的肯定是从layout(),在这里面如果发现位置发生了变化,那么就会onLayout()方法
并且会通知所有注册 OnLayoutChangeListener 的类,又是观察模式。来到了onLayout() 发现是空的,
当然是空的。这是给有子view 的来用的啊。于是看下ViewGroup 中源码的实现,不过还是抽象方法。因为
这需要具体的布局来自己实现。于是选择LinearLayout来看onLayout的具体实现。分析留到下一次~

题外话:
比如 对于一个TextView TextView.measure(0, 0); 会知道TextView 的长宽。
TextView 在测量的时候(测量很复杂,因为是自身的测量),是一定会参考一下父控件传过来的值,并且
只有mode为exactly 或者 at_most 时候才会有所参考,否则就是,按照自己实际情况测量的结果。因为
传递时俩0,那么mode 就是unspecified 因此就会自己测量自己的大小。也就是最小宽度和高度。最小高度
和最小宽度。可以在xml中 android:minHeight 与 android:minWidth 来设置。
不设置就用背景的宽度和高度。

对于Android View绘制的一些思考