Android View的绘制流程及事件分发机制

70 阅读5分钟

一、view的结构层次

image.png View的绘制是由ViewRoot来负责的,每个应用程序窗口的decorView都有一个与之关联的ViewRoot对象,而这种关联关系是由WindowManager来维护的。

二、View创建到呈现的三个阶段

在Android中,一个View从被创建到最终显示在屏幕上,必须经历三个核心阶段:

  1. Measure (测量) : 确定View的宽高(MeasuredWidth 和 MeasuredHeight)。
  2. Layout (布局) : 确定View在父容器中的具体位置(四个顶点坐标:LeftTopRightBottom)。
  3. Draw (绘制) : 将View的内容渲染到Canvas上。

这三个过程遵循  “自上而下”  的递归原则。顶层ViewGroup(如DecorView)发起调用,逐层传递,最终完成整棵视图树的渲染。

第一阶段测量Measure

测量的目的是为了确定一个View的尺寸。在Android中,尺寸的确定并不是单纯由子View决定,而是由父View的约束子View自身的需求共同决定的。

MeasureSpec的三种模式

模式含义对应场景(LayoutParams)
EXACTLY精确大小match_parent 或 具体数值(如100dp)。父容器已经确定了子View的精确大小。
AT_MOST最大值限制wrap_content。父容器给出一个最大限制(通常是父容器剩余空间),子View不能超过这个尺寸,具体多大由子View自身内容决定。
UNSPECIFIED未限制通常出现在ScrollView或系统内部。父容器对子View没有限制,子View想要多大就给多大。

onMeasure的核心实现

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 1. 解析父容器传入的约束
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
    // 2. 计算最终尺寸 (根据自身内容逻辑)
    int finalWidth;
    int finalHeight;
    
    // 计算宽度
    if (widthMode == MeasureSpec.EXACTLY) {
        // 如果父容器给了精确值,直接使用
        finalWidth = widthSize;
    } else {
        // 计算内容需要的宽度(例如根据文字、图片计算)
        int contentWidth = getContentWidth(); 
        
        if (widthMode == MeasureSpec.AT_MOST) {
            // wrap_content: 取 min(内容宽度, 父容器限制)
            finalWidth = Math.min(contentWidth, widthSize);
        } else { // UNSPECIFIED
            finalWidth = contentWidth;
        }
    }
    
    // 同理处理高度...
    
    // 3. 保存测量结果
    setMeasuredDimension(finalWidth, finalHeight);
}

注意事项

千万不要在 `onMeasure` 中直接使用 `getWidth()` / `getHeight()`。
此时View的尺寸尚未确定,这两个方法返回的是上一次layout结束后的值,通常为0。只有在 `onLayout` 结束之后,尺寸才是正确的。

第二阶段layout

Layout阶段决定了View具体放在哪里。对于ViewGroup来说,它需要遍历所有子View,调用子View的 layout 方法,并计算子View的具体坐标。

layout与onLayout

  • layout(l, t, r, b) : 这是一个非私有方法(且是final的),由父View调用。它负责设置View自身的四个顶点,并内部调用 onLayout
  • onLayout(boolean changed, int l, int t, int r, int b) : 这是一个空方法,供ViewGroup重写。用于确定子View的位置。

自定义ViewGroup的Layout实现(FlowLayout)

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int childCount = getChildCount();
    int currentX = getPaddingLeft();
    int currentY = getPaddingTop();
    int lineMaxHeight = 0;
    
    for (int i = 0; i < childCount; i++) {
        View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            // 获取子View测量后的宽高
            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();
            
            // 判断是否需要换行
            if (currentX + childWidth > r - l - getPaddingRight()) {
                // 换行
                currentX = getPaddingLeft();
                currentY += lineMaxHeight;
                lineMaxHeight = 0;
            }
            
            // 关键:调用子View的layout方法,设置其坐标
            child.layout(currentX, currentY, 
                         currentX + childWidth, 
                         currentY + childHeight);
            
            // 更新坐标
            currentX += childWidth;
            lineMaxHeight = Math.max(lineMaxHeight, childHeight);
        }
    }
}

第三个阶段绘制(Draw)

绘制阶段负责将View的内容显示在屏幕上。这个过程也是递归的,父View会调用子View的 draw 方法。

draw的执行步骤:View的draw方法内部逻辑非常清晰,按照以下顺序执行(源码分析):

  1. 绘制背景drawBackground(canvas)
  2. 保存画布图层: 为动画或渐变做准备(非必须)
  3. 绘制自身内容onDraw(canvas) -> 子类重写
  4. 绘制子ViewdispatchDraw(canvas) -> ViewGroup重写
  5. 绘制滚动条/前景onDrawForeground(canvas)

性能优化建议

1、避免在 `onDraw` 中创建对象:`onDraw` 会被频繁调用,在其中`new`对象会导致内存抖动,引发GC,造成掉帧。
2、尽量减少 `onDraw` 的复杂度:复杂的计算逻辑应当放在初始化或测量阶段。
3、使用 `clipRect`:在自定义ViewGroup中,如果某个子View被遮挡,
可以在 `dispatchDraw` 前使用 `canvas.clipRect` 来避免绘制不可见的部分,提高绘制效率。

触发重绘的方式

  1. requestLayout() :

    • 作用:触发 measure 和 layout
    • 场景:View的尺寸发生变化(例如数据更新导致高度变化)。
    • 注意:它不会强制触发 draw。如果只是尺寸变化,内容不变,系统会跳过draw阶段,或者仅重绘边界变化的区域。
  2. invalidate() :

    • 作用:触发 draw
    • 场景:View的外观发生变化(颜色、文字内容改变),但宽高不变。
    • 注意:它不会触发 measure 或 layout,效率较高。
  3. postInvalidate() :

    • 作用:在非UI线程中请求重绘(最终依然会切回UI线程)。

ps:性能陷阱

`requestLayout` 的代价是非常高的,因为它会从当前View向上回溯到`ViewRootImpl`,
然后再向下递归执行完整的`measure`和`layout`流程。

优化原则 :如果只是改变setVisibility或者简单的位移(没有改变尺寸),尽量使用 invalidate 或 setTranslationX/Y 等属性动画,避免触发 requestLayout

三、事件分发结构模型

  1. 触摸事件由Action_Down、Action_Move、Aciton_UP组成,其中一次完整的触摸事件中,Down和Up都只有一个,Move有若干个,可以为0个。

  2. 参与Touch事件分发的,有Activity、ViewGroup(继承于view)和View。

    • ViewGroup包含:onInterceptTouchEventdispatchTouchEventonTouchEvent
    • View和Activity包含:dispatchTouchEventonTouchEvent
  3. ViewGroup和View组成了一个树状结构,根节点为Activity内部包含的一个ViewGroup。

  4. ViewGroup的dispatchTouchEvent是真正在执行“分发”工作,而View的dispatchTouchEvent方法,并不执行分发工作,而是决定是否把touch事件交给自己处理(onTouchEvent)。

  5. onInterceptTouchEventdispatchTouchEventonTouchEvent这三个方法都是返回true表示消费了事件,返回false则表示不感兴趣,不处理。

后续事件

后续事件是指Action_Move和Action_Up,所有后续事件都会直接传给消费事件的View,不再经历中间的传播过程。

image.png

上图的事件分发顺序为①-②-⑤-⑥-⑦-③-④

事件分发详细流程图

详细事件流程图 (1).png