View的绘制-measure流程详解

栏目: IOS · Android · 发布时间: 5年前

内容简介:用于测量View的宽高,在执行 layout 的时候,根据测量的宽高去确定自身和子 View 的位置。在 measure 过程中,设计到 LayoutParams 和 MeasureSpec 这两个知识点。 这里我们简单说一下,如果还有不明白之处,Google it!简单来说就是布局参数,包含了 View 的宽高等信息。每一个 ViewGroup 的子类都有相对应的 LayoutParams,如:LinearLayout.LayoutParams、RelativeLayout.LayoutParams。可以
View的绘制-measure流程详解

作用

用于测量View的宽高,在执行 layout 的时候,根据测量的宽高去确定自身和子 View 的位置。

基础知识

在 measure 过程中,设计到 LayoutParams 和 MeasureSpec 这两个知识点。 这里我们简单说一下,如果还有不明白之处,Google it!

LayoutParams

简单来说就是布局参数,包含了 View 的宽高等信息。每一个 ViewGroup 的子类都有相对应的 LayoutParams,如:LinearLayout.LayoutParams、RelativeLayout.LayoutParams。可以看出 LayoutParams 是 ViewGroup 子类的内部类。

含义
LayoutParams.MATCH_PARENT 等同于在 xml 中设置 View 的属性为 match_parent 和 fill_parent
LayoutParams.WRAP_CONTENT 等同于在 xml 中设置 View 的属性为 wrap_content

MeasureSpec

MeasureSpec 是 View 的测量规则。通常父控件要测量子控件的时候,会传给子控件 widthMeasureSpec 和 heightMeasureSpec 这两个 int 类型的值。这个值里面包含两个信息, SpecModeSpecSize 。一个 int 值怎么会包含两个信息呢?我们知道 int 是一个4字节32位的数据,在这两个 int 类型的数据中,前面高2位是 SpecMode ,后面低30位代表了 SpecSize

View的绘制-measure流程详解
mode 有三种类型: UNSPECIFIEDEXACTLYAT_MOST
测量模式 应用
EXACTLY 精准模式,当 width 或 height 为固定 xxdp 或者为 MACH_PARENT 的时候,是这种测量模式
AT_MOST 当 width 或 height 设置为 warp_content 的时候,是这种测量模式
UNSPECIFIED 父容器对当前 View 没有任何显示,子 View 可以取任意大小。一般用在系统内部,比如:Scrollview、ListView。

我们怎么从一个 int 值里面取出两个信息呢?别担心,在 View 内部有一个 MeasureSpec 类。这个类已经给我们封装好了各种方法:

//将 Size 和 mode 组合成一个 int 值
int measureSpec = MeasureSpec.makeMeasureSpec(size,mode);
//获取 size 大小
int size = MeasureSpec.getSize(measureSpec);
//获取 mode 类型
int mode = MeasureSpec.getMode(measureSpec);
复制代码

具体实现细节,可以查看源码,or Google it!

执行流程

注:以下涉及到源码的,都是版本27的。

我们知道,一个视图的根 View 是 DecorView。在我们开启一个 Activity 的时候,会将 DecorView 添加到 window 中,同时会创建一个 RootViewImpl对象,并将 RootViewImpl 对象和 DecorView 对象建立关联。RootViewImplement 是连接 WindowManager 和 DecorView 的纽带。具体 DecorView 详解可以看这篇文章

View的绘制流程就是从 RootViewImpl 开始的。在它的 performTraversals() 方法中执行了 performMeasure()performLayoutperformDraw 方法。而这三个方法又分别执行了 view.measure()view.layout()view.draw() 方法,从而开始执行整个 View 树的绘制流程

View的绘制-measure流程详解

ViewGroup 中 measure 的执行流程

ViewGroup 本身是继承 View 的,这是我们大家都知道的。在 ViewGroup 中并没有找到 measure 方法,那么就在它的父类 View 中找,具体源码如下:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    /*....省略代码....*/
    if (forceLayout || needsLayout) {
     /*....省略代码....*/
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            // measure ourselves, this should set the measured dimension flag back
            //执行 onMeasure 方法
            onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
        /*....省略代码....*/
     
    }
    /*....省略代码....*/
}
复制代码

我们可以看出,measure 方法是被 final 修饰了,子类不能重写。measure 方法中调用了 onMeasure 方法。

然后我们继续寻找 onMeasure 方法,会发现在 ViewGroup 中并没有实现 onMeasure 方法,只有在 View 中发现了 onMeasure 方法。WTF?难道 ViewGroup 的 onMeasure 也会走 View 中的方法?并不是的,ViewGroup 本身是一个抽象类,在 Android SDK 中有很多它的子类,如:LinearLayout、RelativeLayout、FrameLayout等等,这些控件的特性都是不一样的,测量规则自然也都不一样。它们都各自实现了 onMeasure 方法,然后去根据自己的特定测量规则进行控件的测量。(PS:如果我们的自定义控件继承 ViewGroup 的时候,一定要重写 onMeasure 方法的,根据需求来制定测量规则)

