Android自定义View

View篇之Measure过程

2018-04-07  本文已影响28人  雾里看花六月天
  1. 简介

•View的绘制过程分为三部分:measure、layout、draw。

measure用来测量View的宽和高。
layout用来计算View的位置。
draw用来绘制View。

•本章主要对measure过程进行详细的分析。

  1. measure的始点

measure是从ViewRootImpl的performTraversals()方法开始的:

2.1 ViewRootImpl的performTraversals

private void performTraversals() {

    //...

    //获得view宽高的测量规格,mWidth和mHeight表示窗口的宽高,lp.widthhe和lp.height表示DecorView根布局宽和高
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//执行测量

    //...
}

首先会获取view宽高的测量规格,测量规格在下一节会详细讲述,然后就是调用performMeasure()了:

2.2 ViewRootImpl的performMeasure

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {

    //...

    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);

    //...
}

performMeasure()中就是调用View的measure()方法开始进行测量。

3.MeasureSpec

了解measure的过程时,我们须先了解MeasureSpec。MeasureSpec,顾名思义,就是测量规格,其决定了一个View的宽和高。

3.1 MeasureSpec组成

MeasureSpec代表一个32位的int值,前2位代表SpecMode,后30位代表SpecSize。其中:SpecMode代表测量的模式,SpecSize值在某种测量模式下的规格大小。来张图解说明:


测量说明.png

3.2 SpecMode测量模式

测量模式分为三种UNSPECIFIED、EXACTLY、AT_MOST,其具体说明如下表所示:

测量模式 说明 应用场景
UNSPECIFIED (未指定) 父容器没有对当前View有任何限制,当前View可以任意取尺寸 系统内部
EXACTLY(精确) 父容器已经确定当前View的大小,无论View想要多大都会在这范围内 match_parent 和 体数值
AT_MOST(最多) 当前View不能超过父容器规格大小,具体数值由view去决定 wrap-content

3.3 MeasureSpec源码分析

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;//模式移位数
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;//模式掩码

    //UNSPECIFIED模式:父容器没有对当前View有任何限制,当前View可以任意取尺寸
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;

    //EXACTLY模式:父容器已经确定当前View的大小,无论View想要多大都会在这范围内
    public static final int EXACTLY     = 1 << MODE_SHIFT;

    //AT_MOST模式:当前View不能超过父容器规格大小,具体数值由view去决定
    public static final int AT_MOST     = 2 << MODE_SHIFT;

    //根据提供的size和mode得到一个测量规格
    public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                      @MeasureSpecMode int mode) {
        //sUseBrokenMakeMeasureSpec = targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR1;
        //即targetSdkVersion<=17时,size与mode是直接相加的;>17则进行位运算
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);//位运算
        }
    }

    //根据提供的size和mode得到一个测量规格 
    public static int makeSafeMeasureSpec(int size, int mode) {
        if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
            return 0;
        }
        return makeMeasureSpec(size, mode);
    }

    //获取测量模式
    @MeasureSpecMode
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }

    //获取测量大小
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }

}

可以看到,MeasureSpec类还是挺简单的,MeasureSpec类通过将mode和size打包成一个32位int值来减少了对象内存分配,并提供了打包和解包的方法。

3.4 确定MeasureSpec值

在measure过程中,系统会将View的LayoutParams和父容器所施加的规则转换成对应的MeasureSpec,然后在onMeasure()方法中根据这个MeasureSpec来确定View的测量宽高。父容器所施加的规则对于DecorView与普通View是不同的,我们分开来看。

3.4.1 确定DecorView的MeasureSpec值

DecorView,作为顶级的View,我们平时setContentView()所设置的布局可能只是DecorView其中的一部分,如下图所示:


DecorView、ContentView、标题栏图解

3.4.1.1 ViewRootImpl的PerformTraveals

在ViewRootImpl的PerformTraveals()方法中会获得DecorView的MeasureSpec值:

private void performTraversals() {

    //...

    //获得view宽高的测量规格,mWidth和mHeight表示窗口的宽高,lp.widthhe和lp.height表示DecorView根布局宽和高
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//执行测量

    //...
}

3.4.1.2 ViewRootImpl的getRootMeasureSpec

