Android 从0开始自定义控件之 View 的 measure 过程(七)

转载请标明出处: http://blog.csdn.net/airsaid/article/details/53678640
本文出自:周游的博客

前言

经过前面2篇的铺垫,终于到正式学习 View 的三大流程:测量、布局、绘制流程了,这一篇就先从学习 measure 过程开始吧。

measure 过程要分两种情况,第一种是 View,第二种是 ViewGroup。如果是 View 的话,那么只通过 measure 方法就完成其测量过程,但是如果是 ViewGroup 的话,不仅需要完成自己的测量过程,还需要完成它所有子 View 的测量过程。如果子 View 又是一个 ViewGroup,那么继续递归这个流程。下面先从 View 开始,详细了解下 View 的 measure 过程。

View 的 measure 过程

View 的测量过程是由 View 的 measure 方法来完成的,但是该方法是一个 finall 方法,所以不能被重写。在 measure 方法中会去调用 onMeasure() 方法,因此我们只需在 View 中重写 onMeasure() 方法来完成 View 的测量即可。那么 View 默认的 measure 实现是怎样的呢? 来看下 View 的 onMeasure() 方法:

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

可以看到,该方法的实现很简单,直接调用了 setMeasuredDimension() 方法来设置测量的尺寸。关键就在于 getDefaultSize() 方法上, 继续跟进,看看 getDefaultSize() 方法的实现::

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

从上述代码上可以看到,关于我们关心的 AT_MOST 和 EXACTLY 测量模式,其实 getDefaultSize() 方法返回的就是 MeasureSpec 的 specSize。
而这个 MeasureSpec 如果阅读过上篇文章后,就应该知道是 ViewGroup 传递而来的。如果不太了解,建议返回去看下上篇文章,这里就不重复介绍了。

到这里也就理解了,为什么当我们在布局中写 wrap_content,如果不重写 onMeasure() 方法,则默认大小是父控件的可用大小了。
当我们在布局中写 wrap_content 时,那么测量模式就是: AT_MOST,在该模式下,它的宽高等于 specSize。而 specSize 由 ViewGroup 传递过来时就是 parentSize,也就是父控件的可用大小。
当我们在布局中写 match_parent 时,那么不用多说,宽高当然也是 parentSize。这时候,我们只需对 AT_MOST 测量模式进行处理:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    int width = 0;
    int height = 0;

    if(widthMode == MeasureSpec.AT_MOST){
        width = ...
    }

    if(heightMode == MeasureSpec.AT_MOST){
        height = ...
    }

    setMeasuredDimension(widthMode != MeasureSpec.AT_MOST ? widthSize : width,
            heightMode != MeasureSpec.AT_MOST? heightSize : height);
}

上述代码,判断当测量模式是最大模式时,自己计算 View 的宽高。其他情况,直接使用 specSize。

至于 UNSPECIFIED 这种情况,则是使用的第一个参数的值,也就是:getSuggestedMinimumWidth()和getSuggestedMinimumHeight()方法,一般用于系统内部的测量过程。
这两个方法的源码如下:

protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

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

大概意思就是,判断 View 有没有背景,没有背景的话,那么值就是 View 最小的宽度或高度,也就是对应 xml 中的:android:minWidth、android:minHeight 属性,如果属性没有指定的话,默认为0。
有背景的话,那么值就是 View 最小的宽度或高度 和 背景的最小宽度或高度,取两者中最大的一个值。这个值就是当测量模式是 UNSPECIFIED 时 View 的测量宽/高。

到这里就完成了整个 View 的 measure 过程,完成之后我们就可以通过 getMeasuredWidth() 和 getMeasuredHeight() 方法获取 View 正确的测量宽/高了。但是需要注意的时,在某些极端情况下,系统可能需要再多次 measure 过程后才能确定最终的测量宽/高,在这种情况下,直接在 onMeasure() 方法中获取的测量宽/高可能是不准确的,保险的做法是在 onLayout() 方法中去获取。

ViewGroup 的 measure 过程

ViewGroup 的 measure 过程 和 View 不同,不仅需要完成自身的 measure 过程,还需要去遍历所有子 View 的 measure 方法,各个子元素之间再递归这个流程。
ViewGroup 提供了一个叫 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);
        }
    }
}

该方法遍历了所有的子 View,判断如果子 View 没有 GONE 掉的时候,就继续执行 measureChild() 方法:

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

