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,结果水波纹跑到了矩形角落外面,产品经理当场就来找我了。

注意:RippleDrawable在Android 5.0(API 21)以上才支持。如果你的minSdk低于21,记得准备一个fallback的selector。

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动画实现

最简单的按压缩放,就是监听触摸事件,动态改变scaleXscaleY

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_DOWNACTION_UP会频繁触发。我建议在动画开始前cancel()掉之前的动画,或者加一个标志位判断。
  • 水波纹与背景色冲突:如果按钮本身有颜色背景,水波纹颜色太浅会看不见,太深又显得脏。我一般用半透明黑色(#33000000)或半透明白色(#33FFFFFF),具体看背景深浅。
  • ListView/RecyclerView中的触摸反馈:在列表项里做缩放动画,要注意elevation可能会盖住相邻项。我曾经在列表里用了缩放+阴影,结果按下的项阴影投射到了上一项上,视觉上很诡异。解决方案是降低elevation差值,或者用translationZ代替。

11.6 知识体系总览

下面这张图概括了触摸反馈动画的核心脉络:

触摸反馈动画知识体系 触摸反馈动画 水波纹效果 XML定义Ripple 代码动态创建 mask裁剪形状 ColorStateList颜色 按压缩放效果 scaleX/scaleY动画 elevation阴影变化 ACTION_CANCEL处理 列表项冲突规避 水波纹 + 按压缩放 = 组合反馈

从图中可以看到,水波纹和按压缩放是两条独立的技术路线,但最终可以组合使用。我个人强烈建议你在实际项目中把两者结合起来——用户不会注意到具体用了什么技术,但他们会觉得「这个按钮按起来很舒服」。

好了,触摸反馈动画就聊到这里。记住一句话:好的反馈让用户感觉App是「活」的。下次你写按钮的时候,不妨多花两分钟加上水波纹和缩放,效果绝对值得。


公众号:蓝海资料掘金营,微信deep3321