美文网首页
View 工作原理(二)| 艺术探索笔记

View 工作原理(二)| 艺术探索笔记

作者: brickx | 来源:发表于2018-10-26 15:01 被阅读0次

    View 工作流程

    View 的工作流程包括 Measure、Layout、Draw 三个过程。其中 Measure 测量 View 的宽高,Layout 确定 View 最终宽高和四个顶点的位置,Draw 将 View 绘制到屏幕上。

    Measure

    Measure 过程分为两种情况:原始 View 和 ViewGroup。如果是原始 View,则在 measure 方法中完成测量过程。如果是 ViewGroup,除了完成了自己的测量过程,还会遍历并调用所有子元素的 measure 方法,然后子元素中再递归执行该流程。

    View 的 Measure 过程

    View 的 Measure 由 measure 方法完成,measure 方法中会调用 onMeasure 方法,来看 onMeasure 方法

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

    setMeasureDimension 方法会设置 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;
    }
    

    在 AT_MOST 和 EXACTLY 两种情况中,getDefaultSize 方法返回的就是 MeasureSpec 中的 SpecSize,而 SpecSize 是 View 测量后的大小。这里说的测量后的大小,指的是 Measure 过程中确定的大小,而最终的大小在 Layout 中确定,但大部分情况中,View 的测量大小与最终大小相等。

    UNSPECIFIED 的情况适用于系统内部测量过程。这种情况 getDefaultSize 返回的是传入的第一个参数,即为 getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 两个方法的返回值

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

    从 getSuggestedMinimumWidth 中可以看到,当 View 没有设置背景,那么宽度为 mMinWidth,mMinWidth 对应于 android:minWidth 这个属性值,如果这个属性不指定,则默认为 0。当 View 指定了背景,则宽度为 max(mMinWidth, mBackground.getMinimumWidth()),于是转到 Drawable 中的 getMinimumWidth 方法

    public int getMinimumWidth() {
        final int intrinsicWidth = getIntrinsicWidth();
        return intrinsicWidth > 0 ? intrinsicWidth : 0;
    }
    

    getMinimumWidth 返回的是 Drawable 的原始宽度(待补充),如果没有就返回 0。getSuggestedMinimumHeight 原理是一样的,这里不重复。

    从以上可知,当 View 没有设置背景,那么 getSuggestedMinimumWidth 返回 android:Width 的属性值,如果值没有指定,返回 0。当 View 设置了背景,那么返回 android:minWidth 和背景最小宽度这两者中的最大值。getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 的返回值就是 UNSPECIFIED 情况下的测量宽高。

    在 getDefaultSize 方法中可以看到,View 的宽高由 SpecSize 指定,所以直接继承 View 的自定义控件需要重写 onMeasure 方法并设置 wrap_content 时的自身大小,否则使用 wrap_content 就等于 match_parent。原因是,当布局使用 wrap_content,SpecMode 为 AT_MOST 模式,根据 View 工作原理(一)中普通 View 的 MeasureSpec 创建规则可知,这种情况下 View 的 SpecSize 为 parentSize,而 parentSize 就是当前父容器的剩余大小,即 View 的宽高等于父容器的剩余大小,这就导致了使用 wrap_content 相当于 match_parent。

    普通 View 的 MeasureSpec 创建规则

    解决方法是,当使用 wrap_content 时设置一个默认内部宽高,非 wrap_content 则使用系统的测量宽高。

    ViewGroup 的 Measure 过程

    在 ViewGruop 中,除了完成自己的 Measure 过程,还会遍历调用子元素的 measure 方法,所有子元素再递归执行这个过程。由于 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);
            }
        }
    }
    

    可以看到,它会对每一个子元素进行 measure,再看 measureChild 方法

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

    measureChild 取出子元素的 LayoutParams,再通过 getChildMeasureSpec 来创建子元素的 MeasureSpec,最后将 MeasureSpec 传给 View 的 measure 方法测量。getChildMeasureSpec 方法在 View 工作原理(一)中有分析。

    由于 ViewGroup 是抽象类,所以测量过程的 onMeasure 方法需要子类去实现,下面以 LinearLayout 的 onMeasure 方法为例,来继续分析 ViewGroup 的 Measure 过程。

    先看 LinearLayout 的 onMeasure 方法

    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,
            totalWeight == 0 ? mTotalLength : 0);
    
        if (oldHeight != Integer.MIN_VALUE) {
            lp.height = oldHeight;
        }
    
        final int childHeight = child.getMeasuredHeight();
        final int totalLength = mTotalLength;
        mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
            lp.bottomMargin + getNextLocationOffset(child));
    }
    

    代码中遍历了子元素并对每个都执行了 measureChildBeforeLayout 方法,该方法内部会调用子元素的 measure 方法,并且还会通过 mTotalLength 变量来存储 LinearLayout 在竖直方向的初步高度,其中主要是子元素的高度和子元素在竖直方向上的 margin 值。当子元素测量完毕,LinearLayout 会测量自己的大小。

    // 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;
    
    ...
    
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
        heightSizeAndState);
    

    对竖直方向的 LinearLayout 来说,水平方向的测量与 View 测量过程相同,竖直方向上,当布局中使用的是 match_parent 时,它与 View 测量过程一致,即高度为 specSize。如果是 wrap_content,那么高度是不超过父容器剩余空间下,所有子元素所占用的高度加上其他竖直方向的 padding。resolveSizeAndState 方法如下

    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 过程就完成了。这时可以通过 getMeasuredWidth 和 getMeasuredHeight 方法得到 View 的测量宽高。

    相关文章

      网友评论

          本文标题:View 工作原理(二)| 艺术探索笔记

          本文链接:https://www.haomeiwen.com/subject/qpoxtqtx.html