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图像