在 Android 开发中,我们难免会使用动画来处理各种各样的动画效果,以满足 UI 的高逼格设计。对于比较复杂的动画效果,我们通常会采用著名的开源库:lottie-android,或许你会对 lottie 的原理充满好奇,但这并不在我们这篇文章的讨论范围,感兴趣的自行 Google 吧~
属性动画和补间动画的基本编写方式我一度在论坛上看到人使用了 TranslateAnimation 对控件做了移动操作,然后发现在 View 的新位置点击并没有响应自己的点击事件,反倒是之前的位置能够响应。实际上,补间动画仅仅是对 View 在视觉效果上做了移动、缩放、旋转和淡入淡出的效果,其实并没有真正改变 View 的属性。但我们大多数情况下肯定希望 View 在经过动效后响应触摸事件的位置和视觉效果相同,所以在 Android 3.0 之后引入了属性动画,彻底解决了这个难题。
可能还有一些小伙伴不明白怎样的代码是属性动画,怎样的代码是补间动画。下面针对 View 向右平移 500 px 做一下简单的演示。
对于属性动画,你可以用下面的两种方式。
ObjectAnimator.ofFloat(tv1, "translationX", 0f, 500f) .setDuration(1000) .start() // 或者像这样 tv1.animate().setDuration(1000).translationX(500f)但用补间动画,并且你想达到同样的效果的话。
val anim = TranslateAnimation(0f, 500f, 0f, 0f) anim.duration = 1000 anim.fillAfter = true // 设置保留动画后的状态 tv1.startAnimation(anim) 属性动画的使用注意点对于属性动画来说,尤其需要注意的是操作的属性需要有 set 和 get 方法,不然你的 ObjectAnimator 操作就不会生效。比如水平平移,我们知道,View 的 translationX 属性设置方法接受的是 float 值,所以你把上面的操作编写为 ofInt 就不会生效,比如:
ObjectAnimator.ofInt(tv1, "translationX", 0, 500) .setDuration(1000) .start()对于我们需要用到但又没有写好的属性,比如我们自定义一个进度条 View,我们需要实时展示进度,这时候我们就可以自己定义一个属性,并让它支持 set 和 get,那么在外面就可以对这个自定义的 View 做属性动画操作了。
属性动画和补间动画工作原理 属性动画属性动画的工作原理很简单,其实就是在一定的时间间隔内,通过不断地对值进行改变,并不断将该值赋给对象的属性,从而实现该对象在属性上的动画效果。
这个属性可以是任意对象的属性。
从上述工作原理可以看出属性动画有两个非常重要的类:ValueAnimator 类 & ObjectAnimator 类,二者的区别在于:
ValueAnimator 类是先改变值,然后 手动赋值 给对象的属性从而实现动画;是 间接 对对象属性进行操作;而 ValueAnimator 类本质上是一种 改变值 的操作机制。
ObjectAnimator 类是先改变值,然后 自动赋值 给对象的属性从而实现动画;是 直接 对对象属性进行操作;可以理解为:ObjectAnimator 更加智能、自动化程度更高。
补间动画而对于补间动画,我们不妨跟进源码,看看到底做了什么操作。
/** * Start the specified animation now. * * @param animation the animation to start now */ public void startAnimation(Animation animation) { animation.setStartTime(Animation.START_ON_FIRST_FRAME); setAnimation(animation); invalidateParentCaches(); invalidate(true); }看到了非常明显 invalidate() 方法,很明显,补间动画在执行的时候,直接导致了 View 执行 onDraw() 方法。总的来说,补间动画的核心本质就是在一定的持续时间内,不断改变 Matrix 变换,并且不断刷新的过程。
为什么属性动画移动一个 View 后,目标位置还可以响应触摸事件呢?这个问题来自 wanandroid,在此前,我一直认为既然 View 的属性得到了改变,那么经过属性动画后的控件应该所有属性都等同于直接设置在动画后的位置的控件。
看完「陈小缘」的回答后,我突然才想到,虽然 View 做了属性上的改变,但其实并没有更改 View 的 left、right、top、bottom 这些属性,而这些属性恰恰决定了 ViewGroup 的触摸区域判断。
tv1.animate().setDuration(1000).translationX(500f)那么,假定我们的 View 经过了上面的平移操作后,为什么点击新的位置能够响应到这个点击事件呢?
看了「陈小缘」的回答,我顺便深入了一波源码,想想必须在这分享给大家。
我们知道,在 ViewGroup 没有重写 onInterceptTouchEvent() 方法进行事件拦截的时候,我们一定会通过其 dispatchTouchEvent() 方法进行事件分发,而决定我们哪一个子 View 响应我们的触摸事件的条件又是 我们手指的位置必须在这个子 View 的边界范围内,也就是 left、right、top、bottom 这四个属性形成的矩形区域。