动态设置 View 宽度不生效?聊聊View的测量和布局的大坑

278 阅读3分钟

前言

我们常说 View “绘制不对动”,但真正的问题常常发生在 测量 (onMeasure)布局 (onLayout) 阶段。我通过项目中实际遇到的几个编译后正常,运行时乱七八糟的坑,来分析下 View 为什么会 width = 0,为什么动态设置宽度不生效,为什么模算逻辑反复触发导致布局异常。


IMG_20250625_200934.png

一、背景说明

项目需要动态地修改自定义 View 的宽度,并应用一份 XML 中的 UI 布局。

这看似是一件极为简单的事,但实际上却不断跟得 ConstraintLayout 的规则走,这里是一份布局结构:

<!-- 外层 ConstraintLayout -->
<androidx.constraintlayout.widget.ConstraintLayout
    android:id="@+id/outerLayout"
    android:layout_width="170dp"
    android:layout_height="wrap_content">

    <!-- 内层 ConstraintLayout -->
    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/innerLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

然后在 Kotlin 中动态设置宽度:

outerLayout.updateLayoutParams {
    width = (getWidthProportion() * dip(170)).toInt()
}
outerLayout.requestLayout()

二、ConstraintLayout 不加约束 = 宽度为 0

首先要明确:ConstraintLayout 是一个基于约束编解器 (Constraint Solver) 的布局系统,不依赖传统的 match_parent 或 wrap_content 来确定宽度

当你在 ConstraintLayout 里写上:

android:layout_width="match_parent"

如果 未配合 app:layout_constraintStart_toStartOf app:layout_constraintEnd_toEndOf,则 match_parent 就不能表示“支撑全容器宽度”,而是被规范为 0 或空布局。

正确写法:

android:layout_width="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"

用 0dp (即 match_constraints) + 双向约束,才是 ConstraintLayout 中表示“支撑全容器”的正确方式。


三、wrap_content / dp 固定值看似安全,实际不能动态修改

你可能记得:

outerLayout.updateLayoutParams {
    width = 300
}

却发现视觉上一点变化都没有。

原因:

  • wrap_content 以内容为主,自动表现
  • 170dp 是确定值,系统模算后记录并维持,如果你后期再改 layoutParams,没有侦測到应该重新 measure,那么是不会生效的。

解决方案:

  • post {} 中调用 updateLayoutParams
tv.post {
    tv.updateLayoutParams {
        width = 300
    }
    tv.requestLayout()
}
  • 或者在自定义 View 中重写 onMeasure() ,强制 setMeasuredDimension()

四、频繁布局刷新 = measure 重复触发,最终宽度为 0

因为项目里有重复的 post + updateLayoutParams ,实际上会造成 measure/layout 一直循环触发,而 ConstraintLayout 如果未能解答子结构应该怎样 layout,最终给出的是 width = 0 。

1750852512236_4344E18F-AD63-44dc-AF3E-9CF71AD0495D.png

解决方案:重写 onMeasure

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    val desiredWidth = (getWidthProportion() * dip(170)).toInt()
    val newWidthSpec = MeasureSpec.makeMeasureSpec(desiredWidth, MeasureSpec.EXACTLY)
    super.onMeasure(newWidthSpec, heightMeasureSpec)
}

相当于告诉系统:我就是要这个宽度,你别算了。


五、总结

1. 尽量避免使用 ConstraintLayout 处理简单布局

为了项目的后续的拓展性,能简单布局的地方,尽量简单,我发现在很多业务代码中,明明是几行占满的textView,用线性布局可以很简单的实现,但是写的人非要复杂的套上约束布局,写上一堆topToTop,为后续的很多功能都会埋下很多的坑

2. 布局中的约束布局一定要注意约束

不是视觉上暂时看起来没问题就是真的没问题了,没有加上约束,相当于一次性的绘制,一旦涉及到子布局或者整体布局的改变,没有走到测绘的那步,布局就会出现问题

3. 避免重复密集的刷新布局

在项目中有很多频繁、密集且没有必要的刷新,属于项目的历史屎山,历经多位研发的堆叠,频繁的刷新的view,改变view的宽高、设置子ImageView的图片、设置textView的文案,甚至很多地方没有加上post,导致view在绘制的计算时频繁的触发到新的绘制,出现了问题,很容易出现宽高为0的情况

公众号原文地址:mp.weixin.qq.com/s/6X7gazuKd…

更多文章可以看历史记录:blog.itpub.net/69917874/