自定义控件

高级UI<第十篇>:视图的测量(onMeasure)

2019-11-24  本文已影响0人  NoBugException

当自定义一个视图时,基本都会重写onMeasureonLayout以及onDraw这三个方法,本文的重点是onMeasure

(1)onMeasure的作用

简单的说,onMeasure的主要负责视图绘制之前的测量工作。

(2)自定义视图时,必须重写onMeasure吗?

不是必须重写onMeasure
如果没有重写onMeasure,则默认测量当前视图;
如果重写了onMeasure方法,也必须测量当前视图,否则会抛出异常,如下:

java.lang.IllegalStateException: View with id -1: com.zyc.hezuo.myapplication.ViewGroupDemo#onMeasure() did not set the measured dimension by calling setMeasuredDimension()

为了解决这个异常,我们必须测量当前视图,那么怎么去测量当前视图呢?

(3)怎么去测量当前视图?
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);//测量当前视图
}

那么,widthMeasureSpecheightMeasureSpec又是什么呢?为什么测量当前视图时必须传递这两个参数?

(4)测量规格

widthMeasureSpecheightMeasureSpec作为onMeasure方法的形参,为视图的测量提供了重要的帮助,我们必须了解这两个参数的意义和作用,这两个参数是由父视图通过一定的计算方式计算出来的,至于父视图是怎么计算出这两个参数的值,我们不需要去关注。(这里需要再次提醒,这里的两个参数是由父视图通过某个计算方式计算出来的,不排除嵌套自定义view的情况,也就是说,某自定义view的父视图是自定义viewGroup,它的这两个参数就是父视图计算出来的)

那么,widthMeasureSpecheightMeasureSpec到底是什么呢?

widthMeasureSpec可以分成三个英文单词,分别是:widthMeasureSpec,根据英文翻译组合起来的意思是:宽度测量规格,对应着heightMeasureSpec的意思就是:高度测量规格

我翻阅了源码,发现这两个参数是这个方法计算的来的,如下:

/**
 * Figures out the measure spec for the root view in a window based on it's
 * layout params.
 *
 * @param windowSize
 *            The available width or height of the window
 *
 * @param rootDimension
 *            The layout params for one dimension (width or height) of the
 *            window.
 *
 * @return The measure spec to use to measure the root view.
 */
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;
}

方法MeasureSpec.makeMeasureSpec有两个参数,第一个参数windowSize是父视图的实际大小,第二个参数是测量模式

为了了解widthMeasureSpecheightMeasureSpec的计算方式,查看makeMeasureSpec的源码,源码如下:

    public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                      @MeasureSpecMode int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }

大致的意思如下,如果targetSdkVersion小于等于17,那么

measureSpec = size + mode;

否则

measureSpec  = (size & ~MODE_MASK) | (mode & MODE_MASK);

这里我们假设targetSdkVersion > 17(就目前而言,为了解决有些兼容问题,targetSdkVersion肯定>17了),所以最终的算法如下:

measureSpec  = (size & ~MODE_MASK) | (mode & MODE_MASK);

size是某视图的实际大小,暂定为100,mode是某视图的测量模式测量模式有三种,分别是:

MeasureSpec.UNSPECIFIED:不准确的大小,不对View大小做限制,如:ListView,ScrollView
MeasureSpec.EXACTLY:确切的大小,如:100dp或者march_parent
MeasureSpec.AT_MOST:大小不可超过某数值,如:wrap_content

这三种模式的取值可从源码获取,如下:

    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    public static final int AT_MOST     = 2 << MODE_SHIFT;

MODE_SHIFT的取值为30,所以UNSPECIFIED值为0向左移动30位,即0 * 230
EXACTLY的取值为1向左移动30位,,即1 * 230
AT_MOST的取值为2向左移动30位,,即2 * 230

从源码中找到这样一句代码:

    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

所以MODE_MASK取值为 3 * 230

万事俱备,只欠东风,现在开始计算measureSpec的取值,按照如下步骤计算:

【size的二进制表示】

size的值为100,二进制表示为

00000000000000000000000001100100

【MODE_MASK的二进制表示】

11000000000000000000000000000000(30个0,加上前面两位共32位)

MeasureSpec由两部分组成,测量模式大小,

【~MODE_MASK的二进制表示】

00111111111111111111111111111111(30个1,加上前面两位共32位)

【size & ~MODE_MASK的二进制表示】

   00000000000000000000000001100100
&  00111111111111111111111111111111
--------------------------------------------------
   00000000000000000000000001100100

【mode的二进制表示】

UNSPECIFIED:

00000000000000000000000000000000(32个0)

EXACTLY:

01000000000000000000000000000000(31个0)

AT_MOST:

11000000000000000000000000000000(30个0)

【mode & MODE_MASK的二进制表示】

UNSPECIFIED:

     00000000000000000000000000000000
&    11000000000000000000000000000000
--------------------------------------------------
     00000000000000000000000000000000

EXACTLY:

     01000000000000000000000000000000
&    11000000000000000000000000000000
--------------------------------------------------
     01000000000000000000000000000000

AT_MOST:

     10000000000000000000000000000000
&    11000000000000000000000000000000
--------------------------------------------------
     10000000000000000000000000000000

