View的绘制流程
Activity、Window、DecorView、ViewRoot之间关系
知识储备
Activity : Activity只负责控制生命周期和处理事件,视图控制由Window负责。
Window:每个Activity都会创建一个Window用于承载View视图的显示,Window是一个抽象类存在了一个唯一实现类PhoneWindow
PhoneWindow:PhoneWindow是Android系统中的一个关键类,它是Android应用程序窗口的实现类之一,用于管理应用程序窗口的显示和交互。
DecorView:DecorView继承自FrameLayout,可以认为DecorView是View的根布局。DecorView内部是一个LinearLayout。1
包含三个子View:ViewStub(用于运行时动态加载布局资源)、TitleView、ContentView。
- ViewStub:
ViewStub(用于运行时动态加载布局资源) - TitleBar:屏幕顶部的状态栏
- ContentView:
Activity对应的XML布局,通过setContentView设置到DecorView中。
在Activity.onCreate()方法中调用setContentView()方法就是将Activity资源文件添加到ContentView中。
ViewRoot:ViewRoot的实现类为ViewRootImpl,它是连接WindowManagerService和DecorView的纽带。所有View的绘制和事件分发都是通过ViewRoot完成。
View的绘制
前期工作
-
通过
ClassLoader创建一个Activity实例后,调用activity#attach()方法对Activity进行初始化。 -
在
Activity#attach()方法中初始化一个PhoneWindow对象,与该Activity进行绑定。final void attach() { ...... mWindow = new PhoneWindow(this, window, activityConfigCallback); ...... } -
在
Activity#onCreate()生命周期初始化DecorView对象; -
在
Activity#onResume()生命周期之后,在WindowManagerGlobal#addView()方法中初始化ViewRoot对象,然后将DecorView和ViewRoot关联,调用ViewRoot#setView()方法开始View绘制流程。 -
在
ViewRoot中将View的绘制流程封装为一个Runnable,在Choreographer类中将其封装为一个异步消息,然后通过主线程Handler发送该消息,使View的绘制优先被处理。 -
这个消息被处理后,最终会来到
View的绘制的入口方法performTraversals(),分别调用performMeasure()、performLayout()、performDraw()进行View的测量、布局与绘制。
private void performTraversals() {
...............
//measur过程
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...............
//layout过程
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...............
//draw过程
performDraw();
}
开始绘制
- measure:为测量宽高过程,如果是ViewGroup还要在onMeasure中对所有子View进行measure操作。
- layout:用于摆放View在ViewGroup中的位置,如果是ViewGroup要在onLayout方法中对所有子View进行layout操作。
- draw:往View上绘制图像。
Measure
在ViewRootImpl#performMeasure()方法中会调用DecorView#measure()(也就是View#measure()方法),从根布局DecorView开始View的绘制。下面代码的mView就是我们的DecorView
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
.在ViewGroup#onMeasure()方法中,会遍历所有的子View,通过ViewGroup#measureChildWithMargins(),结合父View的MeasureSpec、margin、padding属性计算子View的MeasureSpec,然后调用子View的measure()方法。
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//计算子`View`的`MeasureSpec`
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);
//调用子`View`的`measure()`方法
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
然后ViewGroup结合MeasureSpec和子View的最大宽高计算出自己的宽高。
在View#onMeasure()方法中,只需要设置自己的宽高即可,若SpecMode为AT_MOST或EXACTLY,则直接使用SpecSize作为控件的宽高。
在View#setMeasruedDimensionRaw()方法中会设置mMeasureWidth、mMeasureHeight的值,这样在Measure阶段完成之后就可以通过View#getMeasureWidth()和View#getMeasureHeight()方法获取控件的测量宽高了。
补充知识点:MeasureSpec
在Measure过程中使用到一个比较重要的参数MeasureSpec,这是一个int型变量,前2位代表测量模式SpecMode,后30位代表测量尺寸SpecSize,它是父控件提供给子控件的参数,用于作为一个子控件自身大小的参考。
SpecMode共有以下三种模式:
| SpecMode | 说明 |
|---|---|
UNSPECIFIED | 父容器不对View有任何限制,一般用于系统内部。例如我们常用的ScrollView就是这种测量模式。 |
EXACTLY | 父容器已经检测出View所需大小,View的最终大小为SpecSize。对应LayoutParams中的match_parent或具体数值。 |
AT_MOST | 父容器指定了一个可用大小SpecSize,View的大小不能超过这个阈值。对应LayoutParams中的wrap_content。 |
Layout
Layout阶段根据前面测量出View的尺寸以及View的一些属性(padding、margin)来确定View的位置,同样也是从根布局DecorView开始,调用DecorView#layout()方法,最终来到View#layout()。
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
.........
final View host = mView;
if (host == null) {
return;
}
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
.........
}
在View中通过mLeft、mTop、mBottom、mRight四个变量来保存该View到父View的距离,在setFrame()方法中更新这几个变量的值,来确定该View在父布局中的位置。
如果View是ViewGroup类型,需要重写onLayout()方法对子控件进行布局(默认情况下onLayout()方法是个空实现),以FrameLayout为例,调用layoutChildren()遍历所有子View,计算出子View与父View的相对位置,然后调用child#layout()方法对子View进行布局。
在Layout阶段会设置View的mLeft、mTop、mBottom、mRight四个变量的值,然后可以通过调用View#getWidth()和View#getHeight()方法来获取控件实际的宽高。
Draw
相比于measure和layout阶段,draw阶段中View和ViewGroup变得没那么紧密了,View的绘制过程中不需要考虑ViewGroup,而ViewGroup也只需触发子View的绘制方法即可。
performDraw()执行后同样会从根布局开始逐层对每个View进行draw操作,在View中绘制操作时通过draw()进行,来看一下其主要源码:
public void draw(Canvas canvas) {
........
// 绘制背景
drawBackground(canvas);
// 绘制内容,每个控件自己实现自己的绘制逻辑
onDraw(canvas);
// 绘制子View
dispatchDraw(canvas);
// 绘制装饰,如scrollBar
onDrawForeground(canvas)
........
}
一些常用的绘制内容:
- Canvas:画布,不管是文字,图形,图片都要通过画布绘制而成
- Paint:画笔,可设置颜色,粗细,大小,阴影等等等等,一般配合画布使用
- Path:路径,用于形成一些不规则图形。
- Matrix:矩阵,可实现对画布的几何变换。
总结
从ViewRoot#perfromTraversals()方法开始View的绘制,分别调用performMeasure()、performLayout()、performDraw()进行View的测量、布局、绘制。
Measure:首先调用根布局DecorView#measure()方法开始,实际测量逻辑在onMeasure()方法中,measure()方法被final修饰,因此如果我们要重写测量的逻辑只能重写onMeasure()方法,父View先遍历子View,然后调用子View#measure()方法,测量完子View之后才测量父View自己。
Layout:首先调用DecorView#layout()方法开始布局,layout()方法同样被final修饰,因此如果我们要自定义View的布局只能在onLayout方法中完成。在父View中调用setFrame()方法完成自身布局,然后调用onLayout()方法对子View进行布局,调用layoutChildren() -> child#layout()
Draw:首先调用DecorView#draw(),如果是ViewGroup判断是否需要跳过自身绘制,是则调用dispatchDraw(),否则调用onDraw()绘制本身,然后再dispatchDraw()。
自定义View重写方法:
- onMeasure():自定义View的大小
- onLayout():改变View在父控件中的位置
- onDraw():绘制View图像