文章目录
一、ViewRoot 和 DecorView
DecorView 作为顶级 View,它本质上是一个 FrameLayout。DecorView 有唯一的子 View,它是一个垂直 LinearLayout,包含两个子元素(具体情况和 Android 版本及主题有关),上面是标题栏,下面是内容栏。获得 content 的代码:ViewGroup content = (ViewGroup) findViewById(android.R.id.content),获取 content 中设置的 View 可以这样写: content .getChildAt (0)。
WindowManager 是一个接口,里面常用的方法有:添加View,更新View和删除View。主要是用来管理 Window 的。WindowManager 具体的实现类是WindowManagerImpl。最终,WindowManagerImpl 会将业务交给 WindowManagerGlobal 来处理。
ViewRoot 是连接 WindowManager 和 DecorView 的纽带,View 的三大流程均是通过 ViewRoot 来完成的。它的具体实现类是 ViewRootImpl,拥有 DecorView 的实例,通过该实例来控制 DecorView 绘制。
绘制流程
View 的绘制流程是从 ViewRoot 的 performTraversal 方法开始的,它经过 measure、layout 和 draw 三个过程才能最终将一个 View 绘制出来,其中 measure 用来测量 View 的宽和高,layout 用来确定 View 在父容器中的放置位置,而 draw 则负责将 View 绘制在屏幕上。
performTraversals 会依次调用 performMeasure、performLayout 和 performDraw 三个方法,这三个方法分别完成顶级 View 的 measure、layout 和 draw 这三大流程。
- performMeasure 中会调用 measure 方法,在 measure 方法中又会调用 onMeasure 方法,在 onMeasure 方法中则会对所有的子元素进行 measure 过程,接着子元素就会重复父容器的 measure 过程,如此反复完成整个 View 树的遍历。
- performLayout 的传递流程和 performMeasure 是类似的。
- performDraw 有所不同,它的传递过程是在 draw 方法中通过 dispatchDraw 来实现的,不过这并没有本质区别。
Measure 过程决定了 View 的宽/高,完成之后,可以通过 getMeasuredWidth 和 getMeasuredHeight 方法获取到 View 测量后的宽/高,在几乎所有情况下它都等同于 View 最终的宽/高,但是特殊情况除外,后面会进行说明。Layout 过程决定了 View 的四个顶点的坐标和实际的 View 的宽/高,完成之后,可以通过 getTop、getBottom、getLeft 和 getRight 来拿到 View 的四个顶点的位置,并可以通过 getWidth 和 getHeight 方法来拿到 View 的最终宽/高。draw 的过程则决定了 View 的显示,只有 draw 方法完成以后 View 的内容才能呈现在屏幕上。
二、理解 MeasureSpec
在测量过程中,系统会将 View 的 LayoutParams 根据父容器所施加的规则转换成对应的 MeasureSpec,然后再根据这个 measureSpec 来测量出 View 的宽/高,不过这里的宽/高并不一定等于 View 的最终宽/高。
2.1、MeasureSpec
MeasureSpec 代表一个 32 位的 int 值,高 2 位代表 SpecMode,低 30 位代表 SpecSize,SpecMode 是指测量模式,而 SpecSize 是指在某种测量模式下的规格大小。下面是部分源码。
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(int size, int mode) {
return size + mode;
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
MeasureSpec 通过将 SpecMode 和 SpecSize 打包成一个 int 值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包方法。SpecMode 和 SpecSize也是一个 int 值,一组 SpecMode 和 SpecSize 可以打包为一个 MeasureSpec,而一个 MeasureSpec 可以通过解包的形式来得出其原始的 SpecMode 和 SpecSize。
SpecMode 有三类
- UNSPECIFIED
父容器不对 View 有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。 - EXACTLY
父容器已经检测出 View 所需的精确大小,这时 View 的最终大小就是 SpecSize 所指定的值。它对应于 LayoutParams 中的 match_parent 和具体的数值这两种模式。 - AT_MOST
父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值,具体是什么值要看不同 View 的具体实现。它对应于 LayoutParams 中的 wrap_content。
2.2、MeasureSpec 和 LayoutParams 的对应关系
系统内部是通过 MeasureSpec 来进行 View 的测量,但是正常情况下我们使用 View 指定 MeasureSpec,尽管如此,但是我们可以给 View 设置 LayoutParams。在 View 测量时,系统会将 LayoutParams 在父容器的约束下转换成对应的 MeasureSpec,然后再根据这个 MeasureSpec 来确定 View 测量后的宽/高。需要注意的是,MeasureSpec 不是唯一由 LayoutParams 决定的,LayoutParams 需要和父容器一起才能决定 View 的 MeasureSpec,从而进一步决定 View 的宽/高。另外,对于顶级 View(即 DecorView)和普通 View 来说,MeasureSpec 的转换过程略有不同。对于 DecorView,其 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 来共同确定;对于普通 View,其 MeasureSpec 由父容器 MeasureSpec 和自身的 LayoutParams 来共同决定,MeasureSpec 一旦确定后,onMeasure 中就可以确定 View 的测量宽/高。
1、DocorView 的 MeasureSpec 的产生过程,是根据它的 LayoutParams 中的宽/高的参数来划分。
- LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小。
- LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小。
- 固定大小(比如 100dp):精确模式,大小为 LayoutParams 中指定的大小。
2、对于普通 View 来说,这里指我们布局中的 View,View 的 measure 过程由 ViewGroup 传递而来。子元素的 MeasureSpec 的创建与父容器的 MeasureSpec 和子元素本身的 LayoutParams 有关,此外还和 View 的 margin 及 padding 有关。
针对不同的父容器和 View 本身不同的 LayoutParams,View 可以有多种 MeasureSpec。当 View 采用固定宽/高时,不管父容器的 MeasureSpec 是什么,View 的 MeasureSpec 都是精确模式并且其大小遵循 LayoutParams 中的大小。当 View 的宽/高是 match_parent 时,如果父容器的模式是精准模式,那么 View 也是精准模式并且其大小是父容器的剩余空间;如果父容器是最大模式,那么 View 也是最大模式并且其大小不会超过父容器的剩余空间。当 View 的宽/高是 wrap_content 时,不管父容器的模式是精准还是最大化,View 的模式总是最大化并且大小不能超过父容器的剩余空间。对于 UNSPECIFIED 模式,主要用于系统内部多次 measure 的情形,不需过多关注。
三、View 的工作流程
View 的工作流程主要是指 measure、layout、draw 这三大流程,即测量、布局和绘制,其中 measure 确定 View 的测量宽/高,layout 确定 View 的最终宽/高和四个顶点的位置,而 draw 则将 View 绘制到屏幕上。
3.1、measure 过程
measure 过程要分情况来看,如果只是一个原始的 View,那么通过 measure 方法就完成了其测量过程,如果是一个 ViewGroup,除了完成自己的测量过程外,还会遍历去调用所有子元素的 measure 方法,各个子元素再递归去执行这个流程,下面针对这两种情况分别讨论。
View 的 measure 过程
View 的 measure 过程由其 measure 方法来完成,measure 方法是一个 final 类型的方法,这意味着子类不能重写此方法,在 View 的 measure 方法中回去调用 View 的 onMeasure 方法,因此只需要看 onMeasure 的实现即可,View 的 onMeasure 方法如下。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
onMeasure 方法中调用了一个方法 setMeasuredDimension ,这个方法设置了 View 宽/高的测量值,而这里的宽/高两个测量值又由 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,而这个 specSize 就是 View 测量后的大小,这里多次提到测量后的大小,是因为 View 的最终大小是在 layout 阶段确定的,这里必须要加以区分,但是几乎所有情况下 View 的测量大小与最终大小是相等的。
对于 UNSPECIFIED 模式,一般用于系统内部的测量过程,在这种情况下,View 的大小为 getDefaultSize 的第一个参数 size,即宽/高分别为 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 没有设置背景,那么 View 的宽度为 mMinWidth,对应了 android:minWidth 属性的值,如果不指定默认就为 0;如果 View 设置了背景,则返回 android:minWidth 和getMinimumWidth 这两者中的最大值。
public int getMinimumHeight() {
final int intrinsicHeight = getIntrinsicHeight();
return intrinsicHeight > 0 ? intrinsicHeight : 0;
}
getMinimumWidth 返回的就是 Drawable 的原始宽度,前提是这个 Drawable 有原始宽度,比如对于 ShapeDrawable 无原始宽/高,而 BitmapDrawable 有原始宽/高(图片的尺寸)。
从 getDefaultSize 方法的实现来看,View 的宽/高由 specSize 决定,所以我们可以得出如下结论:直接继承 View 的自定义控件需要重写 onMeasure 方法并设置 wrap_content 时的自身大小,否则在布局中使用 wrap_content 就相当于使用 match_parent。这是由于 View 如果在布局中使用 wrap_content,那么它的 specMode 是 AT_MOST 模式,在这种模式下,它的宽/高等于 specSize,而对照上面的表可知,在这种情况下 View 的 specSize 是 parentSize,而 parentSize 是父容器中目前可以使用的大小,也就是父容器当前剩余的空间大小。很显然,View 的宽/高就等于 父容器当前剩余的空间大小,这种效果和在布局中使用 match_parent 完全一致。解决这个问题代码如下。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthSpecMode: Int = MeasureSpec.getMode(widthMeasureSpec)
val widthSpecSize: Int = MeasureSpec.getSize(widthMeasureSpec)
val heightSpecMode: Int = MeasureSpec.getMode(heightMeasureSpec)
val heightSpecSize: Int = MeasureSpec.getSize(heightMeasureSpec)
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWith, mHeight)
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWith, heightSpecSize)
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, mHeight)
}
}
我们给 View 指定一个默认的内部宽/高(mWidth 和 mHeight),并在 wrap_content 时设置此宽/高即可。对于非 wrap_content 情形,我们沿用系统的测量值即可,至于这个默认的内部宽/高的大小如何指定,没有固定的依据,根据需要灵活指定即可。
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);
}
}
}
在 measureChildren 方法中,会调用一个 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];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
measureChild 的思想就是取出子元素的 LayoutParams,然后再通过 getChildMeasureSpec 来创建子元素的 MeasureSpec,接着将 MeasureSpec 直接传递给 View 的 measure 方法进行测量。
ViewGroup 并没有定义其测量的具体过程,这是因为 ViewGroup 是一个抽象类,其测量过程的 onMeasure 方法需要各个子类去具体实现,比如 LinearLayout、RelativeLayout 等,之所以不像 View 一样对其 onMeasure 方法做统一的实现,是由于不同的 ViewGroup 子类有不同的布局特性,这导致它们的测量细节各不相同,比如 LinearLayout 和 RelativeLayout 这两者的布局特性显然不同,因此 ViewGroup 无法做统一实现。
View 的 measure 过程是三大流程中最复杂的一个,measure 完成之后,通过 getMeasuredWidth/Height 方法就可以正确地获取到 View 的测量宽/高。需要注意的是,在某些极端情况下,系统可能需要多次 measure 才能确定最终的测量宽/高,在这种情形下,在 onMeasure 方法中拿到的测量宽/高很可能是不准确的。一个比较好的习惯是在 onLayout 方法中去获取 View 的测量宽/高或者最终宽/高。
3.2、layout 过程
layout 的作用是 ViewGroup 用来确定子元素的位置,当 ViewGroup 的位置被确定后,它在 onLayout 中会遍历所有的子元素并调用其 layout 方法,在 layout 方法中 onLayout 方法又会被调用。layout 过程和 measure 过程相比就简单多了,layout 方法确定 View 本身的位置,而 onLayout 方法则会确定所有子元素的位置,先看 View 的 layout 方法。
@SuppressWarnings({"unchecked"})
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);
}
}
layout 方法的大致流程如下:首先会通过 setFrame 方法来设定 View 的四个顶点的位置,即初始化 mLeft、mRight、mTop 和 mButton 这四个值,View 的四个顶点一旦确定,那么 View 在父容器中的位置也就确定了;接着会调用 onLayout 方法,这个方法的用途是父容器确定子元素的位置,和 onMeasure 方法类似,onLayout 的具体实现同样和具体的布局有关,所以 View 和 ViewGroup 均没有真正实现 onLayout 方法。
说一说View 的测量宽/高的区别:在 View 的默认实现中,View 的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于 View 的 measure 过程,而最终宽/高形成于 View 的 layout 过程,即两者的赋值时机不同,测量宽/高的赋值时机早一些。因此,在日常开发中,我们可以认为 View 的测量宽/高就等于最终宽/高。但是在某些情况下,View 需要多次 measure 才能确定自己的测量宽/高,在前几次的测量过程中,其得出的测量宽/高有可能和最终宽/高不一致,但最终来说,测量宽/高还是和最终宽/高相同。
3.2、draw 过程
draw 过程比较简单,它的作用是将 View 绘制到屏幕上面。View 的绘制过程遵循如下几步:
- 绘制背景 background.draw(canvas)。
- 绘制自己(onDraw)。
- 绘制 children(dispatchDraw)。
- 绘制装饰(onDrawScrollBars)。
View 绘制过程的传递是通过 dispatchDraw 来实现的,dispatchDraw 会遍历调用所有子元素的 draw 方法,如此 draw 事件就一层层地传递了下去。View 有一个特殊的方法 setWillNotDraw,先看它的源码,如下所示。
/**
* 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 should 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);
}
从 setWillNotDraw 这个方法的注释可以看出,如果一个 View 不需要绘制任何内容,那么设置这个标记位为 true 以后,系统会进行相应的优化。默认情况下,View 没有启用这个优化标记位,但是 ViewGroup 会默认启用这个优化标记位。这个标记位对实际开发的意义是:当我们自定义控件继承于 ViewGroup 并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。当然,当明确知道一个 ViewGroup 需要通过 onDraw 来绘制内容时,我们需要显示地关闭 WILL_NOT_DRAW 这个标记位。
四、自定义 View
4.1、自定义 View 的分类
自定义 View 的分类标准不唯一,这里把自定义 View 分为 4 类。
1、继承 View 重写 onDraw 方法
这种方法主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,往往需要静态或者动态地显示一些不规则的图形。需要通过绘制的方式来实现,即重写 onDraw 方法。采用这种方法需要自己支持 wrap_content,并且 padding 也需要自己处理。
2、继承 ViewGroup 派生特殊的 Layout
这种方法主要用于实现自定义的布局,即除了 LinearLayout、RelativeLayout、FrameLayout 这几种系统布局之外,我们重新定义一种新布局,当某种效果看起来很像几种 View 组合在一起的时候,可以采用这种方法来实现。采用这种方式稍微复杂一些,需要合适地处理 ViewGroup 的测量、布局这两个过程,并同时处理子元素的测量和布局过程。
3、继承特定的 View(比如 TextView)
这种方法比较常见,一般是用于扩展某种已有的 View 的功能,比如 TextView,这种方法比较容易实现。这种方法不需要自己支持 wrap_content 和 padding 等。
4、继承特定的 ViewGroup(比如 LinearLayout)
这种方法也比较常见,当某种效果看起来很像几种 View 组合在一起的时候,可以采用这种方法来实现。采用这种方法不需要自己处理 ViewGroup 的测量和布局这两个过程。需要注意这种方法和方法 2 的区别,一般来说方法 2 能实现的效果方法 4 也都能实现,两者的主要差别在于方法 2 更接近 View 的底层。
4.2、自定义 View 须知
1、让 View 支持 wrap_content
这是因为直接继承 View 或 ViewGroup 的控件,如果不在 onMeasure 中对 wrap_content 做特殊处理,那么当外界布局中使用 wrap_content 时就无法达到预期的效果。
2、如果有必要,让你的 View 支持 padding
这是因为直接继承 View 的控件,如果不在 draw 方法中处理 padding,那么 padding 属性是无法起作用的。另外,直接继承自 ViewGroup 的控件需要在 onMeasure 和 onLayout 中考虑 padding 和子元素的 margin 对其造成的影响,不然会导致 padding 和子元素的 margin 失效。
3、尽量不要在 View 中使用 Handler,没必要
这是因为 View 内部本身就提供了 post 系列的方法,完全可以替代 Handler 的作用,当然除非很明确地要使用 Handler 来发送消息。
4、View 中如果有线程或者动画,需要及时停止
如果线程或者动画需要停止时,那么 onDetachedFromWindow 是一个很好地时机。当包含 View 地 Activity 退出或者当前 View 被 remove 时,View 的 onDetachedFromWindow 方法会被调用,和此方法对应的是 onAttachedToWindow,当包含此 View 的 Activity 启动时,View 的 onAttachedToWindow 方法会被调用。同时,当 View 变得不可见时我们也需要停止线程和动画,如果不及时处理这种问题,有可能会造成内存泄漏。
5、View 带有滑动嵌套情形时,需要处理好滑动冲突
如果有滑动冲突的话,要合适地处理滑动冲突,否则将会严重影响 View 的效果。