Android 从0开始自定义控件之 View 的 measure 过程(七)
2016-12-15 23:10
501 查看
转载请标明出处: http://blog.csdn.net/airsaid/article/details/53678640
本文出自:周游的博客
前言
View 的 measure 过程
ViewGroup 的 measure 过程
注意事项
measure 过程要分两种情况,第一种是 View,第二种是 ViewGroup。如果是 View 的话,那么只通过 measure 方法就完成其测量过程,但是如果是 ViewGroup 的话,不仅需要完成自己的测量过程,还需要完成它所有子 View 的测量过程。如果子 View 又是一个 ViewGroup,那么继续递归这个流程。下面先从 View 开始,详细了解下 View 的 measure 过程。
可以看到,该方法的实现很简单,直接调用了 setMeasuredDimension() 方法来设置测量的尺寸。关键就在于 getDefaultSize() 方法上, 继续跟进,看看 getDefaultSize() 方法的实现::
从上述代码上可以看到,关于我们关心的 AT_MOST 和 EXACTLY 测量模式,其实 getDefaultSize() 方法返回的就是 MeasureSpec 的 specSize。
而这个 MeasureSpec 如果阅读过上篇文章后,就应该知道是 ViewGroup 传递而来的。如果不太了解,建议返回去看下上篇文章,这里就不重复介绍了。
到这里也就理解了,为什么当我们在布局中写 wrap_content,如果不重写 onMeasure() 方法,则默认大小是父控件的可用大小了。
当我们在布局中写 wrap_content 时,那么测量模式就是: AT_MOST,在该模式下,它的宽高等于 specSize。而 specSize 由 ViewGroup 传递过来时就是 parentSize,也就是父控件的可用大小。
当我们在布局中写 match_parent 时,那么不用多说,宽高当然也是 parentSize。这时候,我们只需对 AT_MOST 测量模式进行处理:
上述代码,判断当测量模式是最大模式时,自己计算 View 的宽高。其他情况,直接使用 specSize。
至于 UNSPECIFIED 这种情况,则是使用的第一个参数的值,也就是:getSuggestedMinimumWidth()和getSuggestedMinimumHeight()方法,一般用于系统内部的测量过程。
这两个方法的源码如下:
大概意思就是,判断 View 有没有背景,没有背景的话,那么值就是 View 最小的宽度或高度,也就是对应 xml 中的:android:minWidth、android:minHeight 属性,如果属性没有指定的话,默认为0。
有背景的话,那么值就是 View 最小的宽度或高度 和 背景的最小宽度或高度,取两者中最大的一个值。这个值就是当测量模式是 UNSPECIFIED 时 View 的测量宽/高。
到这里就完成了整个 View 的 measure 过程,完成之后我们就可以通过 getMeasuredWidth() 和 getMeasuredHeight() 方法获取 View 正确的测量宽/高了。但是需要注意的时,在某些极端情况下,系统可能需要再多次 measure 过程后才能确定最终的测量宽/高,在这种情况下,直接在 onMeasure() 方法中获取的测量宽/高可能是不准确的,保险的做法是在 onLayout() 方法中去获取。
ViewGroup 提供了一个叫 measureChildren() 的方法:
该方法遍历了所有的子 View,判断如果子 View 没有 GONE 掉的时候,就继续执行 measureChild() 方法:
该方法获取了子 View 的 LayoutParams,然后通过 getChildMeasureSpec() 方法创建了子 View 的 MeasureSpec,至于是怎么生成的,上一篇关于 MeasureSpec 的文章有写。
创建好子 View 的 MeasureSpec 后,然后将 MeasureSpec 传给了子 VIew 进行 View 的 measure 过程。
通过上面的代码我们可以发现,ViewGroup 并没有定义其具体的测量过程,这是因为 ViewGroup 是一个抽象类,它测量过程的 onMeasure 方法需要它的子类去实现,比如说像 LinearLayout、RelativeLayout等。
它并不像 View 一样,对 onMeasure 方法做了统一实现,这是因为它的子类都有不同的布局特性,就像 LinearLayout 和 RelativeLayout 一样,两者的布局特性截然不同,没有办法做统一实现。
因为没办法保证当走这些生命周期回调方法前,View 的 measure 过程已经走完。如果没有走完就直接获取的话,那么得到的只会是 0。下面给出几种解决方法:
方案1:
重写 onWindowFocusChanged() 方法,在该方法中获取宽/高:
该方法会在当前 Activity 的 Window 获得或失去焦点的时候回调,当回调该方法时,表示 Activtiy 是完全对用户可见的,这时候 View 已经初始化完毕、宽/高都已经测量好了,这时就能获取到宽/高了。
方案2:
该方案,通过 post 方法将一个 runnable 投递到消息队列的底部,然后等待 Looper 调用该 runnable 时,View 也已经初始化好了,这时就能获取到宽/高了。
方案3:
该方案,通过监听 View 树的状态发生改变或者 View 树内部的 View 可见性发生改变时,在 onGlobalLayout 回调中获取 View 的宽/高。需要注意的时,该回调会被调用多次,所以这里在第一次回调中,就移除了监听,避免多次获取。
方案4:
该方案是通过手动对 View 进行 measure 来得到 VIew 的宽/高。这种方案对比前三种方案要复杂一下,因为要根据 View 的 LayoutParams 分情况来处理,所以不推荐使用。
如果 View 的宽高是写死的,比如都是 100px,那么可以通过如下方式获取:
如果 View 的 LayoutParent 是 wrap_content,那么可以通过如下方式获取:
需要注意的是,这里的 (1 << 30) -1,是 View 尺寸所能支持的最大的值。通过上篇分析 MeasureSpec 我们知道,View 的尺寸是用30位2进制来表示的,也就是说是 30 个 1,也就是 (1 << 30) -1,所以在最大测量模式下,我们用 View 理论上能支持的最大值去构建 MeasureSpec 是合理的。
如果 View 的 LayoutParent 是 match_parent,那么是无法获取具体的宽/高的,因为通过前面的了解,我们已经知道,构建 MeasureSpec 时需要 parentSize,也就是父控件的大小,但是我们是不知道的,所以遇到这种情况可以直接放弃使用该方案来获取 View 的宽/高。
参考
《Android开发艺术探索》
本文出自:周游的博客
前言
View 的 measure 过程
ViewGroup 的 measure 过程
注意事项
前言
经过前面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开发艺术探索》
相关文章推荐
- Android 从0开始自定义控件之 View 的 draw 过程 (九)
- Android中View的绘制过程 onMeasure方法简述 附有自定义View例子
- Android自定义View(三、深入解析控件测量onMeasure)
- Android 从0开始自定义控件之 View 的滑动冲突详解(四)
- Android 从0开始自定义控件之 View 的弹性滑动(三)
- Android中View的绘制过程 onMeasure方法简述 附有自定义View例子
- Andriod 从0开始自定义控件之 View 的 layout 过程 (八)
- 【Android - 自定义View】之View的measure过程解析
- Android 从0开始自定义控件之 自定义 View 基础实例(十)
- Android 自定义View 测量控件大小onMeasure中MeasureSpec作用
- 【转】Android中View的绘制过程 onMeasure方法简述 附有自定义View例子
- Android中View的绘制过程 onMeasure方法简述 附有自定义View例子
- Android 从0开始自定义控件之 View 的滑动(二)
- Android自定义View(三、深入解析控件测量onMeasure)
- (总结篇)Android 牛不牛?决定于自定义View控件(一)——view绘制流程(onMeasure,onLayout,onDraw)
- [置顶] Android自定义View(三、深入解析控件测量onMeasure)
- Android 从0开始自定义控件之 View 基础知识与概念(一)
- Android中View的绘制过程 onMeasure方法简述 附有自定义View例子
- Android中View的绘制过程 onMeasure方法简述 附有自定义View例子
- (转)Android自定义View(三、深入解析控件测量onMeasure)