ConstraintLayout动画后子控件冻结,如何实现布局重绘?
你遇到的这个问题其实是AndroidView动画的典型坑——默认的View动画只是对View的绘制快照做变换,并没有真正改变View在布局中的实际位置和触摸区域。动画结束后,虽然你看到控件回到了屏幕内,但它的可交互区域还停留在动画前的位置(甚至可能在屏幕外),所以看起来像是“冻结”了。
下面给你两种靠谱的解决思路,按需选择:
一、改用属性动画(推荐)
属性动画是真的修改View的属性(比如translationX、translationY),会同步更新View的实际位置和触摸区域,完全不会有快照残留的问题。用ViewPropertyAnimator写起来超简单:
示例代码:
// 移出屏幕动画 yourConstraintLayout.animate() .translationX(-yourConstraintLayout.width.toFloat()) .setDuration(300) .withEndAction { // 动画结束后执行移入 yourConstraintLayout.animate() .translationX(0f) .setDuration(300) .start() } .start()
如果还是想用XML定义动画,可以用属性动画的XML格式(放在res/animator/目录下,不是res/anim/):
比如slide_out_left_animator.xml:
<?xml version="1.0" encoding="utf-8"?> <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android" android:propertyName="translationX" android:valueFrom="0" android:valueTo="-100%" android:duration="300"/>
slide_in_left_animator.xml:
<?xml version="1.0" encoding="utf-8"?> <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android" android:propertyName="translationX" android:valueFrom="-100%" android:valueTo="0" android:duration="300"/>
然后代码中调用:
val outAnim = AnimatorInflater.loadAnimator(context, R.animator.slide_out_left_animator) val inAnim = AnimatorInflater.loadAnimator(context, R.animator.slide_in_left_animator) outAnim.setTarget(yourConstraintLayout) inAnim.setTarget(yourConstraintLayout) outAnim.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) inAnim.start() } }) outAnim.start()
二、如果必须用View动画(兼容旧代码)
要是你不想改动画类型,那一定要在动画结束后清除动画并同步View的实际位置:
- 给动画设置监听器,在结束时执行以下操作:
val slideOut = AnimationUtils.loadAnimation(context, R.anim.slide_out_left) val slideIn = AnimationUtils.loadAnimation(context, R.anim.slide_in_left) slideOut.setAnimationListener(object : Animation.AnimationListener { override fun onAnimationStart(animation: Animation?) {} override fun onAnimationEnd(animation: Animation?) { // 先清除动画,避免快照残留 yourConstraintLayout.clearAnimation() // 把View的位置设置到动画结束的位置(这里是移出屏幕左边缘) yourConstraintLayout.translationX = -yourConstraintLayout.width.toFloat() // 执行移入动画 slideIn.setAnimationListener(object : Animation.AnimationListener { override fun onAnimationStart(animation: Animation?) {} override fun onAnimationEnd(animation: Animation?) { yourConstraintLayout.clearAnimation() // 移入后把位置重置回0,确保触摸区域正确 yourConstraintLayout.translationX = 0f // 强制触发重绘和布局更新 yourConstraintLayout.requestLayout() yourConstraintLayout.invalidate() } override fun onAnimationRepeat(animation: Animation?) {} }) yourConstraintLayout.startAnimation(slideIn) } override fun onAnimationRepeat(animation: Animation?) {} }) yourConstraintLayout.startAnimation(slideOut)
关键就是动画结束后调用clearAnimation()清除快照,然后用translationX或者layout()方法把View的实际位置同步到视觉位置,最后调用requestLayout()和invalidate()确保布局重新计算和绘制。
为什么你的原方案会出问题?
旧的View动画(Animation类)只是在绘制时对Canvas做变换,View的getLeft()、getTop()等位置属性完全没变,系统的触摸事件还是会发送到原来的位置。动画结束后如果不清除动画,系统还会继续显示快照,导致你看到的控件和实际可交互的控件“分家”。
内容的提问来源于stack exchange,提问作者Calco

