12、动画与状态切换:StateListAnimator使用、Selector与动画结合、按钮状态动画实战

按钮的点击反馈,说白了就是用户和界面之间的「握手」。按下去没反应,用户心里会发毛。Android 提供了很多方式来做这件事,但 StateListAnimator 是我个人觉得最优雅的一种。它把动画和状态绑定在一起,不用写一堆 if-else 来判断按钮是 pressed 还是 focused。

12.1 StateListAnimator 是什么?

StateListAnimator 是 Android 5.0(API 21)引入的。它允许你为 View 的不同状态定义不同的动画。比如按钮按下时缩小一点,松开时弹回来。

它的工作原理和 StateListDrawable 很像。只不过 StateListDrawable 切换的是图片,而 StateListAnimator 切换的是动画。

核心思想:状态驱动动画。你定义好「当 View 处于 pressed 状态时,执行这个动画;当处于 focused 状态时,执行那个动画」,剩下的交给系统去调度。

12.2 怎么用?XML 方式

我个人习惯先在 res/animator/ 目录下创建动画文件。比如 button_press.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <objectAnimator
        android:propertyName="scaleX"
        android:duration="100"
        android:valueTo="0.95"
        android:valueType="floatType" />
    <objectAnimator
        android:propertyName="scaleY"
        android:duration="100"
        android:valueTo="0.95"
        android:valueType="floatType" />
</set>

再创建一个 button_release.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <objectAnimator
        android:propertyName="scaleX"
        android:duration="100"
        android:valueTo="1.0"
        android:valueType="floatType" />
    <objectAnimator
        android:propertyName="scaleY"
        android:duration="100"
        android:valueTo="1.0"
        android:valueType="floatType" />
</set>

然后在 res/animator/button_state_animator.xml 里把它们组合起来:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <set>
            <objectAnimator
                android:propertyName="scaleX"
                android:duration="100"
                android:valueTo="0.95"
                android:valueType="floatType" />
            <objectAnimator
                android:propertyName="scaleY"
                android:duration="100"
                android:valueTo="0.95"
                android:valueType="floatType" />
        </set>
    </item>
    <item android:state_focused="true">
        <set>
            <objectAnimator
                android:propertyName="translationZ"
                android:duration="100"
                android:valueTo="4dp"
                android:valueType="floatType" />
        </set>
    </item>
    <item>
        <set>
            <objectAnimator
                android:propertyName="scaleX"
                android:duration="100"
                android:valueTo="1.0"
                android:valueType="floatType" />
            <objectAnimator
                android:propertyName="scaleY"
                android:duration="100"
                android:valueTo="1.0"
                android:valueType="floatType" />
            <objectAnimator
                android:propertyName="translationZ"
                android:duration="100"
                android:valueTo="0dp"
                android:valueType="floatType" />
        </set>
    </item>
</selector>

最后在布局文件里引用:

<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="点我"
    android:stateListAnimator="@animator/button_state_animator" />

小提示:如果你用的是 MaterialButton,它自带了一个默认的 StateListAnimator。你覆盖它的时候要小心,别把 elevation 动画搞丢了。

12.3 代码方式创建

有时候你需要在代码里动态创建。比如从服务器拿到一个配置,决定按钮按下去是缩小还是变色。这时候 XML 就不够灵活了。

val pressAnimator = AnimatorSet().apply {
    playTogether(
        ObjectAnimator.ofFloat(button, "scaleX", 0.95f),
        ObjectAnimator.ofFloat(button, "scaleY", 0.95f)
    )
    duration = 100
}

val releaseAnimator = AnimatorSet().apply {
    playTogether(
        ObjectAnimator.ofFloat(button, "scaleX", 1.0f),
        ObjectAnimator.ofFloat(button, "scaleY", 1.0f)
    )
    duration = 100
}

val stateListAnimator = AnimatorStateListAnimator().apply {
    addState(intArrayOf(android.R.attr.state_pressed), pressAnimator)
    addState(intArrayOf(), releaseAnimator)
}

button.stateListAnimator = stateListAnimator

嗯,这里要注意:addState 的第二个参数是 Animator,不是 AnimatorSet。但 AnimatorSet 本身继承自 Animator,所以可以传进去。我刚开始用的时候踩过这个坑,以为要传一个单独的 ObjectAnimator,结果发现 AnimatorSet 也行。

12.4 和 Selector 结合使用

StateListAnimator 管的是动画,Selector 管的是背景。两者可以完美配合。比如按钮按下去时,背景变色 + 按钮缩小。

