使用ConstraintLayout与ValueAnimator实现展开/收起动画遇问题求助
嘿,这个坑我之前踩过好几次!当用ConstraintLayout搭配ValueAnimator做展开/收起动画时,最容易碰到的就是这个“先闪一下完整高度再动画”的问题——本质原因是你把视图设为VISIBLE的瞬间,ConstraintLayout会立刻完成测量和布局,直接把视图渲染成它的完整高度,而这时候你的动画还没开始跑,所以就出现了那个尴尬的闪屏。
下面给你两种靠谱的解决办法,亲测有效:
方法一:手动控制高度 + ValueAnimator(适合自定义动画逻辑)
这种方法的核心是在视图变为VISIBLE后,立刻把它的高度强制设为动画的初始值(比如0),然后再启动动画到目标高度。不过要注意,得先拿到视图的真实目标高度,不然动画会没效果:
// 假设你的父布局是constraintLayout,要动画的视图是expandableView fun toggleExpand() { if (expandableView.visibility == View.GONE) { // 先让视图可见,然后测量它的真实高度 expandableView.visibility = View.VISIBLE // 手动测量视图的高度(因为GONE状态下measuredHeight为0) expandableView.measure( View.MeasureSpec.makeMeasureSpec(constraintLayout.width, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) ) val targetHeight = expandableView.measuredHeight // 立刻把高度设为0,避免闪屏 val params = expandableView.layoutParams as ConstraintLayout.LayoutParams params.height = 0 expandableView.layoutParams = params // 启动展开动画 val animator = ValueAnimator.ofInt(0, targetHeight) animator.duration = 300 animator.addUpdateListener { animation -> val currentHeight = animation.animatedValue as Int val layoutParams = expandableView.layoutParams as ConstraintLayout.LayoutParams layoutParams.height = currentHeight expandableView.layoutParams = layoutParams } animator.start() } else { // 收起动画,从当前高度到0 val currentHeight = expandableView.height val animator = ValueAnimator.ofInt(currentHeight, 0) animator.duration = 300 animator.addUpdateListener { animation -> val currentHeight = animation.animatedValue as Int val layoutParams = expandableView.layoutParams as ConstraintLayout.LayoutParams layoutParams.height = currentHeight expandableView.layoutParams = layoutParams } animator.addListener(object : Animator.AnimatorListener { override fun onAnimationEnd(animation: Animator?) { expandableView.visibility = View.GONE } override fun onAnimationStart(animation: Animator?) {} override fun onAnimationCancel(animation: Animator?) {} override fun onAnimationRepeat(animation: Animator?) {} }) animator.start() } }
方法二:用ConstraintSet + TransitionManager(官方推荐,更优雅)
ConstraintLayout自带的ConstraintSet结合TransitionManager可以更流畅地处理布局动画,不需要手动控制高度,官方会帮你处理测量和过渡:
private var isExpanded = false private val constraintSetCollapsed = ConstraintSet() private val constraintSetExpanded = ConstraintSet() // 在onCreate里初始化两个状态的ConstraintSet override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.your_layout) // 克隆当前布局作为收起状态(把目标视图高度设为0) constraintSetCollapsed.clone(constraintLayout) constraintSetCollapsed.constrainHeight(R.id.expandableView, 0) // 克隆当前布局作为展开状态(目标视图高度设为WRAP_CONTENT) constraintSetExpanded.clone(constraintLayout) } fun toggleExpand() { TransitionManager.beginDelayedTransition(constraintLayout) if (isExpanded) { // 切换到收起状态 constraintSetCollapsed.applyTo(constraintLayout) // 动画结束后可以设为GONE,也可以保持VISIBLE但高度为0 expandableView.postDelayed({ expandableView.visibility = View.GONE }, 300) } else { expandableView.visibility = View.VISIBLE // 切换到展开状态 constraintSetExpanded.applyTo(constraintLayout) } isExpanded = !isExpanded }
这种方法的优势在于,TransitionManager会自动捕捉布局状态的变化,生成平滑的过渡动画,完全不会出现闪屏的问题,代码也更简洁。
为什么原来的代码会出问题?
当你直接把视图从GONE/INVISIBLE改为VISIBLE时,ConstraintLayout会立即触发一次布局重绘,这时候视图的高度会被设置为它的固有高度(比如wrap_content对应的真实高度),而你的动画是在这之后才启动的——所以用户会先看到完整高度的视图,然后才看到动画效果,视觉上就像是“闪了一下”。
内容的提问来源于stack exchange,提问作者Patryk Jabłoński
相关产品推荐
相关产品推荐

