view.requestLayout()

view.requestLayout() 是 Android 视图系统中的一个关键方法,它的作用是标记当前视图(View)需要重新测量(measure)和布局(layout),但不会立即执行,而是等待系统下一次界面刷新时统一处理。具体来说View.requestLayout()做了以下几件事:

  1. 通知 View 系统:这个 View 需要重新测量和布局(measure 和 layout)
  2. 触发从该 View 开始,向上逐层传播请求,最终可能影响整个 View 树。
  3. 下一帧时,View会在onMeasure()onLayout()阶段重新处理这个 View。

具体作用

  1. 触发重新布局:当你调用view.requestLayout()时,View 会向其父容器请求重新执行测量(measure)、布局(layout)过程,以确保 View 及其子 View 的位置和大小正确更新。
  2. 适用于动态变化:如果 View 的属性(如宽度、高度、位置、内容等)发生变化,调用此方法可以确保布局反映这些变化。例如,动态调整 View 的大小、添加或移除子 View、更改文本内容等场景。
  3. 异步操作:调用requestLayout()不会立即触发重新布局,而是将请求加入到系统的布局队列中,等待下一次布局周期(通常在下一帧)执行。
  4. **影响范围:**不仅影响调用该方法的 View,还可能影响其父容器和整个 View 树(取决于布局依赖关系)。

使用场景

你需要在以下情况调用view.requestLayout()

  1. View 尺寸变化:例如,修改了view.layoutParams.widthview.layoutParams.height
  2. View 位置变化:调整了 View 的位置(如 setTranslationX/Y 可能需要配合重新布局)。
  3. 子 View 变化:添加、移除或修改了子 View,导致父容器需要重新布局。
  4. 内容变化影响布局:例如,TextView 文本变化导致尺寸变化。

注意事项

  1. 性能影响:频繁调用 requestLayout() 可能导致性能问题,因为它会触发整个 View 树的重新测量和布局(比如它有一个父布局,你修改了高度,而父布局的高度恰好是wrapper_content),应尽量避免不必要的调用。
  2. 不一定立即生效:布局过程是异步的,实际的重绘可能在下一帧发生。
  3. 仅影响布局:requestLayout() 只处理 View 的测量和布局,不会直接触发重绘(invalidate() 负责重绘)。

示例

// 假设有一个 TextView
TextView textView = findViewById(R.id.text_view);
textView.setText("新内容"); // 改变文本内容
textView.requestLayout(); // 请求重新布局以适应新内容

总结来说,view.requestLayout()是用来通知系统重新计算 View 布局的方法,适用于 View 尺寸或内容变化的场景,但需谨慎使用以避免性能问题。

实际案例

private fun animateHeight(view: View, from: Int, to: Int) {
    val animator = ValueAnimator.ofInt(from, to)
    animator.duration = 300
    animator.interpolator = DecelerateInterpolator()
    animator.addUpdateListener {
        val value = animator.animatedValue as Int
        view.layoutParams.height = value
        view.requestLayout()
    }
    animator.start()
}

在动画过程中,ValueAnimator会不断变化value,这个value被用来更新view.layoutParams.height,也就是动态改变view的高度。

但是,仅仅修改layoutParams.height是不够的,因为系统不会自动知道你修改了布局参数。为了让系统知道这个 View 的尺寸变了,就必须调用:

view.requestLayout()

如果不调用requestLayout(),那么可能会发生以下情形:

  • 系统不会重新测量这个 View。
  • View 的显示区域可能不会马上更新。
  • 你看到的界面不会立即反映高度变化。
  • 有时甚至可能根本没有任何效果(视觉上)。

一些疑问

 v.updateLayoutParams<ViewGroup.LayoutParams> {
    height = newHeight.toInt()
}

Q:为什么上述代码块这种可以马上得到布局的更新?

updateLayoutParams { ... }是 Kotlin Android KTX 提供的一个扩展函数,其定义大致如下:

inline fun <reified T : ViewGroup.LayoutParams> View.updateLayoutParams(block: T.() -> Unit) {
    val params = layoutParams as? T
    if (params != null) {
        block(params)
        layoutParams = params // ⚠️ 这一行是关键
    }
}

当你重新赋值给layoutParams时,系统会自动调用requestLayout()

public void setLayoutParams(ViewGroup.LayoutParams params) {
    if (params == null) {
        throw new NullPointerException("Layout parameters cannot be null");
    }
    mLayoutParams = params;
    requestLayout(); // 系统在内部调用了requestLayout()
}

也就是说,只要你:

v.layoutParams = newParams // 在 Kotlin 里相当于调用了 v.setLayoutParams
// 根据上方代码块可知

或通过updateLayoutParams {}间接完成这个操作,系统会帮你调用requestLayout()

这里需要注意一点的是,下方代码片段 A 和代码片段 B 是完全不同的两码事。

val newParams = view.layoutParams
newParams.height = 500
 // ⚠️ 这一步相当于“告诉系统参数有变化”,内部触发 requestLayout
view.layoutParams = newParams
view.layoutParams.height = value
view.requestLayout()

代码 B 不会触发requestLayout()**。**因为你只是直接修改了 LayoutParams 的字段,系统没有任何通知机制来知道参数已经“变更”。所以视图不会立即重布局,除非你手动 requestLayout()