这里我们以 LinearLayout 为例,来进行源码分析:

//LinearLayout 类
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
    //如果方向是垂直方向,就进行垂直方向的测量
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
    //进行水平方向的测量
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}
复制代码

measureVertical 和 measureHorizontal 过程类似,我们对 measureVertical 进行分析。(以下源码有所删减)

//LinearLayout 类
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    mTotalLength = 0;
    float totalWeight = 0;

    final int count = getVirtualChildCount();
    //获取 LinearLayout 的宽高模式 SpecMode
    final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

    boolean skippedMeasure = false;

    // See how tall everyone is. Also remember max width.
    //遍历子 View ,查看每一个子类有多高,并且记住最大的宽度。
    for (int i = 0; i < count; ++i) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
        //measureNullChild() 恒返回 0,
            mTotalLength += measureNullChild (i);
            continue;
        }
        //如果子控件时 GONE 状态,就跳过,不进行测量。
        //也可以看出,如果子 View 是 INVISIBLE 也是要测量大小的。
        if (child.getVisibility() == View.GONE) {
        //getChildrenSkipCount 也是恒返回为 0 的。
           i += getChildrenSkipCount(child, i);
           continue;
        }

        //获取子控件的参数信息。
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();

        totalWeight += lp.weight;
        //子控件是否设置了权重 weight 
        final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
        if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
            final int totalLength = mTotalLength;
            mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
            //如果设置了权重,就将 skippedMeasure 标记为 true。
            //后面会根据 skippedMeasure 的值和其他条件来决定是否进行重新绘制。
            //所以说,在 LinearLayout 中使用了 weight 权重,会导致测量两次,比较耗时。
            //可以考虑使用 RelativeLayout 或者 ConstraintLayout
            skippedMeasure = true;
        } else {
            if (useExcessSpace) {
                lp.height = LayoutParams.WRAP_CONTENT;
            }

           //计算已经使用过的高度
            final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
            /*这句代码是关键,从字面意思就可以理解出,该方法是在 layout 
            之前进行子 View 的测量。*/
            measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                    heightMeasureSpec, usedHeight);
        }
    }
}
复制代码

那么我们在查看 measureChildBeforeLayout 方法:

//LinearLayout 类
void measureChildBeforeLayout(View child, int childIndex,
        int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
        int totalHeight) {
    measureChildWithMargins(child, widthMeasureSpec, totalWidth,
            heightMeasureSpec, totalHeight);
}
复制代码

再查看 measureChildWithMargins 方法,最终来到了 ViewGroup 类:

//ViewGroup 类
protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
        /*获取子 View 的布局参数 MarginLayoutParams 可以获取子 View 
        设置的 margin 属性。*/
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    //获取子 View 宽度的 MeasureSpec 值。
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    //获取子 View 高度的 MeasureSpec 值。
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
复制代码

在 ViewGroup 中还有一个方法为 measureChild(int widthMeasureSpec, int heightMeasureSpec) 。这个方法和 measureChildWithMargins 作用一致,都是生成子 View 的 measureSpec。只是传参不同。

里面在获取子 View 宽高属性的时候,都是通过 getChildMeasureSpec 方法来获取的。这个方法是 ViewGroup 具体实现根据自身的 measureSpec 和子 View 的 LayoutParams 来设置子 View 的 measureSpec 的主要过程。

//ViewGroup 类
/**
 * @param spec 父类的 measureSpec
 * @param padding 父类的 padding + 子类的 margin
 * @param childDimension 子 View 的 LayoutParams.width/LayoutParams.height 属性
 */
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    //获取父控件的测量模式 specMode
    int specMode = MeasureSpec.getMode(spec);
    //获取父控件的测量大小 SpecSize
    int specSize = MeasureSpec.getSize(spec);
    //获取父控件剩余的宽度/高度大小
    int size = Math.max(0, specSize - padding);
    //子 View 的测量大小
    int resultSize = 0;
    //子 View 的测量模式
    int resultMode = 0;

    switch (specMode) {
    // 父控件的宽高模式是精准模式 EXACTLY
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            //如果子 View 的宽/高是具体的值(具体的 xxdp/px)
            //模式 mode 就设置为精准模式 EXACTLY,大小 size 就是具体设置的大小
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            //如果子 View 的宽/高是 MATCH_PARENT
            //模式 mode 就设置为精准模式 EXACTLY,大小 size 就是父控件剩余的空间
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            //如果子 View 的宽/高是 WRAP_CONTENT
            /*模式 mode 就设置为精准模式 AT_MOST,大小 size 就是父控件剩余的空间,
            子控件可以在在这个size大小范围内设置宽高*/
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent has imposed a maximum size on us
    //父控件测量模式为 AT_MOST,会给子 View 一个最大的值
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            //如果子 View 的宽/高是具体的值(具体的 xxdp/px)
            //模式 mode 就设置为精准模式 EXACTLY,大小 size 就是具体设置的大小
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            //如果子 View 的宽/高是 MATCH_PARENT
            /*模式 mode 就设置为精准模式 AT_MOST,大小 size 就是父控件剩余的空间,
            子控件可以在在这个size大小范围内设置宽高*/
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            //如果子 View 的宽/高是 MATCH_PARENT
            /*模式 mode 就设置为精准模式 AT_MOST,大小 size 就是父控件剩余的空间,
            子控件可以在在这个size大小范围内设置宽高*/
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent asked to see how big we want to be
    //父控件不限制子 View 的宽高,一般用于 ListView、Scrollview
    //平时基本不用,暂不分析
    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;
    }
    //生成子 View 的 measSpec
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
复制代码