我们再来看看getRootMeasureSpec()方法:

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {

    case ViewGroup.LayoutParams.MATCH_PARENT:
        //如果View的布局参数为MATCH_PARENT,则其为精确模式,大小为窗口的大小
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
        break;
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        // Window can resize. Set max size for root view.
        //如果View的布局参数为WRAP_CONTENT,则其为最多模式,大小不定,但不能超过窗口的大小
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    default:
        // Window wants to be an exact size. Force root view to be that size.
        //如果View的布局参数为准确的数值,如100dp等,则其为精确模式,大小为View设定的数值
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}

可以看到:DecorView的MeasureSpec值是由窗口大小及其布局参数来决定的。

来张表格总结:

布局参数 LayoutParams.MATCH_PARENT LayoutParams.WRAP_CONTENT 准确数值
MeasureSpec值 EXACTLY windowSize(窗口大小) AT_MOST windowSize(不能超过窗口大小) EXACTLY rootDimension(准确数值)

3.4.2 确定普通View的MeasureSpec值

普通View的MeasureSpec值通过getChildMeasureSpec()方法来获取,getChildMeasureSpec()位于ViewGroup中:

3.4.2.1 ViewGroup的getChildMeasureSpec

/**
 *
 * @param spec 父容器的测量规格(MeasureSpec)
 * @param padding 子view的内边距和外边距(padding,margin) 
 * @param childDimension 子view的布局参数(宽/高)
 * @return 子view的测量规格(MeasureSpec)
 */
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    //父容器的测量模式
    int specMode = MeasureSpec.getMode(spec);
    //父容器的测量大小
    int specSize = MeasureSpec.getSize(spec);

    //子view大小 = 父容器大小-子view边距(子view只在某些地方地方用到这个值,具体看下面的代码)
    int size = Math.max(0, specSize - padding);

    //初始化子view的大小和模式(最终的结果需要下面代码计算出来) 
    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {

    case MeasureSpec.EXACTLY://当父容器模式为EXACTLY时
        if (childDimension >= 0) {// 当子view的LayoutParams>0,即有确切的值
            //子view大小为子自身所赋的值,模式大小为EXACTLY
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {// 当子view的LayoutParams为MATCH_PARENT时(-1)
            //子view大小为父容器剩余大小,模式为EXACTLY
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {// 当子view的LayoutParams为WRAP_CONTENT时(-2)
            //子view决定自己的大小,但最大不能超过父容器剩余大小,模式为AT_MOST
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    case MeasureSpec.AT_MOST: // 当父容器的模式为AT_MOST时
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;


    case MeasureSpec.UNSPECIFIED:// 当父容器的模式为UNSPECIFIED时
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            //这里的sUseZeroUnspecifiedMeasureSpec值为targetSdkVersion < Build.VERSION_CODES.M;即<23为true,否则为false.
            //大于等于23时会将size作为提示值,否则为0
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }

    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

可以看到:普通View的MeasureSpec值是由父容器的MeasureSpec及其布局参数来决定的。

来张表格总结:

父容器测量模式(右侧)———————————-— 子View布局参数(下侧) EXACTLY(精确) AT_MOST(最多) UNSPECIFIED(未指定)
具体dp/px EXACTLY childDimension(子View尺寸) EXACTLY childDimension(子View尺寸) EXACTLY childDimension(子View尺寸)
MATCH_PARENT EXACTLY parentSize(父容器剩余大小) AT_MOST parentSize(不能超过父容器剩余大小) UNSPECIFIED targetSdkVersion<23 ? 0 : size; (大于等于23时会将size作为提示值)
WRAP_CONTENT AT_MOST parentSize(不能超过父容器剩余大小) AT_MOST parentSize(不能超过父容器剩余大小)) UNSPECIFIED targetSdkVersion<23 ? 0 : size; (大于等于23时会将size作为提示值)

对上表作一些规律总结:
•以子View布局参数为标准,横向观察:

1.当子View使用具体的dp/px时,无论父容器的测量模式是什么,子View的测量模式都是EXACTLY且大小等于设置的具体数值;
2.当子View使用match_parent时,子View的测量模式与父容器的测量模式保存一致。

另外,UNSPECIFIED模式一般很少用到,可以不用过多关注。

  1. measure过程分析

measure过程根据View的类型可以分为两种情况:

  1. 测量单一View时,只需测量自身;
  2. 测量ViewGroup时,需要对ViewGroup中包含的所有子View都进行测量。

我们对这两种情况分别进行分析。

4.1 单一View的measure过程

单一View的measure过程由View的measure()来实现:

4.1.1 View的measure

/**
 *
 * @param widthMeasureSpec view的宽测量规格
 * @param heightMeasureSpec view的高测量规格
 */
 public final void measure(int widthMeasureSpec, int   heightMeasureSpec) {
    //...
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    //...  }

这里传入的widthMeasureSpec/heightMeasureSpec是当前View的测量规格,通过getChildMeasureSpec()来获得的。具体细节可以看上面的分析。
measure()方法为final类型,子类不能对其进行重写。measure()方法中主要就是对基本测量逻辑作判断,最终会调用onMeasure()方法。

4.1.2 View的onMeasure

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

上面就一行代码,看起来很简单,但是包含了三个方法:getSuggestedMinimumWidth()/getSuggestedMinimumHeight()、getDefaultSize()、setMeasuredDimension(),我们分开来看:

4.1.3 View的getSuggestedMinimumWidth

getSuggestedMinimumHeight()与getSuggestedMinimumWidth()同理,我们这里只看下getSuggestedMinimumWidth():

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

如果View没有设置背景,那么View的宽度为mMinWidth,而mMinWidth就是android:minWidth属性所指定的值,若android:minWidth没有指定,则默认为0;
如果View设置了背景,那么View的宽度为mMinWidth和mBackground.getMinimumWidth()中的最大值。

那么mBackground.getMinimumWidth()的值是多少呢,我们来看看:

4.1.4 Drawable的getMinimumWidth

public int getMinimumWidth() {
    final int intrinsicWidth = getIntrinsicWidth();//返回背景图Drawable的原始宽度
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

可以看出:mBackground.getMinimumWidth()返回的是背景图Drawable的原始宽度,若无原始宽度则返回0。

那么Drawable什么情况下有原始宽度?如:ShapeDrawable没有,但BitmapDrawable有。

4.1.5 View的getDefaultSize

我们再来看看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) {
    //测量模式为UNSPECIFIED时,View的测量大小为默认大小,即getSuggestedMinimumWidth()/getSuggestedMinimumHeight()返回的结果。
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    //测量模式为AT_MOST、EXACTLY时,View的测量大小为测量规格中的测量大小
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

4.1.6 View的setMeasuredDimension

再来看setMeasuredDimension():

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    //...
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

setMeasuredDimension()里会调用setMeasuredDimensionRaw():

4.1.7 View的setMeasuredDimensionRaw

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

setMeasuredDimensionRaw()中就是对测量出的宽高进行保存。
到这里,单一View的measure过程就完成了。

4.1.8 时序图

最后,来张相关的时序图:


单一View测量过程时序图

4.1.9 流程图

以及来张测量过程细节流程图:


单一View测量过程流程图

4.2 ViewGroup的measure过程

由于ViewGroup包含了子View,因此其measure过程是通过遍历所有的子View并对子View测量,然后合并所有子View的尺寸,最后得到ViewGroup的测量值。

ViewGroup测量顺序图

如上图所示,要执行ViewGroup的measure,首先从顶部的ViewGroup开始遍历,自上而下递归执行子View的measure。

ViewGroup的measure过程是从measureChildren()开始的。

4.2.1 ViewGroup的measureChildren

/**
 *
 * @param widthMeasureSpec ViewGroup的宽测量规格
 * @param heightMeasureSpec ViewGroup的高测量规格
 */
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {//遍历子View
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

再来看measureChild():

4.2.2 ViewGroup的measureChild

protected void measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();//获得子view的布局参数

    //获得子view的宽/高测量规格
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);

    //子view开始测量
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

4.2.3 View的measure

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
//...
onMeasure(widthMeasureSpec, heightMeasureSpec);
//...
}
这里的measure跟上面单一View的measure方法是一样的。

4.2.4 ViewGroup的onMeasure

如果子View是一个单一的View的话,则直接调用View的onMeasure()方法,跟上面单一View的measure过程是一样大的;
如果子View是ViewGroup的话,理论上应该是应该调用ViewGroup的onMeasure()方法的。
但实际上,ViewGroup中是没有onMeasure()这个方法的,ViewGroup作为一个抽象类,需要其子类去实现测量过程的onMeasure()方法,比如LinearLayout、RelativeLayout等。因为LinearLayout、RelativeLayout这些布局具体不同的特性,其测量细节各有不同,无法进行统一的实现,因此需要其子类去进行具体的实现。因此我们自定义ViewGroup时需要重写onMeasure()方法。
我们这里看下LinearLayout的onMeasure()实现:

4.2.5 LinearLayout的onMeasure

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {//方向判断
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}

LinearLayout会区分方向来进行不同的测量方法,我们主要看下竖向的测量measureVertical(),横向的原理差不多这里就不看了。

4.2.6 LinearLayout的measureVertical

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {

    //获取子view数量
    final int count = getVirtualChildCount();

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

    //...

    for (int i = 0; i < count; ++i) {//遍历子View
        final View child = getVirtualChildAt(i);

        //..

        // 最终会调用子View的measure(),计算出子View的大小
        measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                    heightMeasureSpec, usedHeight);

        // 获取子View的测量高度
        final int childHeight = child.getMeasuredHeight();

        //..

        final int totalLength = mTotalLength;

        // 将子View的测量高度以及Margin加到LinearLayout的总高度上
        mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                   lp.bottomMargin + getNextLocationOffset(child));
    }

    //..

    // 加上LinearLayout设置的Padding
    mTotalLength += mPaddingTop + mPaddingBottom;

    int heightSize = mTotalLength;
    heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
    int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);

    //..

    //保存测量值
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),heightSizeAndState);


}

