11、动画与触摸反馈:触摸反馈动画设计、水波纹效果(RippleDrawable)、按压缩放效果实现
触摸反馈,说白了就是让用户知道「你点到了」。我见过太多App,按钮按下去毫无反应,用户会怀疑自己是不是没点到,然后疯狂再点几下——体验直接崩了。今天我们就来聊聊,怎么用动画把触摸反馈做得既自然又优雅。
11.1 触摸反馈的本质:一触即发的视觉响应
触摸反馈的核心逻辑其实很简单:用户手指按下时,视觉上立刻给出变化;手指抬起时,变化恢复。这个「变化」可以是颜色、大小、阴影、形状,甚至是组合效果。
我个人习惯把触摸反馈分成两类:
- 瞬时反馈:按下瞬间就触发,比如水波纹扩散、颜色变深
- 持续反馈:按住期间保持状态,比如按钮缩小、阴影加深
你想想看,如果两者结合,效果会非常扎实。比如一个按钮,按下时缩小+水波纹同时出现,松开后弹回原大小——用户会感觉「嗯,这App很跟手」。
11.2 RippleDrawable:Android自带的水波纹方案
水波纹效果是Material Design的标配。Android提供了RippleDrawable,用起来其实不复杂,但坑也不少。
11.2.1 基础用法:XML定义水波纹
最简单的做法,是在drawable文件夹里建一个ripple资源:
<!-- res/drawable/ripple_button.xml -->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#33000000">
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<solid android:color="#FFFFFF" />
<corners android:radius="8dp" />
</shape>
</item>
</ripple>
然后把这个drawable设成按钮的背景:
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/ripple_button"
android:text="点我试试" />
这里有个关键点:mask决定了水波纹的裁剪形状。如果你不设mask,水波纹会扩散到整个View边界——有时候会溢出,很难看。我曾经在一个圆角按钮上忘了加mask,结果水波纹跑到了矩形角落外面,产品经理当场就来找我了。
11.2.2 代码中动态创建RippleDrawable
有时候我们需要在代码里动态设置水波纹,比如根据主题颜色变化。可以这样写:
fun View.setRippleBackground(
rippleColor: Int = Color.parseColor("#33000000"),
cornerRadius: Float = 8f.dpToPx()
) {
val mask = GradientDrawable().apply {
shape = GradientDrawable.RECTANGLE
setColor(Color.WHITE)
cornerRadius = cornerRadius
}
val ripple = RippleDrawable(
ColorStateList.valueOf(rippleColor),
null, // 不设内容层,只显示水波纹
mask
)
background = ripple
}
嗯,这里要注意:RippleDrawable的第二个参数是内容层,如果你传null,水波纹会直接显示在View原本的背景上。如果你传一个Drawable,那水波纹会覆盖在它上面。
11.3 按压缩放效果:让按钮「活」起来
水波纹是平面上的反馈,而按压缩放是立体上的反馈。两者结合,效果翻倍。
11.3.1 用View的scale动画实现
最简单的按压缩放,就是监听触摸事件,动态改变scaleX和scaleY:
fun View.setPressScaleAnimation(
scaleFactor: Float = 0.95f,
duration: Long = 100L
) {
setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
animate()
.scaleX(scaleFactor)
.scaleY(scaleFactor)
.setDuration(duration)
.start()
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL -> {
animate()
.scaleX(1f)
.scaleY(1f)
.setDuration(duration)
.start()
}
}
false
}
}
调用方式也很简单:
button.setPressScaleAnimation(0.92f)
我个人习惯把scaleFactor设在0.92到0.96之间。太小了显得按钮「塌了」,太大了又感觉没反馈。你可以在真机上试试,不同尺寸的按钮感觉不一样——小按钮可以缩多一点,大按钮缩一点点就够了。
ViewPropertyAnimator做缩放,记得在ACTION_CANCEL时也恢复原状。用户手指滑出按钮时,系统会发CANCEL事件,不处理的话按钮会一直缩着。
11.3.2 结合Elevation阴影变化
光缩放还不够,加上阴影变化会更真实。按下时降低elevation,模拟按钮被「按下去」的感觉:
fun View.setPressElevationAnimation(
normalElevation: Float = 4f.dpToPx(),
pressedElevation: Float = 1f.dpToPx()
) {
elevation = normalElevation
setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
animate()
.scaleX(0.95f)
.scaleY(0.95f)
.elevation(pressedElevation)
.setDuration(100)
.start()
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL -> {
animate()
.scaleX(1f)
.scaleY(1f)
.elevation(normalElevation)
.setDuration(100)
.start()
}
}
false
}
}
为什么这样做更真实?你想想看,现实世界中你按一个实体按钮,它确实会下沉、阴影会变小。Android的elevation模拟的就是这个物理效果。
11.4 水波纹 + 按压缩放:组合拳
把两者结合起来,效果就很完整了。我一般这样写:
fun View.setupTouchFeedback(
rippleColor: Int = Color.parseColor("#33000000"),
scaleFactor: Float = 0.95f,
cornerRadius: Float = 8f.dpToPx()
) {
// 设置水波纹背景
setRippleBackground(rippleColor, cornerRadius)
// 设置按压缩放
setPressScaleAnimation(scaleFactor)
}
调用一次,水波纹和缩放都有了。用户按下时,水波纹从触点扩散,同时按钮微微缩小——这个组合反馈非常跟手。
11.5 避坑指南:我踩过的几个坑
做触摸反馈动画,有几个坑我印象特别深:
- 动画被重复触发:如果用户快速连点,
ACTION_DOWN和ACTION_UP会频繁触发。我建议在动画开始前cancel()掉之前的动画,或者加一个标志位判断。 - 水波纹与背景色冲突:如果按钮本身有颜色背景,水波纹颜色太浅会看不见,太深又显得脏。我一般用半透明黑色(
#33000000)或半透明白色(#33FFFFFF),具体看背景深浅。 - ListView/RecyclerView中的触摸反馈:在列表项里做缩放动画,要注意
elevation可能会盖住相邻项。我曾经在列表里用了缩放+阴影,结果按下的项阴影投射到了上一项上,视觉上很诡异。解决方案是降低elevation差值,或者用translationZ代替。
11.6 知识体系总览
下面这张图概括了触摸反馈动画的核心脉络:
从图中可以看到,水波纹和按压缩放是两条独立的技术路线,但最终可以组合使用。我个人强烈建议你在实际项目中把两者结合起来——用户不会注意到具体用了什么技术,但他们会觉得「这个按钮按起来很舒服」。
好了,触摸反馈动画就聊到这里。记住一句话:好的反馈让用户感觉App是「活」的。下次你写按钮的时候,不妨多花两分钟加上水波纹和缩放,效果绝对值得。
公众号:蓝海资料掘金营,微信deep3321