Android 中 View 测量

移动开发 简书

版权声明: 本文来自书生依旧 的简书,转载请注明出处。

原文链接: http://www.jianshu.com/p/1631da166d56

measure 确定 View 的测量宽高, layout 确定 View 四个顶点的位置和最终宽高, draw 将 View 绘制到屏幕上

MeasureSpec

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

int widthMeasureSpec, int heightMeasureSpec这两个参数代表 「View 的 MeasureSpec」
, MeasureSpec 是 View 的一个内部类, 它表示 View 的测量模式和测量尺寸, 把一个 32 位的 int 分成 2 部分, 前 2 位表示测量模式 (mode), 后 30 位表示测量尺寸 (size)

// 去掉了一部分代码
public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;

        /**
         * Extracts the mode from the supplied measure specification.
         *
         * @param measureSpec the measure specification to extract the mode from
         * @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
         *         {@link android.view.View.MeasureSpec#AT_MOST} or
         *         {@link android.view.View.MeasureSpec#EXACTLY}
         */
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

        /**
         * Extracts the size from the supplied measure specification.
         *
         * @param measureSpec the measure specification to extract the size from
         * @return the size in pixels defined in the supplied measure specification
         */
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
}

测量模式有以下三种

  • UNSPECIFIED: 父 View 对子 View 没有要求, 子 View 想多大就多大, 这种状态大都用于系统内部, 表示一种测量状态
  • EXACTLY: 父 View 希望子 View 直接使用传给子 View 的 specSize, 对应 LayoutParams 中具体的数值和 match_parent
  • AT_MOST: 父 View 希望 View 的大小不要超过 specSize, 对应 LayoutParams 中的 wrap_content

onMeasure 的两个参数即 「View 的 MeasureSpec」, 表达的是父 View 期望子 View 的测量大小, View 最终的测量大小还是要 onMeasure 来决定, 一般 View 的 onMeasure 都会符合父 View 期望

getMode 与 getSize

UNSPECIFIED = 00 + 30个0
EXACTLY = 01 + 30个0
AT_MOST = 10 + 30个0

// 在二进制只有 0 1 两个数, 我们用 b 统一表示它们
0 & b = 0
1 & b = b

MODE_MASK = 11 + 30个0 
~MODE_MASK = 00 + 30个1

MODE_MASK & 32个b = bb + 30个0   -> 保留前2位  -> getMode
~MODE_MASK & 32个b = 00 + 30个b  -> 保留后30位 -> getSize

super.onMeasure(widthMeasureSpec, heightMeasureSpec);

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

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 根据 MeasureSpec (还有最小宽高) 计算出 View 最终的测量 size

onMeasure 方法最终是调用 setMeasuredDimension 方法将 View 最终的测量 size 保存

注意 onMeasure 方法的参数是 MeasureSpec 包含了测量 mode 和 测量 size, setMeasuredDimension 的参数仅仅是最终的测量 size

问题 :为什么在 ViewGroup 中 View 写 wrap_content 的效果和 match_parent 一样?

解答 :在 View 调用 setMeasuredDimension 时, getDefaultSize 对 wrap_content 的处理和 match_parent 一样, 最后的测量大小都是父 View 的 SpecSize

重写 onMeasure 的流程

根据不同的测量模式和 View 自己的需要来确定 View 最终的测量 size ( View#getDefaultSize 方法就是做这个事情), 然后调用 setMeasuredDimension 将最终的测量 size 保存

onMeasure 的参数是如何确定的

onMeasure 两个参数是 「View 的 MeasureSpec」
, 在 ViewGroup#getChildMeasureSpec 中由 spec 和 childDimension 来确定

// 确定 View 的 MeasureSpec
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);
}

其中 childDimension 是 LayoutParams 的属性, spec 是父 View 的 MeasureSpec, 在 ViewGroup#measureChild 方法中传递给了 getChildMeasureSpec

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();
        
        // measureChild 调用 getChildMeasureSpec 并将 parentWidthMeasureSpec 传递过去
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

结论 : onMeasure 的两个参数即 「View 的 MeasureSpec」, 在 ViewGroup#getChildMeasureSpec 中, 由父 View 的 MeasureSpec 和 View 自身的 LayoutParams 确定(图片来自 Android 开发艺术探索)

image

对于 一般的 View (不在 onMeasure 乱改 MeasureSpec) 有如下规律 (不包括 UNSPECIFIED) :

  • 采用固定宽高, SpecMode 为 EXACTLY, SpecSize 为 childSize 就是 LayoutParams 中设置的大小
  • 采用 match_parent, SpecMode 和父 View 保持一致, SpecSize 为 parentSize
  • 采用 wrap_content, SpecMode 为 AT_MOST, SpecSize 为 parentSize

问题 :为什么直接继承 View 的控件, 如果不重写 onMeasure 方法, 在布局猴子那个使用 wrap_content , 相当于 match_parent ?

原因 :View 使用 wrap_content 时, 它的的 SpecMode 是 AT_MOST, 大小为 parentSize 且不能超过父 View 的剩余空间, 显然此时 View 的大小就是父 View 剩余空间的大小, 这个效果和 match_parent 一样

解决 :重写 onMeasure 方法, 针对 AT_MOST 进行单独处理, 像 TextView ImageView 这些控件都是这么做的

最顶层的 View 的 MeasureSpec 是如何确定的

