ViewRootImpl
1、ViewRootImpl创建和初始化过程通常发生在 Activity 的 onAttachedToWindow() 回调方法中(Activity 视图附加到窗口),它发生在 onResume() 之前或之中(具体取决于 Activity 的启动和恢复过程)。
2、ViewRootImpl作用
- 是连接WindowManager和DecorView的桥梁。每一个View都会有一个
ViewRootImpl
对象,View绘制的起点就是ViewRootImpl的perfromTraversals
方法,在perfromTraversals
内部会调用measure、layout、draw
这三大流程。 - 当Activity的DecorView被创建并添加到Window中后,系统会为其创建一个ViewRootImpl对象,用于管理该View的绘制和事件分发。
获取宽高的时机
所以在 onResume()
直接获取视图宽高( getWidth()
或 getHeight()
)可能会得到 0,因为此时视图可能还没有完成测量和布局。
-
在
onWindowFocusChanged(boolean hasFocus)
方法中获取:当 Activity 的窗口获得焦点时,视图已经完成布局,此时可以安全地获取宽高。 -
使用
ViewTreeObserver
的OnGlobalLayoutListener
:通用方法,它允许您在视图树的全局布局状态改变时(即视图完成布局后)收到回调。您可以在视图的getViewTreeObserver()
上注册这个监听器,并在回调中获取宽高。 -
在
onSizeChanged(int w, int h, int oldw, int oldh)
方法中:自定义视图在这个回调中获取宽高,它会在视图的尺寸改变时被调用。
示例代码
使用 ViewTreeObserver
的示例:
View myView = findViewById(R.id.my_view);
ViewTreeObserver vto = myView.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
// 移除监听器,避免重复调用
myView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
// 在这里安全地获取宽高
int width = myView.getWidth();
int height = myView.getHeight();
// 使用宽高进行后续操作
}
});
1. 测量(Measure)
-
测量过程:
- 目的:确定View的大小,即宽高
- 概述:系统会根据XML布局文件和代码中对控件属性的设置,来计算或获取每个View和ViewGroup的尺寸。
- 执行方式:每个View和ViewGroup会调用
measure()
方法,根据自身的特性(如宽高、间距等)以及父容器的约束条件,计算出自己的测量宽度和高度。
-
测量模式:
- EXACTLY:父容器已经决定了子View的确切大小。
- AT_MOST:子View可以指定自己的最大尺寸,但不应超过父容器允许的大小。
- UNSPECIFIED:父容器不对子View施加任何约束,子View可以自由地确定自己的大小(这种情况很少见)。
-
递归测量:测量过程是从根View(通常是DecorView)开始,递归地向下测量每一个子View。
每个View都会根据自己的测量模式和父容器的约束来计算自己的大小。
2. 布局(Layout)
-
目的:确定View在屏幕上的位置,即其在父视图坐标系的具体位置(左上角原点,往右是x正轴,往下是Y正轴)
-
布局过程:在测量完成后,每个View会调用
layout()
方法来设置自己在父容器中的确切位置和大小。
layout()
方法接受四个参数:left、top、right、bottom,分别表示View的左、上、右、下边界在父容器中的位置。 -
递归布局:布局过程也是从根View开始,递归地向下为每个子View设置位置和大小。
在这个过程中,父容器会根据子View的测量结果和自己的布局参数(如padding、margin等)来确定子View的最终位置和大小。
3. 绘制(Draw)
-
绘制过程:将View的内容渲染到屏幕上。调用
draw()
方法来实现。
通常调用onDraw()
方法来绘制View的自定义内容,同时还会绘制背景、边框、子View等。 -
绘制顺序:绘制顺序通常遵循深度优先的原则,从根View开始,先绘制子View,再绘制父View的内容(如果父View有背景或需要在子View之上绘制其他内容的话)。
但是,需要注意的是,在绘制过程中,如果一个View被其他View遮挡,那么被遮挡的部分可能不会被绘制出来。 -
关键类:Canvas是Android绘图API的核心类,它封装了绘图所需的所有方法。
Paint对象则定义了绘图的样式,包括颜色、粗细、字体等。 -
绘制步骤:
- 绘制背景 (
drawBackground()
):首先绘制View的背景。 - 保存Canvas图层(可选):为后续的绘制操作保存Canvas的状态,以便进行图层合成或淡出效果。
- 绘制View的内容 (
onDraw()
):在View的onDraw()
方法中绘制具体内容。 - 绘制子View (
dispatchDraw()
):ViewGroup通过此方法绘制其子View。 - 绘制淡出边缘并恢复Canvas图层(可选):如果之前保存了Canvas图层,则在此步骤中恢复,并可能绘制淡出效果。
- 绘制装饰 (
onDrawForeground()
):最后,绘制View的前景色、滚动条等装饰元素。
- 绘制背景 (
拓展
-
invalidate() 和 postInvalidate():
- invalidate():在UI线程中调用,用于请求重绘View。调用此方法后,系统会标记View为“需要重绘”,并在下一个绘制周期中重绘该View。
- postInvalidate():在非UI线程中调用,用于请求重绘View。它内部会调用
post(new Runnable() {...})
来确保重绘操作在UI线程中执行。
-
requestLayout():
- 当View的布局参数发生变化(如大小、位置、padding、margin等),或者需要重新计算子View的布局时,应该调用此方法。
调用requestLayout()
后,系统会标记View的布局状态为“无效”,并触发一个完整的测量和布局过程,
从根View开始重新测量和布局所有受影响的子View。这个过程最终会调用到每个View的draw()
方法来重绘View。
- 当View的布局参数发生变化(如大小、位置、padding、margin等),或者需要重新计算子View的布局时,应该调用此方法。