Bootstrap

View绘制流程

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,因为此时视图可能还没有完成测量和布局。

  1. onWindowFocusChanged(boolean hasFocus) 方法中获取:当 Activity 的窗口获得焦点时,视图已经完成布局,此时可以安全地获取宽高。

  2. 使用 ViewTreeObserverOnGlobalLayoutListener:通用方法,它允许您在视图树的全局布局状态改变时(即视图完成布局后)收到回调。您可以在视图的 getViewTreeObserver() 上注册这个监听器,并在回调中获取宽高。

  3. 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对象则定义了绘图的样式,包括颜色、粗细、字体等。

  • 绘制步骤

    1. 绘制背景 (drawBackground()):首先绘制View的背景。
    2. 保存Canvas图层(可选):为后续的绘制操作保存Canvas的状态,以便进行图层合成或淡出效果。
    3. 绘制View的内容 (onDraw()):在View的onDraw()方法中绘制具体内容。
    4. 绘制子View (dispatchDraw()):ViewGroup通过此方法绘制其子View。
    5. 绘制淡出边缘并恢复Canvas图层(可选):如果之前保存了Canvas图层,则在此步骤中恢复,并可能绘制淡出效果。
    6. 绘制装饰 (onDrawForeground()):最后,绘制View的前景色、滚动条等装饰元素。

拓展

  1. invalidate() 和 postInvalidate()

    • invalidate():在UI线程中调用,用于请求重绘View。调用此方法后,系统会标记View为“需要重绘”,并在下一个绘制周期中重绘该View。
    • postInvalidate():在非UI线程中调用,用于请求重绘View。它内部会调用post(new Runnable() {...})来确保重绘操作在UI线程中执行。
  2. requestLayout()

    • 当View的布局参数发生变化(如大小、位置、padding、margin等),或者需要重新计算子View的布局时,应该调用此方法。
      调用requestLayout()后,系统会标记View的布局状态为“无效”,并触发一个完整的测量和布局过程,
      从根View开始重新测量和布局所有受影响的子View。这个过程最终会调用到每个View的draw()方法来重绘View。
;