什么是 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 宽高、在布局完成后执行逻辑 |
OnPreDrawListener | View 即将绘制时调用(但尚未绘制) | 修改 View 属性、控制动画流程等 |
OnScrollChangedListener | 滚动状态改变时调用(如 ScrollView 滚动) | 联动动画、懒加载 |
OnGlobalFocusChangeListener | 焦点发生改变时 | 焦点监测 |
OnWindowAttachListener | View 添加或移除窗口时 | 监听视图是否挂载在窗口上 |
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 可能显得“过于重量级”。
所以在使用的时候有一些需要注意的点:
- 移除监听器:
- 许多监听器(如
OnGlobalLayoutListener、OnPreDrawListener)可能被多次调用,需在适当时候移除以避免重复执行或性能问题。 - 使用
removeOnGlobalLayoutListener或removeOnPreDrawListener(API 16 以上)。
- 许多监听器(如
- 检查 ViewTreeObserver 有效性:
- 在操作前调用
isAlive()检查 ViewTreeObserver 是否有效,避免操作已销毁的 View 树。
- 在操作前调用
- 避免内存泄漏:
- 如果监听器持有了 Activity 或 Fragment 的引用,需在适当的生命周期(如
onDestroy())移除监听器。
- 如果监听器持有了 Activity 或 Fragment 的引用,需在适当的生命周期(如
- 版本兼容性:
- 使用 Build.VERSION.SDK_INT 检查 API 版本,确保监听器移除方法的兼容性。
- 替代方案:
- 对于简单场景,可考虑使用 View.post(Runnable) 或 ViewPropertyAnimator 替代 ViewTreeObserver。
- 对于复杂布局测量,结合 ConstraintLayout 或 View.measure() 可能更高效。
onGlobalLayout 回调时机
原理机制分析
内部结构
ViewTreeObserver 是一个非静态内部类,定义在 View 类中。它的主要职责是管理一组监听器,并将 View 树的事件分发给这些监听器。以下是其核心组成部分(基于 Android 源代码分析):
- 监听器列表:
- ViewTreeObserver 内部维护了多个监听器集合,例如
ArrayList<OnGlobalLayoutListener>、CopyOnWriteArrayList<OnPreDrawListener>等。 - 使用 CopyOnWriteArrayList 确保在遍历监听器时可以安全地添加或移除监听器,适合高并发场景(如绘制过程中)。
- ViewTreeObserver 内部维护了多个监听器集合,例如
- 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:
- 视图树变化:
- 当某个
View调用requestLayout()时,会触发从根视图开始的重新布局(performMeasure→performLayout)。
- 当某个
- 通知观察者:
- 在
ViewGroup.layout()完成后,ViewRootImpl会调用ViewTreeObserver.dispatchOnGlobalLayout()。
- 在
- 回调监听器:
ViewTreeObserver遍历所有注册的OnGlobalLayoutListener,依次执行onGlobalLayout()。
// 伪代码:ViewRootImpl 的布局流程
private void performTraversals() {
// 1. 测量
performMeasure();
// 2. 布局
performLayout();
// 3. 触发全局布局监听
mView.getViewTreeObserver().dispatchOnGlobalLayout();
// 4. 绘制
performDraw();
}**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