什么是 ViewTreeObserver

ViewTreeObserver 是 Android 中一个非常强大但又容易被忽视的机制,是 Android 中View的观察者接口(Observer),主要用于监听 View 树的变化,比如布局完成、绘制过程、全局焦点变化、滚动状态变化等等。

它常用于以下场景:

  • 获取 View 的尺寸(如宽高,因为 onCreate/onStart 中获取是 0)
  • 监听布局过程(如 onGlobalLayout)
  • 监听 View 绘制完成(如 onPreDraw)
  • 监听滚动变化、滑动变化(如 onScrollChanged)
  • 监听窗口附着/分离

原理简述

每个View都可以通过getViewTreeObserver()获取与之关联的观察器。实际上监听的是整个视图树(View Tree)的状态变更,本质是“发布-订阅”模式的实现。

val observer = myView.viewTreeObserver
observer.addOnGlobalLayoutListener { 
    // 在布局完成后执行此处代码
}

核心监听器接口及用途

接口名称功能说明典型用途
OnGlobalLayoutListener视图树全局布局发生改变(添加/删除 View)时回调获取 view 宽高、在布局完成后执行逻辑
OnPreDrawListenerView 即将绘制时调用(但尚未绘制)修改 View 属性、控制动画流程等
OnScrollChangedListener滚动状态改变时调用(如 ScrollView 滚动)联动动画、懒加载
OnGlobalFocusChangeListener焦点发生改变时焦点监测
OnWindowAttachListenerView 添加或移除窗口时监听视图是否挂载在窗口上
OnWindowFocusChangeListener窗口焦点变化判断窗口是否活跃

典型使用场景

获取真实 Width 和 Height

获取 View 的宽高(getWidth()、getHeight())或位置(getLocationOnScreen())。动态调整 UI 布局(例如根据子 View 的大小调整父布局)。

View view = findViewById(R.id.my_view);
ViewTreeObserver vto = view.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        // 获取 View 的宽高
        int width = view.getWidth();
        int height = view.getHeight();
        Log.d("ViewTreeObserver", "Width: " + width + ", Height: " + height);
 
        // 移除监听器,避免重复回调
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
        } else {
            view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
        }
    }
});

动态调整 View 大小

例如,在一个聊天应用中,当用户输入消息时,可以使用 OnPreDrawListener 监听输入框的变化。当输入框即将被绘制前,可以检查输入内容,并动态调整输入框的大小,保证输入内容始终可见。

inputLayout.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
    @Override
    public boolean onPreDraw() {
        inputLayout.getViewTreeObserver().removeOnPreDrawListener(this);
        // 调整输入框大小的代码逻辑
        return true;
    }
});

多监听器协作模式

ViewTreeObserver允许你在同一个视图上注册多个监听器,这对于需要从不同的角度响应视图变化的复杂场景尤为有用。例如,在一个复杂的表单布局中,你可能需要同时监听滚动事件、键盘显示事件以及布局变化事件。通过合理的监听器组合和协作,可以更高效地管理这些事件。

binding.myScrollView.getViewTreeObserver().addOnScrollChangedListener {
    // 处理滚动事件逻辑
}
 
binding.myEditText.getViewTreeObserver().addOnGlobalLayoutListener {
    // 处理键盘显示/隐藏事件逻辑
}
 
binding.root.getViewTreeObserver().addOnWindowFocusChangeListener { hasFocus ->
    if (hasFocus) {
        // 当窗口获得焦点时,执行相关逻辑
    }
}

生命周期管理

  • 有效性检查isAlive()方法检查 ViewTreeObserver 是否有效(即是否仍绑定到有效的 View 树)。
  • 失效场景:当 View 树销毁(如 Activity 销毁、View 从父容器移除),ViewTreeObserver 失效,需重新通过getViewTreeObserver()获取新的实例。

注意事项

ViewTreeObserver 虽然功能强大,但它有以下潜在问题:

  • 复杂性:需要手动添加和移除监听器,代码稍显冗长,尤其是要处理生命周期和版本兼容性。
  • 性能开销:监听器可能被多次触发,需要额外逻辑避免重复执行。
  • 场景限制:某些简单任务(如获取 View 尺寸或执行一次性操作)使用 ViewTreeObserver 可能显得“过于重量级”。

所以在使用的时候有一些需要注意的点:

  1. 移除监听器
    • 许多监听器(如OnGlobalLayoutListenerOnPreDrawListener)可能被多次调用,需在适当时候移除以避免重复执行或性能问题。
    • 使用removeOnGlobalLayoutListenerremoveOnPreDrawListener(API 16 以上)。
  2. 检查 ViewTreeObserver 有效性
    • 在操作前调用isAlive()检查 ViewTreeObserver 是否有效,避免操作已销毁的 View 树。
  3. 避免内存泄漏
    • 如果监听器持有了 Activity 或 Fragment 的引用,需在适当的生命周期(如onDestroy())移除监听器。
  4. 版本兼容性
    • 使用 Build.VERSION.SDK_INT 检查 API 版本,确保监听器移除方法的兼容性。
  5. 替代方案
    • 对于简单场景,可考虑使用 View.post(Runnable) 或 ViewPropertyAnimator 替代 ViewTreeObserver。
    • 对于复杂布局测量,结合 ConstraintLayout 或 View.measure() 可能更高效。

