You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在MotionLayout中为wrap_content属性的TextView实现平滑动画并让第二个TextView始终贴合其内容末端?

我之前刚好处理过类似的粘性头部需求,结合你遇到的问题——既要第一个TextView平滑收缩,又要第二个TextView始终紧贴其内容末端——给你几个实用的解决方案,应该能完美解决你的痛点:

方案一:MotionLayout监听过渡进度 + 动态修改约束

这个方案是最贴合你现有MotionLayout实现的,核心思路是不依赖scale缩放(因为缩放不改变布局宽度),而是通过监听动画进度,实时计算第一个TextView的收缩宽度,然后动态调整第二个TextView的约束位置。

具体步骤:

  1. 预先获取第一个TextView的原始宽度:在布局初始化完成后,获取它的measuredWidth(也就是内容完全展开时的宽度)。
  2. 在MotionScene中定义自定义动画属性:不用直接动画scale或textSize,而是定义一个从1f到0.6f的自定义缩放比例属性,比如app:customScale。
  3. 监听MotionLayout过渡变化:实现MotionLayout.TransitionListener,在onTransitionChange方法中根据当前进度计算实时的缩放比例,再算出收缩后的宽度,最后修改第二个TextView的约束,让它的start对齐第一个TextView的start加上收缩后的宽度。

代码示例(Kotlin):

// 布局初始化完成后获取第一个TextView的原始宽度
val originalFirstWidth = tvFirst.measuredWidth

// 设置MotionLayout过渡监听
motionLayout.setTransitionListener(object : MotionLayout.TransitionListener {
    override fun onTransitionStarted(motionLayout: MotionLayout?, startId: Int, endId: Int) {}

    override fun onTransitionChange(
        motionLayout: MotionLayout?,
        startId: Int,
        endId: Int,
        progress: Float
    ) {
        // 计算当前缩放比例:从1.0到0.6
        val currentScale = 1f - progress * 0.4f
        // 计算收缩后的宽度
        val scaledFirstWidth = (originalFirstWidth * currentScale).toInt()

        // 动态修改第二个TextView的约束
        val constraintSet = ConstraintSet()
        constraintSet.clone(motionLayout)
        // 让tvSecond的start = tvFirst的start + 收缩后的宽度
        constraintSet.connect(
            tvSecond.id,
            ConstraintSet.START,
            tvFirst.id,
            ConstraintSet.START,
            scaledFirstWidth
        )
        constraintSet.applyTo(motionLayout)
    }

    override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {}
    override fun onTransitionTrigger(motionLayout: MotionLayout?, triggerId: Int, positive: Boolean, progress: Float) {}
})

这个方案的优势是动画流畅度和布局准确性兼顾,完全利用MotionLayout的动画系统,不需要额外自定义View。

方案二:Barrier + 同步动画textSize与maxWidth

如果不想写太多监听代码,可以利用ConstraintLayout的Barrier组件,配合同步动画第一个TextView的textSize和maxWidth,让布局宽度实时变化,Barrier会自动带动第二个TextView移动。

具体步骤:

  1. 布局中添加Barrier:创建一个end方向的Barrier,引用第一个TextView作为依赖。
  2. 约束第二个TextView到Barrier:把第二个TextView的start约束到Barrier的start。
  3. MotionScene中同步动画属性:同时动画第一个TextView的textSize(从32sp到24sp)和maxWidth(从原始宽度到原始宽度*0.6),确保两者的动画进度完全同步。

这样动画过程中,第一个TextView的宽度会因为maxWidth的限制实时收缩,Barrier会跟着它的末端移动,第二个TextView自然就始终紧贴着了。

方案三:自定义View封装两个TextView

如果前两个方案都没法满足你的精度要求,可以把两个TextView封装到一个自定义View里,完全手动控制它们的布局逻辑。

核心思路:

  • 在自定义View内部维护一个缩放比例变量,动画过程中更新这个变量。
  • 在onMeasure方法中根据缩放比例计算第一个TextView的宽度和textSize,然后布局第二个TextView紧贴在它的末端。
  • 最后在MotionLayout中动画这个自定义View的缩放比例属性即可。

简化代码示例:

class StickyHeaderView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
    private val tvFirst = TextView(context).apply {
        textSize = 32f // 初始字号
        layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, 32.dpToPx())
    }
    private val tvSecond = TextView(context).apply {
        textSize = 24f
        layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, 24.dpToPx())
    }
    private var scaleRatio = 1f

    init {
        orientation = HORIZONTAL
        addView(tvFirst)
        addView(tvSecond)
    }

    fun setScaleRatio(ratio: Float) {
        scaleRatio = ratio
        tvFirst.textSize = 32 * ratio
        requestLayout()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // 先测量原始宽度
        tvFirst.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.makeMeasureSpec(32.dpToPx(), MeasureSpec.EXACTLY))
        val scaledWidth = (tvFirst.measuredWidth * scaleRatio).toInt()
        // 重新测量第一个TextView到收缩后的宽度
        tvFirst.measure(
            MeasureSpec.makeMeasureSpec(scaledWidth, MeasureSpec.EXACTLY),
            MeasureSpec.makeMeasureSpec((32 * scaleRatio).dpToPx(), MeasureSpec.EXACTLY)
        )
        // 测量第二个TextView
        tvSecond.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.makeMeasureSpec(24.dpToPx(), MeasureSpec.EXACTLY))
        // 设置自定义View的总尺寸
        setMeasuredDimension(
            tvFirst.measuredWidth + tvSecond.measuredWidth,
            max(tvFirst.measuredHeight, tvSecond.measuredHeight)
        )
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        // 布局第一个TextView在左侧
        tvFirst.layout(0, 0, tvFirst.measuredWidth, tvFirst.measuredHeight)
        // 布局第二个TextView紧贴第一个的末端,垂直居中
        val secondTop = (tvFirst.measuredHeight - tvSecond.measuredHeight) / 2
        tvSecond.layout(
            tvFirst.measuredWidth,
            secondTop,
            tvFirst.measuredWidth + tvSecond.measuredWidth,
            secondTop + tvSecond.measuredHeight
        )
    }

    private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).toInt()
}

这个方案的优势是完全可控,不会有约束延迟的问题,适合对布局精度要求极高的场景。


内容的提问来源于stack exchange,提问作者Sira Lam

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 20:37:27