该方法获取了子 View 的 LayoutParams,然后通过 getChildMeasureSpec() 方法创建了子 View 的 MeasureSpec,至于是怎么生成的,上一篇关于 MeasureSpec 的文章有写。
创建好子 View 的 MeasureSpec 后,然后将 MeasureSpec 传给了子 VIew 进行 View 的 measure 过程。

通过上面的代码我们可以发现,ViewGroup 并没有定义其具体的测量过程,这是因为 ViewGroup 是一个抽象类,它测量过程的 onMeasure 方法需要它的子类去实现,比如说像 LinearLayout、RelativeLayout等。
它并不像 View 一样,对 onMeasure 方法做了统一实现,这是因为它的子类都有不同的布局特性,就像 LinearLayout 和 RelativeLayout 一样,两者的布局特性截然不同,没有办法做统一实现。

注意事项

由于 View 的 measure 过程和 Activity 的生命周期不是同步的,那么如果直接在 Activity 的生命周期方法,如:onCreate() 、onStart()、onResumt() 中直接获取 View 的宽/高是无法正确获取到的。
因为没办法保证当走这些生命周期回调方法前,View 的 measure 过程已经走完。如果没有走完就直接获取的话,那么得到的只会是 0。下面给出几种解决方法:

  • 方案1:

重写 onWindowFocusChanged() 方法,在该方法中获取宽/高:

@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    if(hasFocus){
        int measuredWidth = view.getMeasuredWidth();
        int measuredHeight = view.getMeasuredHeight();
    }
}

该方法会在当前 Activity 的 Window 获得或失去焦点的时候回调,当回调该方法时,表示 Activtiy 是完全对用户可见的,这时候 View 已经初始化完毕、宽/高都已经测量好了,这时就能获取到宽/高了。

  • 方案2:
view.post(new Runnable() {
    @Override
    public void run() {
        int measuredWidth = view.getMeasuredWidth();
        int measuredHeight = view.getMeasuredHeight();
    }
});

该方案,通过 post 方法将一个 runnable 投递到消息队列的底部,然后等待 Looper 调用该 runnable 时,View 也已经初始化好了,这时就能获取到宽/高了。

  • 方案3:
ViewTreeObserver treeObserver = view.getViewTreeObserver();
treeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
        int measuredWidth = view.getMeasuredWidth();
        int measuredHeight = view.getMeasuredHeight();
    }
});

该方案,通过监听 View 树的状态发生改变或者 View 树内部的 View 可见性发生改变时,在 onGlobalLayout 回调中获取 View 的宽/高。需要注意的时,该回调会被调用多次,所以这里在第一次回调中,就移除了监听,避免多次获取。

  • 方案4:

该方案是通过手动对 View 进行 measure 来得到 VIew 的宽/高。这种方案对比前三种方案要复杂一下,因为要根据 View 的 LayoutParams 分情况来处理,所以不推荐使用。

如果 View 的宽高是写死的,比如都是 100px,那么可以通过如下方式获取:

int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);

int measuredWidth = view.getMeasuredWidth();
int measuredHeight = view.getMeasuredHeight();

如果 View 的 LayoutParent 是 wrap_content,那么可以通过如下方式获取:

int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec( (1 << 30) - 1, View.MeasureSpec.AT_MOST);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec( ( 1 << 30) - 1, View.MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec);

int measuredWidth = view.getMeasuredWidth();
int measuredHeight = view.getMeasuredHeight();

需要注意的是,这里的 (1 << 30) -1,是 View 尺寸所能支持的最大的值。通过上篇分析 MeasureSpec 我们知道,View 的尺寸是用30位2进制来表示的,也就是说是 30 个 1,也就是 (1 << 30) -1,所以在最大测量模式下,我们用 View 理论上能支持的最大值去构建 MeasureSpec 是合理的。

如果 View 的 LayoutParent 是 match_parent,那么是无法获取具体的宽/高的,因为通过前面的了解,我们已经知道,构建 MeasureSpec 时需要 parentSize,也就是父控件的大小,但是我们是不知道的,所以遇到这种情况可以直接放弃使用该方案来获取 View 的宽/高。

参考

  • 《Android开发艺术探索》
发布了55 篇原创文章 · 获赞 117 · 访问量 30万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 精致技术 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览