textview - 点击阴影缩放动画
说在开头
这个动画效果不是我想出来的,是我看到简书- 尘少少少 朋友写的
先放个图大家看看是啥样:
效果图
作者地址:Android简单酷炫点击动画(附源码)
看着是不是挺酷的,我是第一眼就喜欢上了,不过呢,我不需要这么复杂的效果,我只想要点击时 view 大小和阴影缩小就行了
我去翻了翻作者的源码,作者提供的都是不同 view 专用的 view
Snip20180919_4.png
这和我的理念不和,我需要的是非侵入性的动画,于是我自己改一下,提供一个动画工具类,另外在再处理下连点、点击事件、4.X 兼容,基本就能作为开源代码使用了
我改的动画工具类
这个工具类首先是非侵入性的,我处理了作者没处理的 连点、点击事件问题
先来看看我的 API 使用:
var layoutClickAnimator = LayoutClickAnimator(view_02, 300)
view_02.setOnClickListener({
Toast.makeText(application, "AAA", Toast.LENGTH_SHORT).show()
})
是不是很简单,没有侵入性,不要了直接去掉动画工具类就好了,我写的基本可以拿过来直接用了,我写的也和简单,该页好改
我的效果:
ezgif.com-video-to-gif.gif
项目地址:BW_Libs
思路实现
1. 动画自身代码
这里我和原作者一样,追求一个物理效果,就是我按下不松手就不会回弹,这一下就提高了难度。直接在点击时使用一套动画然后加一个 reverse 返回的 repeatMode 就不好使了。
这里必然会使用 2 套动画,一个是 down 按下时的动画,一个是 up 回弹时的动画,还要在 4.X 时去掉 Z 轴阴影的动画。
添加一个带回弹效果的插值器实际效果会非常好,要不大伙都用呢,然后加入相应的动画执行状态标记
- down 按下时动画
var animatorSet = AnimatorSet()
animatorSet.setDuration(time)
animatorSet.interpolator = OvershootInterpolator(3f)
val animatorX = ObjectAnimator.ofFloat(view, "scaleX", 1f, (1 - scaleOffset))
val animatorY = ObjectAnimator.ofFloat(view, "scaleY", 1f, (1 - scaleOffset))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val animatorZ = ObjectAnimator.ofFloat(view, "translationZ", transitionZ, transitionZEnd)
animatorSet.playTogether(animatorX, animatorY, animatorZ)
} else {
animatorSet.playTogether(animatorX, animatorY)
}
animatorSet.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
isDowning = true
}
override fun onAnimationEnd(animation: Animator?) {
isDowning = false
}
})
- up 回弹动画
var animatorSet = AnimatorSet()
animatorSet.setDuration(time)
animatorSet.interpolator = OvershootInterpolator(3f)
val animatorX = ObjectAnimator.ofFloat(view, "scaleX", (1 - scaleOffset), 1f)
val animatorY = ObjectAnimator.ofFloat(view, "scaleY", (1 - scaleOffset), 1f)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val animatorZ = ObjectAnimator.ofFloat(view, "translationZ", transitionZEnd, transitionZ)
animatorSet.playTogether(animatorX, animatorY, animatorZ)
} else {
animatorSet.playTogether(animatorX, animatorY)
}
animatorSet.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
isUping = true
}
override fun onAnimationEnd(animation: Animator?) {
isUping = false
}
})
2. 核心触摸事件处理
最核心的就是触摸事件处理了,我们不光要处理 down 的事件,我们还要处理 up 的事件,要是传入的 view 是 viewGroup 容器类,up 的事件默认是拿不到的,没办法我又不想给每种 view 写一个专门的类所以就没法重写触摸的拦截事件拦截掉本地触摸不再向下传递,但是好在我们还可以添加 touch 的监听,在监听里直接返回 true 消费事件,这样 up 的事件也能拿到,效果也能接受不是
先看代码:
/**
* 给指定 view 添加触摸监听
* 1. 这里的思路是模拟自然,我们按下时开始收缩动画,手不松开时,回弹动画是是不会指定的,所以我们需要分别处理 ACTION_DOWN 和 ACTION_UP,
* 所以这里我们需要2个动画集合
* 2. 我试过把动画都写在一个动画集合中,会掉帧,实际效果不理想,虽然只掉了 2 帧
* 3. 因为考虑要适配所有的 view ,传入的 view 有可能是 layoutGroup 类型的,因为不想写专门的 view ,所以只能添加 addTouchListener ,
* 直接消费掉事件 return true,要不 ACTION_UP 不会相应的,事件会回传给更上层 view
* 4. 使用 2 个标记分别标记缩放动画和回弹动画,是为了处理测试让人讨厌的连续点击,因为是2个动画,一个完整的点击会触发 ACTION_DOWN 和 ACTION_UP 2个事件,
* 所以这里单单使用按钮的防止重复点击的策略就不够了
* 5. 多加一个逻辑维度,逻辑复杂性立马就提升很多,所以能简单还是要写的简单
*/
private fun addTouchListener() {
view.setOnTouchListener(object : View.OnTouchListener {
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
val action = event?.action
if (action == MotionEvent.ACTION_DOWN && !fastClickUtils.isFastClick() && !isDowning && !isUping) {
startAnimartorDown()
}
if ((action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) && !isUping) {
startAnimartorUp()
view.callOnClick()
}
return true
}
})
}
-
- 按下时,我们需要判断是不是连续点击,然后是不是 按下的动画或是回弹的动画在执行,都不是我们才能去启动按下的动画,为的即使过滤掉在整个动画执行时的连续点击了
-
- 我们在回弹时启动 view 的点击事件我想了想是比较合适的时机了,比如我们启动另一个页面时是需要时间的,既然动画已经花了一些时间了,在动画结速后再让用户等一会就显得不恰当了,动画本身也有一种时间过度的意思在里面,在让用户看动画时不知不觉得过度到下一个页面是种很好的体验,至少我觉得是。
- 大家想想典型的一次点击 down ,up 是很快就完成的,这就是说 down 时的动画还没跑完 up 就被触发了,如果这个时候我们再去判断 fastClickUtils.isFastClick 连点和 isDowning down 动画执行完没有就会出问题的。我们只要判断 up 自身的动画就行了
3. 完整代码
最终代码也就百十来行,但是我改了好多次才算是比较稳定,没 bug 了,单反是要求高的代码就没有好些的,大家平时多写写这样的代码,遇到的问题多了,调试的 bug 多了,以后在写时自然就知道哪里可能会有问题了,这种手上的记忆可是很深刻的
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val animatorZ = ObjectAnimator.ofFloat(view, "translationZ", transitionZ, transitionZEnd)
animatorSet.playTogether(animatorX, animatorY, animatorZ)
} else {
animatorSet.playTogether(animatorX, animatorY)
}
animatorSet.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
isDowning = true
}
override fun onAnimationEnd(animation: Animator?) {
isDowning = false
}
})
animatorSet.start()
}
/**
* 手松开时回弹动画
*/
private fun startAnimartorUp() {
var animatorSet = AnimatorSet()
animatorSet.setDuration(time)
animatorSet.interpolator = OvershootInterpolator(3f)
val animatorX = ObjectAnimator.ofFloat(view, "scaleX", (1 - scaleOffset), 1f)
val animatorY = ObjectAnimator.ofFloat(view, "scaleY", (1 - scaleOffset), 1f)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val animatorZ = ObjectAnimator.ofFloat(view, "translationZ", transitionZEnd, transitionZ)
animatorSet.playTogether(animatorX, animatorY, animatorZ)
} else {
animatorSet.playTogether(animatorX, animatorY)
}
animatorSet.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
isUping = true
}
override fun onAnimationEnd(animation: Animator?) {
isUping = false
}
})
animatorSet.start()
}
}