我曾经在一个电商项目里做过一个「抢购按钮」。按下去时背景从红色变成暗红色,同时按钮微微下沉。效果非常真实,用户反馈说「按起来很爽」。

实现方式很简单:

<Button
    android:layout_width="match_parent"
    android:layout_height="48dp"
    android:background="@drawable/btn_selector"
    android:stateListAnimator="@animator/btn_press_animator"
    android:text="立即抢购" />

其中 btn_selector.xml 是普通的 StateListDrawable:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true" android:drawable="@color/dark_red" />
    <item android:drawable="@color/bright_red" />
</selector>

注意:StateListAnimator 和 View 的 animate() 方法不要混用。StateListAnimator 内部会接管 View 的属性动画,如果你同时在代码里调 view.animate().scaleX(0.5f),两个动画会打架。我曾经调试了一个下午才发现是这个问题。

12.5 实战:一个完整的按钮状态动画

我们来做一个实际点的例子。一个登录按钮,有四种状态:

  • normal:默认状态,蓝色背景,正常大小
  • pressed:按下状态,背景变暗,缩小到 0.95
  • focused:聚焦状态(比如用键盘导航时), elevation 升高,加一个光晕
  • disabled:禁用状态,灰色背景,不可点击

StateListAnimator 的 XML 实现:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <set>
            <objectAnimator
                android:propertyName="scaleX"
                android:duration="80"
                android:valueTo="0.95"
                android:valueType="floatType" />
            <objectAnimator
                android:propertyName="scaleY"
                android:duration="80"
                android:valueTo="0.95"
                android:valueType="floatType" />
            <objectAnimator
                android:propertyName="translationZ"
                android:duration="80"
                android:valueTo="2dp"
                android:valueType="floatType" />
        </set>
    </item>
    <item android:state_focused="true">
        <set>
            <objectAnimator
                android:propertyName="translationZ"
                android:duration="100"
                android:valueTo="6dp"
                android:valueType="floatType" />
        </set>
    </item>
    <item android:state_enabled="false">
        <set>
            <objectAnimator
                android:propertyName="alpha"
                android:duration="200"
                android:valueTo="0.5"
                android:valueType="floatType" />
        </set>
    </item>
    <item>
        <set>
            <objectAnimator
                android:propertyName="scaleX"
                android:duration="80"
                android:valueTo="1.0"
                android:valueType="floatType" />
            <objectAnimator
                android:propertyName="scaleY"
                android:duration="80"
                android:valueTo="1.0"
                android:valueType="floatType" />
            <objectAnimator
                android:propertyName="translationZ"
                android:duration="80"
                android:valueTo="0dp"
                android:valueType="floatType" />
            <objectAnimator
                android:propertyName="alpha"
                android:duration="200"
                android:valueTo="1.0"
                android:valueType="floatType" />
        </set>
    </item>
</selector>

你想想看,如果没有 StateListAnimator,你要怎么写?监听 onTouchListener,在 ACTION_DOWN 时启动缩小动画,在 ACTION_UP 时启动还原动画。还要处理焦点变化、禁用状态……代码会变得很乱。

StateListAnimator 把这些逻辑全部封装起来了。你只需要声明「状态 -> 动画」的映射,剩下的交给系统。

12.6 性能与注意事项

要点 说明
动画时长 按钮动画建议控制在 80-150ms。太短没感觉,太长显得拖沓。
属性选择 尽量用 scale、translationZ、alpha 这些轻量属性。不要用 layout 参数。
硬件加速 StateListAnimator 默认走硬件加速,性能不用担心。
兼容性 API 21+ 才支持。如果你要兼容更低版本,可以用第三方库或者自己写 View 封装。
重复定义 同一个 View 不要同时设置 stateListAnimator 和 animate() 的持续动画。

我个人建议:把 StateListAnimator 当作按钮交互的标配。它比手写触摸监听更简洁,比用布局动画更精准。尤其是做自定义 View 的时候,加上 StateListAnimator 能让交互质感提升一个档次。

12.7 知识体系图

下面这张图帮你理清 StateListAnimator 在整个动画体系中的位置:

StateListAnimator View 状态变化 Animator 定义 状态-动画映射 scaleX / scaleY translationZ alpha / rotation 实战:按钮 pressed / focused / disabled

从图里可以看到,StateListAnimator 处于中间调度层。左边是输入(状态 + 动画定义),右边是输出(具体属性变化),底部是实战场景。你只要把左边定义好,右边自然就有了。


好了,这一章的内容就到这里。StateListAnimator 是个小而美的工具,用好了能让你的按钮交互质感提升不少。下一章我们会聊聊更复杂的动画编排,到时候见。

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