1. View 的工作流程的概念
- View 的工作流程就是 measure、layout、draw 这三大流程,即测量、布局、绘制
- measure 确定 View 的测量宽高,layout 确定 View 的最终宽高和四个顶点的位置,draw 将 View 绘制到屏幕上
2. measure 过程的分类
- 对于原始的 View: 通过 measure 方法就完成了其测量过程(measure 过程是三大流程中最复杂的一)
- 对于 ViewGroup: 除了完成自己的测量过程外,还会遍历去调用所有子元素的 measure 方法,各个子元素再递归去执行这个流程
3. View 的 measure 过程的大致流程
View 的 measure 过程由其
measure()
方法完成,measure()
方法是一个 final 方法,即子类不能重写该方法。在 View 的measure()
方法中会去调用 View 的onMeasure()
方法:1
2
3protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}setMeasuredDimension()
方法会设置 View 宽高的测量值,只需关注getDefaultSize()
方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public 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 MeasureSpce.EXACTLY:
result = specSize;
break;
}
return result;
}- 只需要看 AT_MOST 和 EXACTLY 两种情况。简单理解,
getDefaultSize()
方法返回的大小就是 measureSpec 中的 specSize,而这个 specSize 就是 View 测量后的大小(这里多次提到测量后的大小,是因为 View 最终的大小是在 layout 阶段确定的,不过几乎所有情况下 View 的测量大小和最终大小是相等的) - UNSPECIFIED 的情况,一般用于系统内部的测量过程,此时 View 的大小为
getDefaultSize()
方法的第一个参数 size,即宽高分别为getSuggestedMinimumWidth()
和getSuggestedMinimumHeight()
这两个方法的返回值
- 只需要看 AT_MOST 和 EXACTLY 两种情况。简单理解,
getSuggestedMinimumWidth()
和getSuggestedMinimumHeight()
这两个方法的源码:1
2
3protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}1
2
3protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}- 只分析
getSuggestedMinimumWidth()
方法,getSuggestedMinimumHeight()
同理 - 如果 View 没有设置背景,那么 View 的宽度即为 mMinWidth,mMinWidth 对应于
android:minWidth
属性所指定的值。这个属性如果不指定,则 mMinWidth 默认为 0 - 如果 View 指定了背景,则 View 的宽度为
max(mMinWidth, mBackground.getMinimumWidth())
,即android:minWidth
和背景的最小宽度这两者中的最大值
- 只分析
Drawable 的
getMinimumWidth()
方法源码:1
2
3
4public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}- 返回值就是 Drawable 的原始宽度,前提是这个 Drawable 有原始宽度,否则返回 0
- 举例,ShapeDrawable 没有原始宽高,BitmapDrawable 有原始宽高(图片的尺寸)
从
getDefaultSize()
方法的实现来看,View 的宽高由 specSize 决定,可以得出结论:直接继承 View 的自定义控件需要重写onMeasure()
方法并设置 wrap_content 时的自身大小,否则在布局中使用 wrap_content 就相当于使用 match_parent(具体解释可查看 P185)
4. ViewGroup 的 measure 过程的大致流程
ViewGroup 除了完成自己的 measure 过程以外,还会遍历去调用所有子元素的 measure 方法,各个子元素再递归去执行这个过程
ViewGroup 是一个抽象类,没有重写 View 的
onMeasure()
方法,但提供了measureChildren()
方法:1
2
3
4
5
6
7
8
9
10
11protected 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);
}
}
}- 从上面代码可看出,ViewGroup 在 measure 时,会对每一个子元素进行 measure
measureChild()
方法实现:1
2
3
4
5
6
7
8protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parenWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}- 从上面代码可看出,
measureChild()
的思想就是取出子元素的 LayoutParams,然后再通过getChildMeasureSpec()
来创建子元素的 MeasureSpec,接着将 MeasureSpec 直接传递给 View 的 measure 方法来进行测量
- 从上面代码可看出,
ViewGroup 并没有定义其测量的具体过程,这是因为 ViewGroup 是一个抽象类,其测量过程的
onMeasure()
方法需要各个子类去具体实现,onMeasure()
方法没有统一实现的原因是因为不同的 ViewGroup 子类有不同的布局特性,导致测量细节各不相同(LinearLayout 的onMeasure()
方法的源码分析请查看 P187)实际使用中,在 Activity 的
onCreate()
、onStart()
和onResume()
方法中均无法正确得到某个 View 的宽高信息- 这是因为 View 的 measure 过程和 Activity 的生命周期方法不是同步执行的
- 因此无法保证 Activity 执行了
onCreate()
、onStart()
和onResume()
方法时某个 View 已经测量完毕,如果 View 还没有测量完毕,那么获得的宽高就是 0
实际开发中,解决问题 5 的四种方法:
Activity/View#onWindowFocusChanged()
:onWindowFocusChanged()
方法的含义:View 已经初始化完毕了,宽高已经准备好了,这个时候去获取宽高是没问题的onWindowFocusChanged()
方法会被调用多次,当 Activity 的窗口得到焦点和失去焦点时均会被调用一次当 Activity 继续执行和暂停执行时,
onWindowFocusChanged()
方法均会被调用,如果频繁得进行onResume()
和onPause()
,那么onWindowFocusChanged()
也会被频繁调用典型代码:
1
2
3
4
5
6
7public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
view.post(runnable)
:通过 post 可以将一个 runnable 投递到消息队列的尾部,然后等待 Looper 调用此 runnable 的时候,View 也已经初始化好了
典型代码:
1
2
3
4
5
6
7
8
9
10protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
ViewTreeObserver
:可以使用 ViewTreeObserver 的众多回调,比如使用
OnGlobalLayoutListener
这个接口,当 View 树的状态发送改变或者 View 树内部的 View 的可见性发生改变时,onGlobalLayout()
方法将被回调,此时是获取 View 宽高的一个很好的时机需要注意的是,伴随着 View 的状态改变等,
onGlobalLayout()
会被调用多次典型代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14protected void onStart() {
super.onStart();
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@SuppressWarnings("deprecation")
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
view.measure(int widthMeasureSpec, int heightMeasureSpec)
: 通过手动对 View 进行 measure 来得到 View 的宽高。这种方法比较复杂,要分情况处理,根据 View 的 LayoutParams 来分:mach_parent: not work,因为构造此种 MeasureSpec 需要知道 parentSize,即父容器的剩余空间,而此时无法知道 parentSize 的大小,所以理论上不可能测量出 View 的大小
wrap_content:
1
2
3int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec);- 注意到 (1 << 30)-1,由 MeasureSpec 的实现可以知道,View 的尺寸使用 30 位二进制表示,也就是最大是 30 个 1(2^30-1),即 (1 << 30)-1
- 在最大化模式下,用 View 理论上能支持的最大值去构造 MeasureSpec 是合理的
具体的数值(dp/px):
1
2
3
4// 举例,比如宽高都是 100px
int widthMesaureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);关于 View 的 measure,有两个错误的用法。首先其违背了系统的内部实现规范(因为无法通过错误的 MeasureSpec 去得出合法的 SpecMode,从而导致 measure 过程出错),其次不能保证一定能 measure 出正确的结果
第一种错误用法:
1
2
3int widthMeasureSpec = MeasureSpec.makeMeasureSpec(-1, MeasureSpec.UNSPECIFIED);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(-1, MeasureSpec.UNSPECIFIED);
view.measure(widthMeasureSpec, heightMeasureSpec);第二种错误用法:
1
view.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
5. View 的 layout 过程的大致流程
layout()
方法确定 View 本身的位置,layout()
方法中的onLayout()
方法确定所有子元素的位置layout()
方法的大致流程:- 首先会通过
setFrame()
方法来设定 View 的四个顶点的位置,即初始化 mLeft、mRight、mTop 和 mBottom 这四个值。View 的四个顶点一旦确定,那么 View 在父容器中的位置也就确定了 - 接着会调用
onLayout()
方法,这个方法的作用是父容器确定子元素的位置 - 和
onMeasure()
方法类似,onLayout()
方法的具体实现和具体的布局有关,所以 View 和 ViewGroup 均没有真正实现onLayout()
方法(LinearLayout 的onLayout()
方法的源码分析请查看 P194)
- 首先会通过
问题:View 的测量宽高和最终宽高的区别?(具体为:View 的
getMeasuredWidth()
和getWidth()
这两个方法的区别?)- 在 View 的默认实现中,View 的测量宽高和最终宽高是相等的,只不过测量宽高形成于 View 的 measure 过程,最终宽高形成于 View 的 layout 过程,即两者的赋值时机不同,测量宽高的赋值时机稍早一些
- 日常开发中,可以认为 View 的测量宽高就等于最终宽高
- 特殊情况下,测量宽高可以不等于最终宽高
- 一种情况是重写 View 的
layout()
方法加入自己的逻辑可能导致不等 - 一种情况是某些场景下,View 需要多次 measure 才能确定自己的测量宽高,在前几次测量过程中可能导致不等,但最终测量宽高和最终宽高相同
- 一种情况是重写 View 的
6. View 的 draw 过程的大致流程
draw 过程的作用是将 View 绘制到屏幕上
分析 draw 方法源码可以看出,View 的绘制过程遵循如下几步:
- 绘制背景(
background.draw(canvas)
) - 绘制自己(
onDraw()
) - 绘制 children(
dispatchDraw()
) - 绘制装饰(
onDrawScrollBars()
)
- 绘制背景(
View 绘制过程的传递是通过
dispatchDraw()
方法来实现的,dispatchDraw()
方法会遍历调用所有子元素的draw()
方法View 有一个特殊的方法
setWillNotDraw()
,源码如下:1
2
3
4
5
6
7
8
9
10
11
12
13/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override (@link #onDraw(android.graphics.Canvas))
* you shoul clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}- 由注释可知,如果一个 View 不需要绘制任何内容,那么设置这个标记位为 true 以后,系统会进行相应的优化
- 默认情况下,View 没有启用这个优化标记位,但是 ViewGroup 会默认启用这个优化标记位
- 这个标记位对实际开发的意义是:当自定义的控件继承自 ViewGroup 并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化
- 当明确知道一个 ViewGroup 需要通过
onDraw()
方法来绘制内容时,需要显示关闭WILL_NOT_DRAW
这个标记位