以上就是 ViewGroup 根据自身 measureSpec 和 子 View 的 LayoutParams 生成子 View 的 measureSpec 的过程。具体总结如下:

View的绘制-measure流程详解

以上就是 LinearLayout 测量子控件宽高的过程。

从上述表格我们也可以看出,当我们在自定义控件继承 View 的时候,还是要重写 View 的 onMeasure 方法来处理 wrap_content 的情况,如果不处理 wrap_content 的情况,wrap_content 的效果是和 match_parent 一样的,都是填充满父控件。可以在 xml 布局中直接添加一个 <View android:layout_width="match_parent" android:layout_height="wrap_content"/> 控件自行感受一下。

LinearLayout 测量完子控件后,根据子控件的宽高来设置自身的宽高:

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    // Add in our padding
    //添加自身的 padding 值
    mTotalLength += mPaddingTop + mPaddingBottom;

    int heightSize = mTotalLength;

    // Check against our minimum height
    //从 最小建议高度 和 heightSize 中取最大值,getSuggestedMinimumHeight 在后面有分析
    heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

    int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
    /*....省略代码....*/
    //遍历完子控件后,来设置自身的宽高
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            heightSizeAndState);
}
复制代码
//如果 LinearLayout 高为具体值,heightSizeAndState 就是具体的值
//否则是 子控件 的高度之和,但是也不能超过它的父容器的剩余空间。
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);
}
复制代码

至此,我们可以得知,当 ViewGroup 生成子 View 宽/高的 measureSpec 后,开始调用子 View 进行测量。如果子 View 继承了 ViewGroup 就重复执行上述流程(各个不同的 ViewGroup 子类执行各自的 onMeasure 方法);如果是具体的 View,就开始执行具体 View 的 measure 过程。最后根据子控件的宽高和其他条件来决定自身的宽高。

View 中 measure 的执行流程

View 的 measure 具体源码在 ViewGroup 中已经分析过,这里主要分析 View 的 onMeasure 过程。

//View 类
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //通过 getDefaultSize 获取宽高大小,设置为测量值。
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
复制代码

getDefaultSize 具体源码

//View 类
/**
 * @param size 通过 getSuggestedMinimumWidth 获取的建议最小宽度
 * @param measureSpec 通过父控件生成的 measureSpec
 */
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:
    //如果是 UNSPECIFIED 就设置为建议最小值
        result = size;
        break;
    /*否则就都设置为通过父控件生成的值(如果子控件为具体的
    xxdp/px值,就是具体的值,如果不是就是父控件的剩余空间。具体可以查看上面的分析)*/
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}
复制代码

//建议最小的值

//View 类
protected int getSuggestedMinimumWidth() {
    //判断是否有设置背景 Background 如果没有,建议最小值就是设置的 minWidth;
    //如果有,就取 mMinWidth 和 背景最小值 两者的最大值。
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
复制代码

背景最小值是多少呢?点击查看源码,就来到了 Drawable 类。

//Drawable 类
public int getMinimumWidth() {
    //首先获取 Drawable 的原始宽度
    final int intrinsicWidth = getIntrinsicWidth();
    //如果有原始宽度,就返回原始宽度;如果没有,就返回 0
    //注: 比如 ShapeDrawable 就没有原始宽度,BitmapDrawable 有原始宽高(图片尺寸)
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
复制代码

至此,View的 measure 就分析完了。

DecorView 的 measureSpec 计算逻辑

可能我们会有疑问,如果所有子控件的 measureSpec 都是父控件结合自身的 measureSpec 和子 View 的 LayoutParams 来生成的。那么作为视图的顶级父类 DecorView 怎么获取自己的 measureSpec 呢?下面我们来分析源码:(以下源码有所删减)

//ViewRootImpl 类
private void performTraversals() {
    //获取 DecorView 宽度的 measureSpec 
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    //获取 DecorView 高度的 measureSpec
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    // Ask host how big it wants to be
    //开始执行测量
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
复制代码
//ViewRootImpl 类
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
    }
    return measureSpec;
}
复制代码

windowSize 是 widow 的宽高大小,所以我们可以看出 DecorView 的 measureSpec 是根据 window 的宽高大小和自身的 LayoutParams 来生成的。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Artificial Intelligence

Artificial Intelligence

Stuart Russell、Peter Norvig / Pearson / 2009-12-11 / USD 195.00

The long-anticipated revision of this #1 selling book offers the most comprehensive, state of the art introduction to the theory and practice of artificial intelligence for modern applications. Intell......一起来看看 《Artificial Intelligence》 这本书的介绍吧!

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具