最后会调用setMeasuredDimension()来保存测量好的值,至此ViewGroup的测量过程就完成了。

4.2.7 时序图

最后,来张相关的时序图:

ViewGroup测量过程时序图

4.2.8 流程图

以及来张ViewGroup测量过程细节流程图:

ViewGroup测量过程流程图
  1. 自定义View

5.1 自定义单一View

自定义单一View,可以直接使用View中默认定义的onMeasure()方法。如果有时需要更精准的测量,可以重写onMeasure()。

5.2 自定义ViewGroup

由于ViewGroup没实现onMeasure(),所以自定义ViewGroup需要重写onMeasure()方法。这里给个简单的模板:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    //定义存放测量后的View的宽和高的变量
    int widthMeasure;
    int heightMeasure;


    //实现测量方法以及具体的测量细节
    measureMethod();

    //必不可少  保存测量后View宽和高的值
    setMeasuredDimension(widthMeasure, heightMeasure);
}
  1. 其他问题

6.1 获取View的测量宽高

我们可以使用getMeasuredWidth()和getMeasuredHeight()来获取View测量出的宽高。

但是从上面的代码分析可以看到,在调用setMeasuredDimension()方法之后,我们才能使用getMeasuredWidth()和getMeasuredHeight()来获取View测量出的宽高,在这之前去调用这两个方法得到的值都会是0。

