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 处于中间调度层。左边是输入(状态 + 动画定义),右边是输出(具体属性变化),底部是实战场景。你只要把左边定义好,右边自然就有了。
好了,这一章的内容就到这里。StateListAnimator 是个小而美的工具,用好了能让你的按钮交互质感提升不少。下一章我们会聊聊更复杂的动画编排,到时候见。