onGlobalLayout 回调时机

原理机制分析

内部结构

ViewTreeObserver 是一个非静态内部类,定义在 View 类中。它的主要职责是管理一组监听器,并将 View 树的事件分发给这些监听器。以下是其核心组成部分(基于 Android 源代码分析):

  • 监听器列表
    • ViewTreeObserver 内部维护了多个监听器集合,例如ArrayList<OnGlobalLayoutListener>CopyOnWriteArrayList<OnPreDrawListener>等。
    • 使用 CopyOnWriteArrayList 确保在遍历监听器时可以安全地添加或移除监听器,适合高并发场景(如绘制过程中)。
  • View 树引用
    • ViewTreeObserver 持有一个对 View 树的引用(通常是根 View 或 ViewRootImpl)。
    • 通过isAlive()方法检查 View 树是否有效(即 View 是否仍附着在窗口上)。
  • 事件分发机制
    • ViewTreeObserver 的事件分发由 Android 的 View 绘制流程(measure、layout、draw)驱动。
    • 事件触发时,ViewTreeObserver 遍历对应的监听器列表,调用其回调方法。

与 View 系统的关系

ViewTreeObserver 深嵌于 Android 的 View 系统,依赖于 View 树的绘制流程。

  • ViewRootImpl:ViewTreeObserver 的事件分发由 ViewRootImpl(View 树的控制器)协调。ViewRootImpl 负责管理整个 View 树的测量、布局和绘制过程,并在特定阶段触发 ViewTreeObserver 的回调。
  • Choreographer:绘制相关事件(如OnPreDrawListener)与 Android 的渲染机制相关,依赖 Choreographer 的帧同步机制。
  • 消息队列:ViewTreeObserver 的事件触发通常发生在 UI 线程的消息循环中,与 View 系统的绘制流程同步。

流程分析

OnGlobalLayoutListener

  • 触发时机:View 树完成布局(layout)阶段后,调用ViewRootImpl.performLayout()时触发。
  • 原理
    • 在 View 树的 layout 阶段,ViewRootImpl 会遍历 View 树,设置每个 View 的位置和大小。
    • 布局完成后,ViewRootImpl 调用ViewTreeObserver.dispatchOnGlobalLayout(),遍历所有 OnGlobalLayoutListener 并执行回调。
    • 如果 View 树的布局发生变化(如 View 大小调整、添加/移除子 View),会再次触发。
  • 源码分析(简化的 ViewRootImpl.performLayout):
private void performLayout() {
    // 执行布局
    mView.layout(...);
    // 分发全局布局事件
    if (mViewTreeObserver != null) {
        mViewTreeObserver.dispatchOnGlobalLayout();
    }
}
  • **用途:**获取 View 的最终尺寸(如 getWidth()、getHeight())或位置。

deepseek:

  1. 视图树变化:
    • 当某个View调用requestLayout()时,会触发从根视图开始的重新布局(performMeasureperformLayout)。
  2. 通知观察者:
    • ViewGroup.layout()完成后,ViewRootImpl会调用ViewTreeObserver.dispatchOnGlobalLayout()
  3. 回调监听器:
    • ViewTreeObserver遍历所有注册的OnGlobalLayoutListener,依次执行onGlobalLayout()
// 伪代码:ViewRootImpl 的布局流程
private void performTraversals() {
    // 1. 测量
    performMeasure();
    // 2. 布局
    performLayout();
    // 3. 触发全局布局监听
    mView.getViewTreeObserver().dispatchOnGlobalLayout();
    // 4. 绘制
    performDraw();
}
1752463358006-cb21835a-741e-4e79-8bb9-f54846dde14a.svg #### OnPreDrawListener * **触发时机**:View 树开始绘制(draw)阶段前,ViewRootImpl.performDraw() 调用 ViewTreeObserver.dispatchOnPreDraw() 时触发。 * **原理**: * 在绘制阶段,ViewRootImpl 会在调用 Canvas 绘制前通知 ViewTreeObserver。 * dispatchOnPreDraw() 遍历所有 OnPreDrawListener,并根据回调的返回值(true 表示继续绘制,false 取消当前绘制)决定是否继续渲染。 * **源码分析**(简化的 ViewRootImpl.performDraw): ```kotlin private void performDraw() { if (mViewTreeObserver != null && mViewTreeObserver.dispatchOnPreDraw()) { // 继续绘制 drawFull(); } } ``` \*\*用途:\*\*在绘制前修改 View 属性或执行动画。 #### OnDrawListener View 的**draw()****方法执行后**、**绘制完成后立即触发**,也就是说,**Canvas 绘制阶段已结束**,此时你已经可以访问 View 的实际显示状态。

