这是AndroidUI绘制流程分析的第二篇文章,主要分析界面中View是如何绘制到界面上的具体过程。
1、ViewRoot和DecorView
ViewRoot
对应于ViewRootImpl
类,它是连接WindowManager
和DecorView
的纽带,View的三大流程均是通过ViewRoot
来完成的。在ActivityThread
中,当Activity
对象被创建完毕后,会将DecorView
添加到Window
中,同时会创建ViewRootImpl
对象,并将ViewRootImpl
对象和DecorView
建立关联。
View
的绘制流程是从ViewRoot
的performTraversals
方法开始的,它经过measure
、layout
和draw
三个过程才最终将一个View
绘制出来。
measure
过程决定了View
的宽/高,Measure
完成以后,可以通过getMeasuredWidth
和getMeasuredHeight
方法来获取View
测量后的宽/高,在几乎所有的情况下,它等同于View的最终的宽/高,但是特殊情况除外。Layout
过程决定了View
的四个顶点的坐标和实际的宽/高,完成以后,可以通过getTop、getBottom、getLeft
和getRight
来拿到View的四个顶点的位置,可以通过getWidth
和getHeight
方法拿到View的最终宽/高。Draw
过程决定了View
的显示,只有draw
方法完成后View
的内容才能呈现在屏幕上。
DecorView
作为顶级View
,一般情况下,它内部会包含一个竖直方向的LinearLayout
,在这个LinearLayout
里面有上下两个部分,上面是标题栏,下面是内容栏。在Activity中,我们通过setContentView
所设置的布局文件其实就是被加到内容栏中的,而内容栏id为content
。可以通过下面方法得到content:ViewGroup content = findViewById(R.android.id.content)
。通过content.getChildAt(0)
可以得到设置的view
。DecorView
其实是一个FrameLayout
,View
层的事件都先经过DecorView
,然后才传递给我们的View
。
MeasureSpec
MeasureSpec
代表一个32位的int值,高2位代表SpecMode
,低30位代表SpecSize
,SpecMode
是指测量模式,而SpecSize
是指在某种测量模式下的规格大小。
SpecMode
有三类,如下所示:
UNSPECIFIED
父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部。
EXACTLY
父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值,对应于LayoutParams中的match_parent和具体的数值这两种模式。
AT_MOST
父容器指定一个可用大小即
SpecSize
,View
的大小不能大于这个值,对应于LayoutParams
中的wrap_content
。
LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而进一步决定View的宽/高。
对于顶级View,即DecorView和普通View来说,MeasureSpec的转换过程略有不同。对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams共同确定;
对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的Layoutparams共同决定;
MeasureSpec一旦确定,onMeasure就可以确定View的测量宽/高。
小结一下
当子
View
采用具体的宽/高时,不管父容器的MeasureSpec
是什么,View
的MeasureSpec
都是精确模式+子View
的LayoutParams
中指定的具体的大小。
当子
View
的宽/高采用match_parent
时,这时候父容器的MeasureSpec
会发挥作用,子View
的模式总是跟父容器的模式一样:
- 如果父容器的模式是精确模式(
EXACTLY
),那么子View
的MeasureSpec
就是精确模式+父容器的剩余空间(或者说父容器的可用空间);- 如果父容器的模式是最大模式
(AT_MOST)
,那么子View
的MeasureSpec
就是最大模式+父容器的剩余空间(或者说父容器的可用空间);- 如果父容器的模式是未指定模式(
UNSPECIFIED
),那么子View
的MeasureSpec
就是未指定模式+父容器的剩余空间或者0
当子 View 的宽高采用
wrap_content
时,这时候父容器的 MeasureSpec 同样会发挥作用:
- 如果父容器的模式是精确模式(
EXACTLY
),那么子View
的MeasureSpec
就是最大模式+父容器的剩余空间(或者说父容器的可用空间);- 如果父容器的模式是最大模式(
AT_MOST
),那么子View
的MeasureSpec
就是最大模式+父容器的剩余空间(或者说父容器的可用空间);- 如果父容器的模式是未指定模式(
UNSPECIFIED
),那么子View
的MeasureSpec
就是未指定模式+父容器的剩余空间或者0。
当子 View
的宽高采用 wrap_content
时,不管父容器的模式是精确模式还是最大模式,子 View
的模式总是最大模式+父容器的剩余空间。
View的工作流程
View
的工作流程主要是指measure
、layout
、draw
三大流程,即测量、布局、绘制。其中measure
确定View
的测量宽/高,layout
确定view
的最终宽/高和四个顶点的位置,而draw
则将View
绘制在屏幕上。
measure过程
measure
过程要分情况,如果只是一个原始的view
,则通过measure
方法就完成了其测量过程,如果是一个ViewGroup
,除了完成自己的测量过程外,还会遍历调用所有子元素的measure
方法,各个子元素再递归去执行这个流程。
View的measure过程
如果是一个原始的 View,那么通过 measure 方法就完成了测量过程,在 measure 方法中会去调用 View 的 onMeasure 方法,View 类里面定义了 onMeasure 方法的默认实现:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 先获取建议的最小宽度和高度
int suggestedMinimumWidth = getSuggestedMinimumWidth();
int suggestedMinimumHeight = getSuggestedMinimumHeight();
// 再通过 getDefaultSize 方法获取宽度和高度的测量值
int measuredWidth = getDefaultSize(suggestedMinimumWidth, widthMeasureSpec);
int measuredHeight = getDefaultSize(suggestedMinimumHeight, heightMeasureSpec);
// 最后调用 setMeasuredDimension 方法设置 View 宽度和高度的测量值。
setMeasuredDimension(measuredWidth, measuredHeight);
}
先看一下 getSuggestedMinimumWidth
和 getSuggestedMinimumHeight
方法的源码:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
我们只看
getSuggestedMinimumWidth
方法:如果View
的背景mBackground == null
成立,即View
没有设置背景,那么View
的建议最小宽度就是mMinWidth
(mMinWidth
对应于android:minWidth
这个属性所指定的值);如果View
设置了背景,那么 View 的建议最小宽度为mMinWidth
和mBackground.getMinimumWidth()
中较大的那个。mBackground
是一个 Drawable 对象,所以去看Drawable 类下的getMinimumWidth
方法:
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
可以看到,getMinimumWidth
方法获取的是 Drawable
的原始宽度。如果存在原始宽度(即满足 intrinsicWidth
> 0),那么直接返回原始宽度即可;如果不存在原始宽度(即不满足intrinsicWidth
> 0),那么就返回 0。
ShapeDrawable
没有原始宽度和高度,而BitmapDrawable
有原始宽度和高度。
接着看最重要的 getDefaultSize
方法:
// 这是一个 static 修饰的方法,所以是一个工具方法
/**
* Utility to return a default size. Uses the supplied size if the
* MeasureSpec imposed no constraints. Will get larger if allowed
* by the MeasureSpec.
*
* @param size Default size for this view
* @param measureSpec Constraints imposed by the parent
* @return The size this view should be.
*/
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;
}
如果specMode
为 MeasureSpec.UNSPECIFIED
即未指定模式,那么返回由方法参数传递过来的尺寸作为 View
的测量宽度和高度;
如果specMode
不是MeasureSpec.UNSPECIFIED
即是最大模式或者精确模式,那么返回从 measureSpec
中取出的specSize
作为 View
测量后的宽度和高度。
看一下刚才的表格:
当 specMode
为 EXACTLY
或者 AT_MOST
时,View 的布局参数为 wrap_content
或者 match_parent
时,给 View
的 specSize
都是 parentSize
。这会比建议的最小宽高要大。这是不符合我们的预期的。因为我们给View
设置wrap_content
是希望View的大小刚好可以包裹它的内容。
因此:
直接继承 View 的自定义控件需要重写 onMeasure 方法并设置 wrap_content
时的自身大小(给 View 指定一个默认的内部宽高)。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth, mHeight);
}else if(widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth, heightSpecSize);
}else if(heightSpecSize == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize, mHeight);
}
}
ViewGroup的measure过程
如果是一个 ViewGroup,除了完成自己的 measure 过程以外,还会遍历去调用所有子元素的 measure 方法,各个子元素再递归去执行 measure 过程。
ViewGroup 并没有重写 View 的 onMeasure 方法,但是它提供了 measureChildren、measureChild、measureChildWithMargins 这几个方法专门用于测量子元素。
// 遍历所有的子元素,并使用 measureChild 方法来对每一个子元素进行 measure。
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];
// 只有当子元素的可见性不是 GONE 时,才对它进行测量。这一点是个细节。
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
// 测量每一个子元素:最重要的逻辑是通过 getChildMeasureSpec 方法来获取测量子元素所需要的宽度 MeasureSpec 和 高度 MeasureSpec。
// getChildMeasureSpec(int spec, int padding, int childDimension) 方法需要的参数必须搞清楚:
// spec 是 ViewGroup 从 onMeasure 方法接收到的 MeasureSpec,
// padding 是 ViewGroup 已使用的空间,childDimension 是子元素的布局参数的宽和高。
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);
}
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
为什么ViewGroup 类没有像 View 一样对其 onMeasure 方法做统一的实现,实际上 ViewGroup 继承了 View 类对 onMeasure 方法的实现?这是因为不同的 ViewGroup 子类有不同的布局特性,这导致它们的测量细节各不相同,比如 LinearLayout 和 RelativeLayout 这两者的布局特性显然不同,因此 ViewGroup 无法统一实现。
为什么说在 onLayout 方法中去获取 View 的测量宽/高是一个好的习惯?
正常情况下,View 的 measure 过程完成以后,通过 getMeasuredWidth/getMeasuredHeight 方法就可以正确地获取到 View 的测量宽/高;但是,在某些极端情况下,系统可能需要多次 measure 才能确定最终的测量宽/高,在这种情形下,在 onMeasure 方法中拿到的测量宽/高很可能是不准确的。
在 Activity 启动时,如何获取某个 View 的宽/高?
方式 | 解释 |
---|---|
Activity/View.onWindowFocusChanged |
在 onWindowFocusChanged 回调时,表示 View 已经初始化完毕了,宽/高已经准备好了,所以这时去获取宽/高是没有问题的。但是,onWindowFocusChanged 会被多次调用,当 Activity 的窗口得到焦点和失去焦点时均会被调用一次。 |
View.post(Runnable runnable) |
通过 post 可以将一个 Runnable 对象放到消息队列的尾部,然后等到 Looper 调用此 Runnable 的时候,View 也已经初始化好了。注意:是 View.post 方法而不是 Handler.post 方法 |
ViewTreeObserver 的多个回调,如 OnGlobalLayoutListener 接口。 |
OnGlobalLayoutListener 接口:当 View 树的状态发生改变或者 View 树内部的 View 的可见性发生改变时,它的 onGlobalLayout 方法将被回调。需要注意的是,伴随着 View 树的状态改变等,onGlobalLayout 会被调用多次。使用完接口后,记得移除。 |
使用 View.measure(int widthMeasureSpec, int heightMeasureSpec) 方法进行手动测量 |
这种方法比较复杂。当 View 的 LayoutParams 为 match_parent 时,这种方式不能测量出具体的宽/高;当 View 的 LayoutParams 为具体的数值(如 100 dp)时,这种方式可以测量出具体的宽/高;当 View 的 LayoutParams 为 wrap_content 时, 这种方式不能测量出具体的宽/高。 |
Layout
如果是 View
的话,那么在它的 layout
方法中就确定了自身的位置(具体来说是通过 setFrame
方法来设定 View
的四个顶点的位置,即初始化 mLeft
,mRight
,mTop
,mBottom
这四个值),layout
过程就结束了。
如果是ViewGroup
的话,那么在它的layout
方法中只是确定了 ViewGroup
自身的位置,要确定子元素的位置,就需要重写 onLayout
方法;在 onLayout
方法中,会调用子元素的 layout
方法,子元素在它的layout
方法中确定自己的位置,这样一层一层地传递下去完成整个 View 树的 layout
过程。
layout 方法和 onLayout 方法的区别是什么?
layout
方法的作用是确定View
本身的位置,即设定View
的四个顶点的位置,这样就确定了 View
在父容器中的位置;
onLayout
方法的作用是父容器确定子元素的位置,这个方法在 View
中是空实现,因为 View
没有子元素了,在 ViewGroup
中则进行抽象化,它的子类必须实现这个方法。
View 的 getMeasuredWidth 和 getWidth 这两个方法有什么区别?
getMeasuredWidth
获取的是测量宽度,定义了一个View
想要在父View
里的尺寸,getWidth
获取的是宽度,有时也被称为绘制宽度,定义了绘制时或者布局后屏幕上的View
的实际尺寸。两者的赋值时机不同,测量宽/高的赋值时机要早于最终宽/高。具体来说,
View
的测量宽/高形成于View
的measure
过程,而View
的最终宽/高形成于View
的layout
过程。两者的值多数情况下是相等的,但在某些特殊情况下会不一致。例如有两种特殊情况:
- 重写 View 的 layout 方法:手动修改传入 layout 方法的参数值;
- View 需要多次 measure 才能确定自己的测量宽/高,在前几次的测量过程中,其得出的测量宽/高有可能和最终宽/高不一致,但最终来看,测量宽/高还是会和最终宽/高相同。
Draw
1.绘制背景(background.draw(canvas);
);
2.绘制自己(onDraw
);
3.绘制 children(dispatchDraw(canvas)
);
4.绘制装饰(onDrawScrollBars
)。
dispatchDraw
方法的调用是在 onDraw
方法之后,也就是说,总是先绘制自己再绘制子 View
。
对于 View
类来说,dispatchDraw
方法是空实现的,对于 ViewGroup
类来说,dispatchDraw
方法是有具体实现的。
ViewGroup 的 draw 过程是如何传递的
通过 dispatchDraw
来传递的。dispatchDraw
会遍历调用子元素的 draw
方法,如此 draw
事件就一层一层传递了下去。dispatchDraw 在 View 类中是空实现的,在 ViewGroup 类中是真正实现的。
View 类中的 setWillNotDraw 方法的含义及其开发意义是什么?
如果一个 View 不需要绘制任何内容,那么就设置这个标记为 true,系统会进行进一步的优化。
当创建的自定义控件继承于 ViewGroup 并且不具备绘制功能时,就可以开启这个标记,便于系统进行后续的优化;当明确知道一个 ViewGroup 需要通过 onDraw 绘制内容时,需要关闭这个标记。
查看 LinearLayout 对这个方法的调用:setWillNotDraw(divider == null);,在有 divider 时才会关闭这个标记,否则是打开的;ScrollView 直接使用 setWillNotDraw(false);;ViewStub 类则使用 setWillNotDraw(true);。
自定义View
自定义View的分类
分类 | 用途 | 特点 |
---|---|---|
1.继承 View 重写 onDraw 方法 | 用于实现一些不规则的效果,不方便通过布局的组合方式来达到 | 需要通过绘制的方式来实现,即重写 onDraw 方法;需要自己支持 wrap_content,处理 padding |
2.继承 ViewGroup 派生特殊的 Layout | 用于实现自定义的布局 | 稍微复杂一些,需要合适地处理 ViewGroup 的测量、布局这两个过程,并同时处理好子元素的测量和布局过程 |
3.继承特定的 View | 用于扩展已有的 View 的功能 | 不需要自己支持 wrap_content 和 padding 等 |
4.继承特定的 ViewGroup | 用于实现几种 View 组合在一起的效果 | 不要自己处理 ViewGroup 的测量和布局这两个过程,方法2能实现的效果一般方法4也可以实现,但方法2更接近 View 底层 |
自定义 View 的注意事项
- 对于直接继承
View
或者ViewGroup
的控件,要在onMeasure
方法中对wrap_content
做特殊处理; - 必要时,让自定义
View
支持padding
; - 尽量不要在自定义
View
中使用Handler
,用View
自己的post
方法就行; - 及时关闭自定义
View
中的线程或者动画,比如在onDetachedFromWindow
方法中; - 自定义
View
带有嵌套滑动时,要处理好滑动冲突; - 有自定义布局属性的,在构造方法中取得属性后应及时调用
recycle
方法回收资源; - 在
onDraw
和onTouchEvent
方法中都应避免创建对象,过多操作会造成卡顿; - 自定义
ViewGroup
要重载关于LayoutParams
的几个方法; - 必要时添加对
View
的状态存储与恢复的支持; - 对于自定义
ViewGroup
,需要绘制内容但是又没有在布局中设置background
的话,会画不出来,这时候需要调用setWillNotDraw
方法,并设置为false。
参考:《Android开发艺术探索》