Android 不会自动监听你对LayoutParams字段的修改,只有当你显示调用requestLayout()或者通过setLayoutParams()才会触发重布局流程。

invalidate()

view.invalidate()是 Android View 类的一个方法,用于请求 View 系统重新绘制(重绘)View 及其子 View。它通知系统,View 的视觉内容需要更新,通常在 View 的外观(如颜色、文本、背景等)发生变化时调用。

invalidate() 是 Android View 系统中一个非常核心的方法,它和 requestLayout() 是一对经常容易混淆的“好兄弟”。

具体作用

  • 触发重绘:调用 view.invalidate() 会使 View 的 onDraw() 方法被调用,重新绘制 View 的内容到屏幕上。
  • 异步操作:类似于 requestLayout(),invalidate() 不会立即触发重绘,而是将请求加入绘制队列,等待下一次屏幕刷新(通常每 16 毫秒一帧)。
  • 影响范围:默认情况下,invalidate() 只重绘调用它的 View 及其子 View,不会影响布局(位置或大小)。

它不会引起重新测量(measure)和布局(layout),只会触发重新绘制(draw)阶段。

使用场景

你需要在以下情况调用 view.invalidate():

  1. 外观变化:View 的绘制内容发生变化,例如:
    • 更改了 View 的背景颜色、文字内容、文字颜色等。
    • 自定义 View 中更新了绘制逻辑(如画布上的图形变化)。
  2. 动画效果:需要动态更新 View 的视觉效果,例如透明度、旋转、或自定义绘制的动画。
  3. 内容刷新:TextView 的文本内容变化但不影响尺寸时,可能仅需重绘而非重新布局。

示例代码

val view = findViewById<View>(R.id.my_view)
view.setBackgroundColor(Color.RED) // 更改背景颜色
view.invalidate() // 请求重绘以显示新颜色

实际案例

/**
 * 只改颜色调用
 * 
 * @param newColor
 */
fun changeColorOnly(newColor: Int) {
    color = newColor
    // ❗ 这里只需要 invalidate,不能只 requestLayout
    invalidate()
}

一些疑问

Q:为什么有的时候用requestLayout()也会引起重绘?好像不需要invalidate()

比如下段代码:

class MyView(context: Context) : View(context) {
 
    /** 颜色 */
    private var color = Color.RED
    /** 半径 */
    private var circleRadius = 100f
 
    /**
     * 改变尺寸和颜色
     *
     * @param newRadius
     * @param newColor
     */
    fun changeSizeAndColor(newRadius: Float, newColor: Int) {
        circleRadius = newRadius
        color = newColor
        layoutParams.width = (newRadius * 2).toInt()
        layoutParams.height = (newRadius * 2).toInt()
 
        // ❗只调用 requestLayout,不调用 invalidate
        requestLayout()
    }
 
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        Log.d(MyView::class.java.simpleName, "onDraw called") // ⬅️ 看日志判断是否执行了 draw
        val paint = Paint().apply { color = this@MyView.color }
        // 画一个圆
        canvas.drawCircle(width / 2f, height / 2f, circleRadius, paint)
    }
}

requestLayout()并不会直接调用draw(),但在布局(layout)发生改变时,系统会在下一帧自动安排一次完整的measure → layout → draw流程。因此你看到它“重绘”了,但那不是requestLayout()自己干的

流程:

  • 虽然我们只调用了 requestLayout(),**并没有手动调用 **invalidate()
  • 但你会发现 onDraw() 依然被调用了(从日志中能看到)
  • 原因是:View 的尺寸变了,系统下一个 VSync 帧发现有 layout 变化,于是调度了 measure → layout → draw 全流程
  • 所以这次 draw() 也被触发了

背后机制(重要):

当你调用requestLayout()

  1. 系统标记这个 View 为layout requested
  2. ViewRootImpl 会在下一个帧(VSync)执行performTraversals()
    • 如果检测到 layout 变化,它就会走measure → layout → draw
    • 如果 layout 区域发生变化,draw 就必然会执行(否则屏幕显示不对)
/**
 * 只改颜色调用 requestLayout
 *
 * 因为没有 layout 变化,所以不会调用 invalidate
 * @param newColor
 */
fun changeColorOnly(newColor: Int) {
    color = newColor
    // ❗ 这里只需要 invalidate,不能只 requestLayout
    requestLayout()
}

比如上述这种没有尺寸变化的情况,调用requestLayout()就不能起作用了。

回顾 View 的渲染流程

理解invalidate()必须先理解 Android View 的三个核心阶段:

measure()   -->   layout()   -->   draw()
  测量             布局            绘制
特性view.requestLayout()view.invalidate()
作用请求重新测量和布局,更新 View 的位置和大小请求重新绘制,更新 View 的视觉内容
触发过程触发onMeasure()onLayout()触发onDraw()
使用场景修改尺寸、位置、或布局参数(如layoutParams
修改绘制内容(如颜色、文字、自定义图形)
性能影响可能影响整个 View 树,重新计算布局,耗时较多仅重绘指定 View,性能开销通常较小

更新: 2025-08-18 21:08:57
原文: https://www.yuque.com/dongpozhouzi-mshe3/zhm85g/fmilexg8ld87rfe6


相关笔记