每个 View 在执行完draw()方法(含 drawBackground、onDraw、drawChildren)后,会触发ViewTreeObserver.dispatchOnDraw()

Info

在 60Hz 屏幕刷新率下,OnDrawListener** 论上每秒会调用 60 次**,前提是该 View(或其任何子 View)每一帧都进入了 draw() 流程。

  • OnDrawListener 不常用于 UI 改动(动画、属性设置),因为它发生在绘制结束之后。
  • 适合用来记录 “当前 View 的真实展示状态”,如 alpha/log/调试绘制流程

实际流程图梳理:

measure()    --> OnGlobalLayoutListener(布局完成)

layout()     --> OnGlobalLayoutListener(布局完成)

------> dispatchOnPreDraw() → OnPreDrawListener ✅

draw(Canvas)
  ├─ onDraw()               --> 你自定义 View 绘制逻辑
  ├─ dispatchDraw()         --> 绘制子 View
  └─ onDrawForeground()     --> 绘制前景等

------> dispatchOnDraw() → OnDrawListener ✅

参考

获取控件宽高的时序问题

Android ViewTreeObserver使用详解与示例-CSDN博客

一个例子(ScrollView 滑动过度动画)

package com.example.viewtreeobserve_bestpractices
 
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.view.Gravity
import android.view.ViewTreeObserver
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import kotlin.math.max
import kotlin.math.min
 
/**
 * 通过 ScrollView + ViewTreeObserver.OnScrollChangedListener + OnPreDrawListener
 * 来挑战一个更贴近实际场景的复杂 UI 效果。你可以从这个练习中学到:
 *
 * 如何监听 View 滚动位置并做联动动画
 
 * 如何在绘制前修改 View 的属性
 *
 * 如何结合多种监听器,实现流畅的视觉效果
 */
 
class ScrollDemoActivity : AppCompatActivity() {
 
    private lateinit var scrollView: ScrollView
    private lateinit var container: LinearLayout
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(R.layout.activity_scroll_demo)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }
 
        scrollView = findViewById(R.id.scrollView)
        container = findViewById(R.id.container)
 
        // 添加 30 个带卡片样式的 TextView
        for (i in 1..30) {
            val card = TextView(this).apply {
                text = "Card Item $i"
                textSize = 20f
                setBackgroundColor(Color.parseColor("#DDDDDD"))
                setTextColor(Color.BLACK)
                gravity = Gravity.CENTER
                alpha = 0f // 初始化透明
                setPadding(50, 100, 50, 100)
                layoutParams = LinearLayout.LayoutParams(
                    LinearLayout.LayoutParams.MATCH_PARENT,
                    LinearLayout.LayoutParams.WRAP_CONTENT
                ).apply {
                    bottomMargin = 40
                }
            }
            container.addView(card)
        }
 
        // 使用 OnPreDrawListener 实现淡入动画
        container.viewTreeObserver.addOnPreDrawListener(object :
            ViewTreeObserver.OnPreDrawListener {
            override fun onPreDraw(): Boolean {
                container.viewTreeObserver.removeOnPreDrawListener(this)
                for (i in 0 until container.childCount) {
                    val child = container.getChildAt(i)
 
                    val screenHeight = scrollView.height
                    val childTop = child.top
                    val childAlpha = max(0.1f, childTop / screenHeight.toFloat())
 
                    child.animate()
                        .alpha(childAlpha)
                        .setDuration(800)
                        // 逐个展开动画
                        .setStartDelay(i * 80L)
                        .start()
                }
                return true
            }
        })
 
        container.viewTreeObserver.addOnDrawListener {
            val item1 = container.getChildAt(0)
            Log.d("alphaTest", "pre-draw 时 alpha = ${item1.alpha}")
        }
 
        // 使用 OnScrollChangedListener 实现滚动联动缩放 & 透明动画
        scrollView.viewTreeObserver.addOnScrollChangedListener {
            applyScrollEffect()
        }
    }
 
    private fun applyScrollEffect() {
        val scrollY = scrollView.scrollY
        val screenHeight = scrollView.height
 
        for (i in 0 until container.childCount) {
            val child = container.getChildAt(i)
 
            /**
             * child.top 是子 View 相对于父布局顶部的距离
             * scrollY 是 ScrollView 当前滚动的垂直距离
             *
             * 当child.top - scrollY > 0:表示子View在可视区域下方
             * 当child.top - scrollY < 0:表示子View在可视区域上方
             */
            val childTopInScroll = child.top - scrollY
            val ratio = childTopInScroll.toFloat() / screenHeight
 
            val scale = max(0.85f, min(1f, ratio))
            val alpha = max(0.1f, min(1f, ratio))
 
            child.scaleX = scale
            child.scaleY = scale
            child.alpha = alpha
        }
    }
 
}

更新: 2025-07-15 17:42:00
原文: https://www.yuque.com/dongpozhouzi-mshe3/zhm85g/nmggnrrmvb6d8pmb


相关笔记