前言
DecorView
他是一个真正的View(FrameLayout子类),整个页面最顶层的布局。
我们写的
setContentView(R.layout.xxx),只是把写的xml布局塞到了DecorView内部的一块内容区域里面。DecorView还包含状态栏、标题栏这些系统UI。
ViewRootImpl
它不是View,并不继承于View,只是一个调度控制器:
- 每一个Activity窗口,就对应一个ViewRootImpI对象。
- 手里持有DecorView的引用。
- 负责接收屏幕刷新信号(VSync),驱动整棵View树执行measure测量→layout布局→draw绘制。
- 触摸事件也是它分发给各个View。
创建时机:
onCreate的setContentView()仅仅把xml解析成一堆View对象,此时ViewRootImpI还没有被创建,屏幕还什么都没画。等到
onResume()之后,系统把DecorView交给WindowManager,这时才new出ViewRootImpI,开始工作。
Canvas画布
Canvas就是绘图画板。
系统提前准备好一块画板,传给各个View。所有绘制操作
draw图形、draw文字、draw图片,全部在这块画板上。
画板全部画完之后,系统把整块画板的内容渲染到手机屏幕。
当启动一个Activity时,ActivityThread.performLaunchActivity()创建了Activity实例,调用onCreate()方法,在onCreate()中我们调用了setContentView(),此时仅仅只是把布局xml解析成View树,但是View还没有执行measure、layout、draw,屏幕上什么都还没有画出来,那他是怎么画出UI的呢?我们接下来来看一下。
整个View树的调度入口是ViewRootImpl.performTraversals() 。
而performTraversals()就是整个View体系的总调度方法,内部执行三大流程:
- performMeasure()测量流程
- performLayout()布局流程
- performDraw()绘制流程
三大流程全部是递归执行:从根View(DecorView)开始,一层一层遍历所有子View。
Measure测量过程
作用:测量每一个View期望的宽、高,得到
mMeasuredWidth、mMeasuredHeight。注意:测量出来的只是期望尺寸,不一定等于最终屏幕显示尺寸
ViewRootImpI#performMeasure
java
private void performMeasure(int childWidthMeasureSpec, int heightMeasureSpec) {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
mView就是DecorView(整个窗口的根布局),从根开始发起测量
需要注意的是,在performMeasure方法中需要传入两个参数childWidthMeasureSpec、heightMeasureSpec;要了解这两个参数,需要先了解MeasureSpec。
MeasureSpec 规则
MeasureSpec是View的内部类,封装了View的尺寸规格,代表了32位的int值:高2位代表mode模式(specMode,测量模式),低30位代表size尺寸(specSize,测量大小)。每一个View都持有一个MeasureSpec;specMode有三种模式:
EXACTLY:对应match_parent、具体dp数值。父容器已经确定的View的大小,View直接使用size。AT_MOST:对应wrap_content。View最大不能超过size,View自己计算合适的大小。UNSPECIFIED:父容器不做限制,子View可以任意大小,多用于ScrollView。
View#measure()
java
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
//处理缓存、是否需要重新测量标记
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
measure()被final修饰,子类不能重写- measure是对外的入口,真正留给我们自定义的是
onMeasure()
View#onMeasure()
java
protected void omMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
getSuggestedMinimumWidth():返回View最小宽度,来源于android:minWidth或者drawable背景的最小宽度。getDefaultSize():根据父容器传入的MeasureSpec模式,计算出View尺寸。setMeasuredDimension():必须调用,把计算好的宽高保存到成员变量mMeasuredWidth,mMeasuredHeight。
ViewGroup的测量逻辑
ViewGroup没有提供统一的onMeasure实现,不同子类(LinearLayout、RelativeLayout)各自重写。
ViewGroup测量两件事:
- 遍历所有子View,调用
child.measure(childWidthSpec, childHeightSpec),让子View完成测量,得到子View的measuredWidth/measuredHeight。 - 根据所有子View的测量结果,计算ViewGroup自身需要多大,调用
setMeasuredDimension保存自己的测量宽高。
小结:
- View:onMeasure -> setMeasuredDimension,确定自己大小
- ViewGroup:先全部子View measure,再计算自己大小。
Layout布局过程
作用:确定View在父容器当中的位置,给View赋值
mLeft、mTop、mRight、mBottom四个坐标。measure只管大小,layout管放在哪里。
ViewRootImpI#performLayout
java
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
mView.layout(0, 0, desiredWindowWidth, desiredWindowHeight);
}
从DecorView开始递归布局
View#layout
java
public final void layout(int l, int t, int r, int b) {
boolean changed = setFrame(l, t, r, b);
if(changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
}
}
layout同样是final,不能重写。setFrame(l, t, r, b):核心方法,把传入的左上右下四个坐标保存给成员变量mLeft、mTop、mRight、mBottom,标记这个View在父容器中的矩形位置。返回值changed代表位置是否发生改变。- 如果位置发生变化,调用
onLayout()。
View#onLayout
java
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
- 普通View:onLayout是空实现,没有子View,不需要摆放子元素。
- ViewGroup必须重写onLayout :循环遍历子View,调用
child.layout(l, t, r, b),把计算好的子View坐标传递过去,完成子View的位置摆放。
重要区分
getMeasuredWidth():measure阶段得到的测量宽高getWidth():return right-left,layout之后,由left、right计算出来的实际显示宽度。两者大多数情况相等,但layout阶段可以人为修改坐标,二者可以不相等。
scrollX、scrollY:不会修改left、top、right、bottom这四个坐标,仅仅是绘制阶段画布偏移,只影响draw流程
Draw绘制过程
作用:把View绘制到屏幕画布Canvas上。
入口:ViewRootImpl.performDraw (),触发DecorView的draw方法,递归绘制整棵View树。
两个易混淆的的方法
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime):绘制总入口;protected void onDraw(Canvas canvas):空实现,自定义View重写,绘制自己的内容。
draw方法内部完整6步流程
- 绘制背景drawBackground();
- 保存画布(可选,可跳过);
- 绘制View自身内容onDraw();
- 绘制子View dispatchDraw();
- 绘制褪色边缘阴影效果(可选,可跳过);
- 绘制前景装饰 onDrawForefround();
绘制顺序规则:后绘制的内容,会覆盖先绘制的内容。
1. drawBackground 绘制背景
java
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
if (background == null) {
return;
}
setBackgroundBounds();
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
-
mBackground对应xml中android:background设置的Drawable。 -
setBackgroundBounds()设置背景Drawable的绘制边界。 -
判断
scrollX、scrollY滚动偏移:- 如果没有滚动,直接
background(canvas)绘制背景 - 如果存在滚动,先
canvas.translate(scrollX,scrollY)平移画布,绘制背景,绘制完成再把画布平移复原。
scrollX/scrollY 在这里生效,仅仅改变画布,不会改变 View 的位置坐标。
- 如果没有滚动,直接
3. onDraw()绘制View自身内容
java
protected void onDraw(Canvas canvas) {
}
系统是空实现,由我们自定义View重写,在这里绘制图形、文字。
ViewGroup默认开启
WILL_NOT_DRAW标记,标记自己不需要绘制自身,onDraw不会被回调。如果希望ViewGroup绘制自己,调用
setWillNotDraw(false)关闭标记。
4. dispatchDraw()绘制子View
java
protected void dispatchDraw(Canvas canvas) {
}
- 普通View:空实现,没有子View,什么都不执行。
- ViewGroup重写
dispatchDraw,遍历所有子View:
java
// ViewGroup.dispatchDraw核心逻辑
for (int i = 0; i < childrenCount; i++) {
View child = getChildAt(i);
drawChild(canvas, child, drawingTime);
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
drawChild内部调用dhild.draw(),开启递归,每一个子View完整执行整套draw六步流程:绘制背景→onDraw→dispatchDraw→前景。
递归链条:
java
ViewGroup.draw()
→ drawBackground()
→ onDraw()
→ dispatchDraw()
→ drawChild(child1) → child1.draw()【子View完整6步】
→ drawChild(child2) → child2.draw()【子View完整6步】
→ onDrawForeground()
6. onDrawForeground()绘制前景装饰
java
public void onDrawForeground(Canvas canvas) {
onDrawScrollIndicators(canvas);
onDrawScrollBars(canvas);
//绘制foreground前景Drawable
}
负责绘制:android:foreground前景Drawable、滚动条、滚动指示器。
这一步在绘制流程的最后,绘制内容会覆盖背景、自身内容、所有子View,显示在最顶层。
整体流程
java
ViewRootImpl.performTraversals()
├─ performMeasure() 测量阶段
│ └─ DecorView.measure()
│ └─ onMeasure()
│ └─ ViewGroup循环调用child.measure(),递归测量全部子View
├─ performLayout() 布局阶段
│ └─ DecorView.layout()
│ └─ onLayout()
│ └─ ViewGroup循环调用child.layout(),递归摆放全部子View
└─ performDraw() 绘制阶段
└─ DecorView.draw(canvas)
├─ drawBackground() 绘制背景
├─ onDraw() 绘制自己的内容
├─ dispatchDraw() 遍历子View
│ └─ child.draw() 每一个子View完整执行draw全部6步(递归)
└─ onDrawForeground() 绘制前景、滚动条