如何在MotionLayout中为wrap_content属性的TextView实现平滑动画并让第二个TextView始终贴合其内容末端?
我之前刚好处理过类似的粘性头部需求,结合你遇到的问题——既要第一个TextView平滑收缩,又要第二个TextView始终紧贴其内容末端——给你几个实用的解决方案,应该能完美解决你的痛点:
方案一:MotionLayout监听过渡进度 + 动态修改约束
这个方案是最贴合你现有MotionLayout实现的,核心思路是不依赖scale缩放(因为缩放不改变布局宽度),而是通过监听动画进度,实时计算第一个TextView的收缩宽度,然后动态调整第二个TextView的约束位置。
具体步骤:
- 预先获取第一个TextView的原始宽度:在布局初始化完成后,获取它的
measuredWidth(也就是内容完全展开时的宽度)。 - 在MotionScene中定义自定义动画属性:不用直接动画scale或textSize,而是定义一个从
1f到0.6f的自定义缩放比例属性,比如app:customScale。 - 监听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移动。
具体步骤:
- 布局中添加Barrier:创建一个
end方向的Barrier,引用第一个TextView作为依赖。 - 约束第二个TextView到Barrier:把第二个TextView的
start约束到Barrier的start。 - 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
相关产品推荐
相关产品推荐