View 的 MeasureSpec 是父 View 的 MeasureSpec 和自身的 LayourParams 确定的, 那么 DecorView 的 MeasureSpec 是在 ViewRootImpl#getRootMeasureSpec 确定的

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;
}
  • LayoutParams.MATCH_PARENT : SpecMode 为 EXACTLY, 大小为窗口大小
  • LayoutParams.WRAP_CONTENT : SpecMode 为 AT_MOST, 大小不能超过窗口大小
  • 固定数值, 如 100dp : SpecMode 为 AT_MOST, 大小为该数值

继续找 getRootMeasureSpec 调用的位置发现 int rootDimension 这个参数来自下面这个 WindowManager.LayoutParams

final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();

// WindowManager.LayoutParams 的构造方法
public LayoutParams() {
    super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    type = TYPE_APPLICATION;
    format = PixelFormat.OPAQUE;
}

public LayoutParams(int _type) {
    super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    type = _type;
    format = PixelFormat.OPAQUE;
}

public LayoutParams(int _type, int _flags) {
    super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    type = _type;
    flags = _flags;
    format = PixelFormat.OPAQUE;
}

public LayoutParams(int _type, int _flags, int _format) {
    super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    type = _type;
    flags = _flags;
    format = _format;
}

可以看到 width 和 height 都是 LayoutParams.MATCH_PARENT, 因此 大部分情况下, DecorView SpecMode 为 EXACTLY, 大小为窗口大小

UNSPECIFIED 模式什么时候用到

大部分情况下, DecorView SpecMode 为 EXACTLY, 大小为窗口大小, 按照上面的表格, View 的 MeasureSpec 根本走不到 UNSPECIFIED 这个模式, 但是确确实实存在某个 View 的 SpecMode 是 UNSPECIFIED

比如在 ScrollView 中,ScrollView 包含一个 LinearLayout 的子 View,这个时候其实 LinearLayout 在 measure自己的时候,其实就不需要参考父 View 的大小,所以 ScrollView 会给它的子 View 的 SpecMode 设置成UNSPECIFIED

View 的 measure 流程

View 的绘制流程从 ViewRootImpl 的 performMeasure 开始

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            // 开始整个视图树的测量, mView 是 DecorView 
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
}

View 的测量是由 measure 方法完成的, 这是一个 final 方法

image

ViewGroup 并没有定义具体的测量过程, 也没有重写 onMeasure 方法, 因为 ViewGroup 是一个抽象类, 不同的 ViewGroup 子类有不同的布局特性, 其测量方法 onMeasure 也各不相同, 需要各个子类去具体实现

ViewGroup 提供了一个工具方法 View#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);
            }
        }
}

measureChildren 遍历所有的子 View 然后调用 measureChild 对子 View 进行测量

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 先计算出 View 的 MeasureSpec, 然后调用 View#measure 进行测量, measure 会调用 onMeasure 方法完成 View 的测量

问题 :自定义 ViewGroup 需不需要重写 onMeasure 方法

解答 :需要, ViewGroup 没有重写 onMeasure 方法, 仅仅是继承 View 的 onMeasure 方法, 不重写这个方法 ViewGroup 所有的子 View 都不会被 measure

measure 完成后可以通过 getMeasuredWidth/getMeasuredHeight 拿到 View 的测量宽高, 但是在某些极端情况下, 系统可能需要多次 measure 才能确定最终的测量宽高, 一个比较好的习惯是在 onLayout 方法中去获取 View 的测量宽高或者最终宽高

在 Activity 的 onCreate 方法中获取 View 的宽高

View 的 measure 过程和 Activity 的生命周期不是同步的, 无法保证在 Activity 的 onCreate, onStart, onResume 方法执行完后 View 已经完成测量, 如果 View 没有完成测量宽高就是 0

  • ​ 在 Activity#onWindowFocusChanged 中获取

    override fun onWindowFocusChanged(hasFocus: Boolean) {
            super.onWindowFocusChanged(hasFocus)
            if (hasFocus) {
                MLog.i("onWindowFocusChanged width:" + myView.width)
                MLog.i("onWindowFocusChanged height:" + myView.height)
            }
    }

    onWindowFocusChanged 在 Activity 获取或者失去焦点的时候调用, This is the best indicator of whether this activity is visible to the user, 这个方法和 Activity 的生命周期独立

  • View.post

    myView.post({
      MLog.i("view post width:" + myView.width)
      MLog.i("view post height:" + myView.height)
    })

    通过 post 将一个 Runnable 投递到消息队列的队尾, 等到 Looper 调用这个消息的时候, View 已经被初始化完成

  • 使用 ViewTreeObserver

    final ViewTreeObserver observer = view.getViewTreeObserver();
    observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override public void onGlobalLayout() {
            view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            int width = view.getWidth();
            int height = view.getHeight();
        }
    });

参考

Android 源码 API 25

Android 开发艺术探索

一篇文章理解Android 视图树的测量过程

简书稿源:简书 (源链) | 关于 | 阅读提示

本站遵循[CC BY-NC-SA 4.0]。如您有版权、意见投诉等问题,请通过eMail联系我们处理。
酷辣虫 » 移动开发 » Android 中 View 测量

喜欢 (0)or分享给?

专业 x 专注 x 聚合 x 分享 CC BY-NC-SA 4.0

使用声明 | 英豪名录