【(size & ~MODE_MASK) | (mode & MODE_MASK)的二进制表示】

UNSPECIFIED:

     00000000000000000000000001100100
|    00000000000000000000000000000000
--------------------------------------------------
     00000000000000000000000001100100

计算结果为100。

EXACTLY:

      00000000000000000000000001100100
|     01000000000000000000000000000000
--------------------------------------------------
      01000000000000000000000001100100

计算结果为: 2 * 30 + 100 = 1073741924

AT_MOST:

      00000000000000000000000001100100
|     10000000000000000000000000000000
--------------------------------------------------
      10000000000000000000000001100100

计算结果为: -2 * 2 * 30 + 100 = -2147483548

所以measureSpec的取值受到size和mode的影响。

(5)怎么去获取测量规格?
int measureSpec = MeasureSpec.makeMeasureSpec(当前视图的大小, 测量模式);

measureSpec主要用于子视图的测量,因为onMeasure(int widthMeasureSpec, int heightMeasureSpec)中的两个形参是由父视图中计算的,所以这两个参数应当作为当前视图的测量参数。

(6)根据测量规格获取测量模式和大小?
    //获取测量模式
    int mode = MeasureSpec.getMode(measureSpec);
    //获取测量大小
    int size = MeasureSpec.getSize(measureSpec);
(7)怎么去测量子视图?

如果您想要自定义一个类似于线性布局相对布局的ViewGroup,就必须测量子视图(如果是自定义View,非ViewGroup,不需要重写onMeasure方法,因为自定义View不可能存在子视图)。

测量子视图有以下几种方法:

需要特别注意的是:获取subview的宽和高不能直接使用subview.getWidth()subview.getMeasuredWidth()subview.getHeight()subview.getMeasuredHeight(),因为获取到的数据有时候为0导致测量不准确,这里使用getLayoutParams来获取宽和高,当getLayoutParams获取到数字是-1时,代表MATCH_PARENT,当getLayoutParams获取到的数字是-2时,代表WRAP_CONTENT,当getLayoutParams获取到的数据是固定值时,说明subview的宽或高也是固定值。

需要注意的是:如果用到measureChildWithMargins这个方法,就必须重写generateLayoutParams进行类型转换,否则会报错:

java.lang.ClassCastException: android.view.ViewGroup$LayoutParams cannot be cast to android.view.ViewGroup$MarginLayoutParams

以下是需要重写方法的代码:

@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet params) {
    return new MarginLayoutParams(getContext(), params);
}
(8)onMeasure为什么会被执行两次?

在源码中有这样的方法,源码如下:

private void performTraversals() {

                //...(省略)

                 // Ask host how big it wants to be
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

                // Implementation of weights from WindowManager.LayoutParams
                // We just grow the dimensions as needed and re-measure if
                // needs be
                int width = host.getMeasuredWidth();
                int height = host.getMeasuredHeight();
                boolean measureAgain = false;

                if (lp.horizontalWeight > 0.0f) {
                    width += (int) ((mWidth - width) * lp.horizontalWeight);
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
                            MeasureSpec.EXACTLY);
                    measureAgain = true;
                }
                if (lp.verticalWeight > 0.0f) {
                    height += (int) ((mHeight - height) * lp.verticalWeight);
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
                            MeasureSpec.EXACTLY);
                    measureAgain = true;
                }

                if (measureAgain) {
                    if (DEBUG_LAYOUT) Log.v(mTag,
                            "And hey let's measure once more: width=" + width
                            + " height=" + height);
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                }

                //...(省略)
}

其中performMeasure就是执行onMeasure方法,这里performMeasure被执行了两次,原因是lp.horizontalWeight或者lp.verticalWeight的取值大于0;

那么为什么onMeasure要执行两次呢?

这里我给出的答案是:系统为了测量的准确性,在某种条件下进行多次测量工作。

最后,有关onMeasure被执行多次的补充:

本文的重点是onMeasure方法,从performMeasure源码角度分析,onMeasure可能只被执行一次,也有可能被执行两次,也就是说,最多被执行两次;

也许你们会说,我的自定义ViewGroup的onMeasure方法怎么被执行了3次?4次?

我只想确切的说,在执行onLayout方法之前,最多执行2次,如果一旦执行了onLayout方法方法,onMeasure可能会再次执行,因为从源码中可以得到答案:

ViewRootImpl.java

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {

        //...(省略)

        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

        //...(省略)

}
public void layout(int l, int t, int r, int b) {
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }

        //...(省略)

        onLayout(changed, l, t, r, b);

        //...(省略)
}

layout方法中,可以找到onMeasure方法,当然,只有当(mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0满足条件的时候onMeasure才能被执行。

除此之外,以下三个方法也会执行onMeasure方法,在代码中执行以下方法:

linearLayout.addView(imageView);
linearLayout.setVisibility(View.VISIBLE);

注意:setVisibility(View.GONE)不会触发重新测量。

((TextView)findViewById(R.id.text)).setText("11111");

最后,兵来将挡,水来土掩,不管onMeasure执行多少次,都不会影响onMeasure下的代码逻辑,在后面篇章中,会详细讲解瀑布流布局,到时候一起来见识一下onMeasure的艺术吧。

[本章完...]

上一篇下一篇

猜你喜欢

热点阅读