You need to enable JavaScript to run this app. Android animateLayoutChanges失效时的子View可见性动画实现
优惠活动
大模型
产品
解决方案
定价
更多

Android animateLayoutChanges失效时的子View可见性动画实现

布局高度随子控件显隐变化的过渡动画实现方案

原有animateLayoutChanges配置失效的核心原因

  • 配置android:animateLayoutChanges="true"本质是给父容器挂载系统默认LayoutTransition实例,但默认实现的时序逻辑和当前场景不匹配:
    • 将View设置为View.GONE时,默认流程是先播放View自身的消失动画,动画播放完成后才将View从布局空间中移除,这个移除空间的过程是瞬时跳变,没有动画插值
    • 将View设置为View.VISIBLE时,默认流程是先让View在布局中占据空间,再播放View自身的出现动画,占据空间的过程是瞬时跳变
    • 上述两个过程会直接导致wrap_content高度的CardView瞬间完成高度跳变,最终表现为没有任何过渡动画
  • 部分系统版本的默认LayoutTransition对wrap_content类型的父布局适配存在缺陷,会直接跳过高度变化的动画插值。

可落地的实现方案

方案1:使用Jetpack Transition捕获单次布局变化(最简单)

不需要自定义测量逻辑,系统会自动对比布局变化前后的尺寸差生成平滑动画,是目前适配性最好的轻量方案:

  1. 移除之前在CardView和子View上配置的android:animateLayoutChanges="true"属性,避免两套动画逻辑冲突
  2. 业务代码中在修改可见性前,触发一次延迟过渡捕获:
    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显隐的淡入淡出效果),和业务场景的动画需求完全匹配。

  3. 如果需要调整动画时长或效果,可以传入自定义的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.29 01:15:01