ViewRoot 和 DecorView
ViewRoot 对应于 ViewRootImpl,用来对 view 进行操作,它和最顶层 view 一起组成了显示上的 Window。
DecorView 是 Activity、Dialog 中最顶层的 View。
View 的绘制流程总述:
View 的绘制流程是通过 ViewRoot 完成的。在 ActivityThread 中,当 Actiivity 被创建后,会通过 PhoneWindow 生成 DecorView,通过 WindowManager 创建 ViewRootImpl,ViewRootImpl 会对 DecorView 进行操作,和 DecorView 一起组成用来显示的 Window。
从 ViewRootImpl 的 performTraversals 方法开始,它经过 measure、layout 和 draw 三个过程才能将一个 View 绘制出来。
measure 确定了 View 的测量宽高。完成后,可以通过 getMeasuredWidth 和 getMeasuredHeight 来获取测量后的宽高,除特殊情况外,这个宽高都等于 View 最终的宽高。
layout 确定了 View 四个顶点的坐标和最终宽高。完成后,可以通过 getTop、getBottom、getLeft、getRight 获取四个顶点的位置,并可以通过 getWidth、getHeight 获取 View 的最终宽高。
draw 将 View 绘制到屏幕上。
MeasureSpec
MeasureSpec 包含了 View 的测量信息。
在测量过程中,系统会将 View 的 LayoutParams 根据父容器的 MeasureSpec 转换成相应的 MeasureSpec,然后根据这个 measureSpec 来测量出 View 的宽高。这个宽高不一定是最终的宽高。
MeasureSpec 的表示
MeasureSpec 代表一个 32 位 int 值,高 2 位代表 SpecMode,低 30 位代表 SpecSize,SpecMode 指测量模式,SpecSize 指规格大小。将它们组合成一个 int 值是为了避免过多的对象内存分配,为了方便操作,MeasureSpec 也提供了打包和解包的方法。
SpecMode 有三类,每一类都表示特殊的含义:
- UNSPECIFIED
父容器不对 View 有限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
- EXACTLY
父容器已经检测出 View 所需要的精确大小,这时候 View 的最终大小就是 SpecSize 所指定的值。它对应于 LayoutParams 中的 match_parent 和具体的数值这两种模式。
- AT_MOST
父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值,具体是什么要看不同 View 的具体实现。它对应于 LayoutParams 中的 wrap_content。
MeasureSpec 和 LayoutParams 的对应关系
对于顶级 View:
其 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 共同决定。
对于普通 View:
其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 共同决定。
自身的 LayoutParams 说明了自己希望多大,父容器的 MeasureSpec 说明了能给多大。
对于普通 View,View 的 measure 会由 ViewGroup 的 measureChild 或 measureChildWithMargins 调用:
ViewGroup.measureChildWithMargins()、ViewGroup.measureChild() --->
ViewGroup.getChildMeasureSpec()
getChildMeasureSpec 的表格化:
EXACTLY/parentSize | AT_MOST/parentSize | UNSPECIFIED/0 |
---|---|---|
dp/px | EXACTLY/childSize | EXACTLY/childSize |
match_parent | EXACTLY/parentSize | AT_MOST/parentSize |
wrap_content | AT_MOST/parentSize | AT_MOST/parentSize |
表格的解释:
parent 的 SpecMode/SpecSize | parent 的 SpecMode/SpecSize | parent 的 SpecMode/SpecSize |
---|---|---|
child 的 LayoutParams | child 的 SpecMode/SpecSize | child 的 SpecMode/SpecSize |
View 的工作流程
即 measure、layout、draw 三大流程
measure 过程
View 的 measure
public final void measure(int widthMeasureSpec, int heightMeasureSpec){
...
onMeasure(widthMeasureSpec, heightMeasureSpec)
...
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
);
}
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;
}
// UNSPECIFIED 的情况下:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth :
max(mMinWidth, mBackground.getMinimumWidth());
}
// Drawable.class
public int getMinimumWidth() {
// 有原始宽高,就返回原始宽高,无原始宽高返回 0
// BitmapDrawable 有原始宽高,ShapeDrawable 无原始宽高
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
UNSPECIFIED 情况下,size 会取 mMinWidth 或 mBackground.getMinimmWidth()。
AT_MOST、EXACTLY 情况下,size 会取 MeasureSpec 的 SpecSize。
所以,直接继承 View 的自定义控件需要重新 onMeasure 方法并设置 wrap_content 时的自身大小,否则在布局中使用 wrap_content 就相当于使用 match_parcent。
因为如果不重写,那 View 的 MeasureSpec 就是 AT_MOST/parcentSize,取到的 size 就是 parcentSize,这与设置为 match_parcent 取到的 size 是一样的。
重写 onMeasure:
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
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 (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize,mHeight);
}
}
这个处理方法不止处理了 View 设置为 wrap_content 的情况,也处理了 View 设置为 match_parcent 且其父容器 SpecMode 为 AT_MOST 时的情况(也就是其父容器设置为 wrap_content)。
wrap_content 默认对应的 MeasureSpec 只有 AT_MOST/parentSize。
ViewGroup 的 measure
对于 ViewGroup 来说,除了自己的 measure,还要调用其子元素的 measure。和 View 不同,ViewGroup 是一个 抽象类,它没有重写 View 的 onMeasure 方法,而是提供了一个 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);
}
}
}
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);
}
measureChildren 方法只是 ViewGroup 提供的一个measure 方法,在部分 ViewGroup 里使用到。
ViewGroup 可以实现自己的 onMeasure 方法,如 LinearLayout 的 onMeasure 里没有用 measureChildren,而是使用了自定义的 measureVertical、measureHorizontal,里面会循环调用 measureChildWithMargins;RelativeLayout 则直接在 onMeasure 里循环调用了 measureChild。
以 LinearLayout 为例,展示流程:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
measureVertical 的部分代码:
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
...
for (int i = 0; i < count; ++i) {
...
// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);
if (oldHeight != Integer.MIN_VALUE) {
lp.height = oldHeight;
}
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
...
}
...
}
void measureChildBeforeLayout(View child, int childIndex,
int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
int totalHeight) {
measureChildWithMargins(child, widthMeasureSpec, totalWidth,
heightMeasureSpec, totalHeight);
}
measureChildBeforeLayout 会调用 child View 的 measure 方法,之后会通过 totalLength 记录各 child 的总高度。当 child 的测量完成后,LinearLayout 会测量自己的大小。
// 还是在 measureVertical 内部
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
...
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
...
View 的 measure 过程是三大流程里最复杂的一个,measure 完成后,通过 getMeasuredWidth/Height 就可以获取到 View 的测量宽/高。需要注意的是,在某些极端情况下,系统可能需要多次 measure 才能确定最终的测量宽/高(如 FrameLayout、RelativeLayout measure 两次,待验证),这种情形下,在 onMeasure 中拿到的测量宽/高很可能是不准确的。一个比较好的习惯是在 onLayout 中获取 View 的测量宽/高或者最终宽/高。
View的三次measure,两次layout和一次draw
onCreate 中获取 View 宽高
在 Activity 启动时希望获得 View 的宽高,不能在 onCreate、onResume 中去做。因为 View 的 measure 过程和 Activity 的生命周期方法不是同步的,所以无法保证在 onCreate、onResume 执行时 View 的 measure 已经执行完毕,这时候就会得到 0。
下面介绍四种 onCreate 中获取 View 宽高的方法
(1)Activitity/View#onWindowFocusChanged
onWindowFocusChanged 这个方法的调用时机是,该 view 的 window 的获取焦点、失去焦点时。
window 获取焦点,说明已经在展示在用户面前,这时是可以获取到 view 的宽高的。
onWindowFocusChanged 会被调用多次,每次获取、失去焦点都会调用,所以,如果频繁地 onResume、onPause,那么 onWindowFocusChanged 也会被频繁调用。需要注意。
(2)view.post(runnable)
通过 post 可以将一个 runnable 投递到消息队列尾部,等 Looper 调用到此 runnable 时,View 已经初始化好了。
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {//在 activity onCreate 中attachInfo 当前还没有赋值
return attachInfo.mHandler.post(action);
}
// Assume that post will succeed later
ViewRootImpl.getRunQueue().post(action);
return true;
}
runable 会在 ViewRootImpl 的 performTraversals 中被 handler post,当 performTraversals 执行完后,runable 才会执行,也就是 view 绘制完毕后 runable 才会执行,这时就可以拿到宽高了。
View.post(Runnable)在onCreate获取控件宽高分析
(3)ViewTreeObserver
使用 ViewTreeObserver 的众多回调可以完成这个功能,比如使用 onGlobalLayoutListener 这个接口,当 View 树的状态发生改变或者 View 树内部的 View 的可见性发生改变时,onGlobalLayout 方法将被回调,因此这时获取 View 宽高的一个时机。需要注意,随着 View 树状态变化,onGlobalLayout 会被调用多次。典型代码如下:
ViewTreeObserver observer = mView.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int width = mView.getMeasuredWidth();
int height = mView.getMeasuredHeight();
}
});
(4)view.measure(int widthMeasureSpec, int heightMeasureSpec)
通过手动 measure 得到 View 的宽高,根据 LayoutParams 分三种情况:
match_parcent:
当其 parcent 的 SpecMode 为 EXACTLY 时,没有办法。因为这时 View 的 measure 必须知道 parcentSize,而这我们得不到。
当其 parcent 的 SpecMode 为 AT_MOST 时,可以通过下面 wrap_content 的方法来获取,因为这时 match_parcent 的效果与 wrap_content 的效果是一样的,获取到的 MeasureSpec 都是 AT_MOST/parcentSize。
(这里我认为书中表述有误,我按我的理解写了。)
具体的数值(dp/px):
// 宽高都为 100 px
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int heighMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
mView.measure(widthMeasureSpec, heighMeasureSpec);
int width = mView.getMeasuredWidth();
int heigh = mView.getMeasuredHeight();
其实这种情况下,如果是 dp 的话可以直接换算成 px,如果是 px 的话根本不用算。
wrap_content:
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
int heighMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
mView.measure(widthMeasureSpec, heighMeasureSpec);
int width = mView.getMeasuredWidth();
int heigh = mView.getMeasuredHeight();
如上所说,这时获取到的 MeasureSpec 是 AT_MOST/parcentSize。因为 parcentSize 不确定,这时可以按 parcentSize 理论上的最大值(1<<30-1)来 measure。这需要 View 对 AT_MOST 的情况做处理,就像上面所说的重写 onMeasure,否则获取到的宽高就是 (1<<30-1) 了。
layout 过程
Layout 的作用是 ViewGroup 用来确定子元素的位置,当 ViewGroup 的位置被确定后,它在 onLayout 中会遍历所有的子元素并调用其 layout 方法,layout 又会调用 onLayout。即 layout 方法确定 View 本身位置,onLayout 确定所有子元素位置。
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
首先通过 setFrame 来设定 View 的四个顶点的位置,即初始化 mLeft、mRight、mTop 和 mBottom 四个值,View 的四个顶点确定了,其在父容器中的位置也就确定了;接着会调用 onLayout 方法,这个方法的用途是确定子元素的位置,和 onMeasure 类似,onLayout 的具体实现和布局有关,View 和 ViewGroup 均没有实现。
LinearLayout 的 onLayout 举例。
getWidth 和 getMeasuredWidth
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
一般情况下,它们都是相等的。
特殊情况包括:
人为修改 mRight、mLeft 的值,这会导致 getWidth 数值变化(当然这会导致 View 显示不正常且没什么意义)。如:
public void layout(int l, int t, int r, int b) {
super.layout(l+10, t+10, r+10, b+10);
}
View 需要多次测量的时候,最后的测量值会等于最终值,但前几次的测量值可能与最终宽高不一致。
draw 过程
draw 过程包括:
- 绘制背景 background.draw(canvas)
- 绘制自己 (onDraw)
- 绘制 children (dispatchDraw)
- 绘制装饰 (onDrawScrollBars)
setWillNotDraw
这个 View 的一个特殊方法。
如果一个 View 不需要绘制任何内容,那么设置这个标记位为 true 后,系统会进行相应的优化。ViewGroup 会默认开启这个标记位(View 没有),当我们的自定义控件继承于 ViewGroup 时,可以根据需不需要绘制来进行相应的处理。
自定义 View
自定义 View 的分类
继承 View 重写 onDraw 方法
需要自己支持 wrap_content,padding 也要自己处理。
继承 ViewGroup 派生特殊的 Layout
略复杂,需要自己处理 ViewGroup 的 measure、layout 及其子元素的 measure、layout。
继承特定的View
比较常见,用于扩展已有 View 的功能,如 TextView、ImageView 等。不需要自己支持 wrap_content、padding。
继承特定的 ViewGroup
如 LinearLayout,不需要自己处理 ViewGroup 的测量和布局过程。
自定义 View 须知
1. 让 View 支持 wrap_content
上面说了很多次,需要重写 onMeasure。
2. 如有必要,支持 padding
直接继承于 View,需要在 draw 方法中处理 padding。直接继承于 ViewGroup,需要在 onMeasure 和 onLayout 中考虑 padding 和子元素的 margin 对其造成的影响。
3. 尽量不要在 View 中使用 Handler,没必要
View 本身就提供了 post 系列方法。
4. View 中如果有线程或动画,需要及时停止,参考 View#onDetachedFromWindow
如果有线程或动画需要停止,onDetachedFromWindow 是一个很好的时机。当包含此 View 的 Activity 退出或当前 View 被 remove 时,View 的 onDetachedFromWindow 方法会被调用,和它对应的是 onAttachedToWindow,当包含此 View 的 Activity 启动时,View 的 onAttachToWindow 会被调用(在 Activity#onResume 后,View#onDraw 前)。同时,当 View 变得不可见时也要及时停止,避免内存泄漏。
5. View 带有滑动嵌套情形时,需要处理好滑动冲突