Android animateLayoutChanges失效时的子View可见性动画实现
布局高度随子控件显隐变化的过渡动画实现方案
原有animateLayoutChanges配置失效的核心原因
- 配置
android:animateLayoutChanges="true"本质是给父容器挂载系统默认LayoutTransition实例,但默认实现的时序逻辑和当前场景不匹配:- 将View设置为
View.GONE时,默认流程是先播放View自身的消失动画,动画播放完成后才将View从布局空间中移除,这个移除空间的过程是瞬时跳变,没有动画插值 - 将View设置为
View.VISIBLE时,默认流程是先让View在布局中占据空间,再播放View自身的出现动画,占据空间的过程是瞬时跳变 - 上述两个过程会直接导致wrap_content高度的CardView瞬间完成高度跳变,最终表现为没有任何过渡动画
- 将View设置为
- 部分系统版本的默认LayoutTransition对wrap_content类型的父布局适配存在缺陷,会直接跳过高度变化的动画插值。
可落地的实现方案
方案1:使用Jetpack Transition捕获单次布局变化(最简单)
不需要自定义测量逻辑,系统会自动对比布局变化前后的尺寸差生成平滑动画,是目前适配性最好的轻量方案:
- 移除之前在CardView和子View上配置的
android:animateLayoutChanges="true"属性,避免两套动画逻辑冲突 - 业务代码中在修改可见性前,触发一次延迟过渡捕获:
fun updateVisibility() { // 自动捕获后续布局变化,生成针对尺寸、位置变化的平滑过渡 TransitionManager.beginDelayedTransition(binding.cardView) if (something) { binding.firstView.visibility = View.VISIBLE binding.secondView.visibility = View.GONE } else { binding.secondView.visibility = View.VISIBLE binding.firstView.visibility = View.GONE } }该方法使用的默认Transition集合同时包含ChangeBounds(处理尺寸/位置变化)和Fade(处理View显隐的淡入淡出效果),和业务场景的动画需求完全匹配。
- 如果需要调整动画时长或效果,可以传入自定义的Transition实例替换默认实现,比如针对高度变化配置专用插值器:
val heightChangeTransition = ChangeBounds().apply { duration = resources.getInteger(android.R.integer.config_mediumAnimTime).toLong() } TransitionManager.beginDelayedTransition(binding.cardView, heightChangeTransition)
方案2:手动实现高度值动画(适配全机型无兼容问题)
针对需要完全掌控动画流程的场景,通过手动测量前后高度差值实现过渡,完全不受系统默认动画逻辑的缺陷影响:
fun updateVisibility() { val card = binding.cardView // 记录动画启动前的CardView当前高度 val startHeight = card.height // 临时切换子View可见性,使用INVISIBLE保证布局空间正常分配 if (something) { binding.firstView.visibility = View.VISIBLE binding.secondView.visibility = View.INVISIBLE } else { binding.secondView.visibility = View.VISIBLE binding.firstView.visibility = View.INVISIBLE } // 待预布局完成后,获取切换后的目标高度 card.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { override fun onGlobalLayout() { card.viewTreeObserver.removeOnGlobalLayoutListener(this) val endHeight = card.height // 构造高度插值动画 ValueAnimator.ofInt(startHeight, endHeight).apply { duration = 300 addUpdateListener { // 动画过程中设置固定高度,实现高度平滑变化 card.layoutParams = card.layoutParams.apply { height = it.animatedValue as Int } } doOnEnd { // 动画结束后还原wrap_content的高度规则,避免影响后续布局 card.layoutParams = card.layoutParams.apply { height = ViewGroup.LayoutParams.WRAP_CONTENT } // 真正移除不需要显示的View的布局空间 if (something) { binding.secondView.visibility = View.GONE } else { binding.firstView.visibility = View.GONE } } start() } } }) }
注意事项
- 不要在同一个父布局上同时开启
animateLayoutChanges和调用TransitionManager.beginDelayedTransition(),两套过渡逻辑会互相干扰,出现动画错乱、重复播放的问题 - 手动实现的高度动画,一定要在动画结束后还原为原始的wrap_content高度规则,否则会导致后续布局更新时出现高度计算错误
内容的提问来源于stack exchange,提问作者aryaxt
相关产品推荐
相关产品推荐

