Android自定义ViewGroup:如何理解和利用onMeasure

2018-08-06
在Android自定义开发ViewGroup时 总是避免不了对onMeasure方法的重写
那对这个方法应该如何理解?如何重写?有什么作用?等疑问接踵而来 这篇文章就来简洁地说明下这两个方法的使用

onMeasure此方法主要有两个使用目的
1.为本ViewGroup中所有的子view调用”它们自己的测量方法”:View.measure(int,int)
2.在所有的子view的测量方法调用完成后 理论上所有子view都可以使用getMeasureWidth或getMeasureHeight获取测量宽度和高度 这时由此来确定ViewGroup自己的宽度和高度

现在我们通过阐述这两个目的来间接学习onMeasure的使用

1.调用”它们自己的测量方法”:View.measure(int,int)
我们要先从子view测量方法开始说起
这个测量方式要情况而定 一般最基本也最通用的做法是使用Android已经封装好了的测量方法:

measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec)
measureChildWithMargins(View child,int parentWidthMeasureSpec, int widthUsed,int parentHeightMeasureSpec, int heightUsed)

这两个方法都可以用于单个view的测量:

    for (int i = 0; i < getChildCount(); i++) {
        View view = getChildAt(i);
        if (view.getVisibility() != View.GONE) {
            measureChildWithMargins(view, widthMeasureSpec, 0, heightMeasureSpec, 0);
        }
    }

在这段for循环完成后view便有了自己的宽高 可以使用getMeasureWidth或getMeasureHeight获取测量宽度和高度了

注1:
这两个方法有一定的区别 只有measureChildWithMargins()在测量时会将子view的margin属性考虑进去 在margin会影响子view宽高时(如 子view设置宽度match_parent 并设置了marginLeft为10dp 那么最后获取到的view的测量宽度应该比父控件宽度小10dp)会影响其测量宽高 而measureChild不会 所以有时会造成测量不准的问题

注2:measureChildWithMargins()中那两个被赋值为0的两个参数 在android说明中说是为了在平行 or 垂直布局中设置固定间隔而用的 在代码中其实是直接加在了margin和padding值里 其实算是当作另一个margin来用 详细请查看源码

measureChildren(int widthMeasureSpec, int heightMeasureSpec)

方法源码:

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

没错 这个方法其实就是我们之前写的for循环子view进行测量的那部分代码 android早就给了封装方法 在实际开发中 直接使用这个方法基本就可以解决所有的测量问题
只是它用的是measureChild进行的测量 所以在子view使用了margin的情况下 有可能会影响实际测量值(一般只在子view设置为match_parent时有这种情况 所以也不是很大的问题) 必须在onLayout中考虑到这点

在调用了以上方法后 经过一系列计算 最终view.measure(int width,int height)会被调用 传入的就是view的测量高度和宽度 具体内容可看源码(实际和ViewGroup的onMeasure类似甚至更简单些 明白了onMeasure的用法 就很容易理解了)

2.确定ViewGroup自己的宽度和高度
这个问题要从ViewGroup的测量方法onMeasure(int widthMeasureSpec, int heightMeasureSpec)的参数意义开始说起
这个方法中的两个形参是两个int类型 但是它们不是单纯的数字 没有单纯的算术意义 而是记录了”测量模式”和测量高/宽度的数字(因为int类型共32位 前2位用来表示模式 后30位用来表示宽度或高度 具体可百度了解)
android提供了MeasureSpec工具类 方便我们提取测量模式和宽高:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = MeasureSpec.getSize(widthMeasureSpec);//获取宽度
    int height = MeasureSpec.getSize(heightMeasureSpec);//获取高度

    int modeW = MeasureSpec.getMode(widthMeasureSpec);//获取宽度测量模式
    int modeH = MeasureSpec.getMode(heightMeasureSpec);//获取高度测量模式

    for (int i = 0; i < getChildCount(); i++) {
        View view = getChildAt(i);
        if (view.getVisibility() != View.GONE) {
            measureChildWithMargins(view, widthMeasureSpec, 0, heightMeasureSpec, 0);
        }
    }

    switch (modeW) {//判断宽度模式以演示
        case MeasureSpec.AT_MOST://当本控件在xml中宽度上写入 wrap_content时为此模式 这时返回的width一般为0
        case MeasureSpec.EXACTLY://当本控件在xml中宽度上写入实际值时为此模式 如20dp match_parent(与父布局一样的数值) 这里返回的width也就是xml中写明的数值
        case MeasureSpec.UNSPECIFIED://只有在ListView或类似的控件中会出现 表示不关心大小 这时返回的width一般为0
            break;
    }}

关键点在于如何去理解不同的测量模式给我们对确定ViewGroup宽高的影响
首先说明 确定了ViewGroup的宽高后 应该使用
setMeasuredDimension(int width, int height);
进行赋值 传入的两个Int是确定的数值

如何使用测量模式:
如 模式为 MeasureSpec.EXACTLY:
此时情况最简单 因为此模式下表示开发者使用XML传入了一个固定的值 我们直接设置为ViewGroup的宽高就可以了
setMeasuredDimension(width, height);

如模式为 MeasureSpec.AT_MOST:
此时情况为 XML要求宽高为wrap_content 也就是包裹内容 因为我们已经通过for测量了所有子view的宽高
所以我们可以这样:

    int maxHeight = 0;
    int maxWidth = 0;
    for (int i = 0; i < getChildCount(); i++) {
        View view = getChildAt(i);
        maxHeight = view.getMeasuredHeight() > maxHeight ? view.getMeasuredHeight() : maxHeight;
        maxWidth = view.getMeasuredWidth() > maxWidth ? view.getMeasuredWidth() : maxWidth;
    }
    setMeasuredDimension(maxWidth,maxHeight);

也可以这样:

    int maxHeight = 0;
    int maxWdith = 500;
    for (int i = 0; i < getChildCount(); i++) {
        View view = getChildAt(i);
        maxHeight += view.getMeasuredHeight();
    }
    setMeasuredDimension(maxWidth,maxHeight);

上述前者部分是view没有排布规则(如没有任何其它设置的FrameLayout) 我们取所以子view里最宽和最高的值作为我们布局的宽高 很符合”包裹内容”的定义

后者部分是view有垂直排布规则(如LinearLayout) 先假设宽度一定 因为子view垂直排布 所以 我们viewGroup的高度应该是所以子view的高度的和

至于MeasureSpec.UNSPECIFIED模式
一般会出现这个模式的情况是作为listView的item 这时应该开发者根据实际情况处理(一般直接按包裹内容方式处理)

到这里应该就可以看出 测量方法的重要性 不但是设置子view宽高的方法(子view在调用了测量方法后才会有测量宽高 之前会一直为0 没有经过这个方法也就无法完成view的绘制和排布) 同时也是为了确保控件在”自定义排布功能”的情况下确定自身宽高的方法

    原文作者:瑞神Collection
    原文地址: https://www.jianshu.com/p/ee18a43f8c52
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