结合Activity的启动过程以及生命周期,在onCreate()和onResume()中,都还未开始进行测量的操作,所以这时候调用getMeasuredWidth()和getMeasuredHeight()的值都会是0。

另外,在某些情况下,需要多次测量才能确定View最终宽高;因此这种情况下获得的值可能是不准确的,建议在layout过程中onLayout()去获取最终宽高。

6.2 自定义View时WRAP_CONTENT不生效的问题

从上面getDefaultSize()方法的代码可以看到,无论是AT_MOST模式还是EXACTLY模式,View的测量大小都为测量规格中的测量大小,所以我们就会看到自定义View使用WRAP_CONTENT会起到跟MATCH_PARENT的效果。

为了解决这个问题需要在LayoutParams属性为WRAP_CONTENT时指定一下默认的宽和高,如:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    int width=100;//设置一个默认宽
    int height=100;//设置一个默认高

    if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT && getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
        setMeasuredDimension(width, height);//宽高都为WRAP_CONTENT都设置为默认宽高
    } else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
        setMeasuredDimension(width, heightSize);//只有宽为WRAP_CONTENT时,只设置默认宽
    } else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
        setMeasuredDimension(widthSize, height);//只有高为WRAP_CONTENT时,只设置默认高
    }
}这里的默认值请根据实际情况去确认。 

像系统的TextView这些都支持WRAP_CONTENT,可以去看下其onMeasure()的源码实现。
如果我们自定义的View只需要设置指定的具体宽高或者MATCH_PARENT,可以不用重写onMeasure()方法。

上一篇下一篇

猜你喜欢

热点阅读