一、动画基础与View动画初探
说实话,Android动画体系刚接触时确实有点乱。我记得自己刚入行那会儿,对着Animation、Animator、AnimatorSet这几个类名绕了半天。今天咱们就从最基础的View动画开始,把这块地基打牢。
1.1 Android动画体系概述
Android的动画体系,说白了就三大块:
- View动画(Tween Animation):老牌动画方案,基于View的变换
- 属性动画(Property Animation):API 11引入,真正改变对象属性
- Compose动画:新一代声明式动画,Jetpack Compose专属
我个人习惯把View动画叫做"轻量级动画"。为什么?因为它只改变View的绘制效果,不改变View的实际位置。你想想看,一个按钮做了平移动画,点击事件还在原地——这就是View动画的典型特征。
核心要点:View动画本质是矩阵变换,通过Matrix对View进行平移、缩放、旋转、透明度变化。它不修改View的LayoutParams,只改变绘制时的Canvas变换矩阵。
1.2 View动画核心原理
View动画的核心,其实就是四个变换:
| 动画类型 | XML标签 | 变换矩阵操作 | 典型应用场景 |
|---|---|---|---|
| 透明度 | <alpha> | Alpha矩阵 | 淡入淡出效果 |
| 缩放 | <scale> | Scale矩阵 | 放大缩小、弹跳效果 |
| 平移 | <translate> | Translate矩阵 | 滑动入场、轮播切换 |
| 旋转 | <rotate> | Rotate矩阵 | 加载旋转、翻转效果 |
嗯,这里要注意一个关键点:View动画的变换是"假象"。我在项目中遇到过这样一个坑——给一个按钮做了平移动画,结果点击事件还在原地。用户明明看到按钮在右边,点下去却触发不了。这就是View动画只改绘制不改布局的典型问题。
避坑指南:我曾经在做一个登录页面的"忘记密码"按钮动画时,用了TranslateAnimation把按钮移到了右侧。结果QA测试发现点击无效。排查了半天,才发现View动画不改变触摸区域。后来改用属性动画才解决。
1.3 XML定义与代码实现
我个人习惯把动画定义写在XML里,这样复用性更好。来看一个完整的例子:
1.3.1 XML方式定义动画
在 res/anim/ 目录下创建动画文件:
<!-- res/anim/fade_in.xml -->
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true"
android:interpolator="@android:anim/accelerate_decelerate_interpolator">
<alpha
android:fromAlpha="0.0"
android:toAlpha="1.0"
android:duration="800" />
<scale
android:fromXScale="0.8"
android:toXScale="1.0"
android:fromYScale="0.8"
android:toYScale="1.0"
android:pivotX="50%"
android:pivotY="50%"
android:duration="800" />
</set>
然后在代码中加载:
val animation = AnimationUtils.loadAnimation(this, R.anim.fade_in)
imageView.startAnimation(animation)
1.3.2 代码方式创建动画
有时候需要动态控制参数,那就用代码创建。你想想看,如果动画的起始位置要根据屏幕宽度计算,XML就搞不定了:
val translateAnim = TranslateAnimation(
Animation.RELATIVE_TO_SELF, 0f, // fromX
Animation.RELATIVE_TO_SELF, 0.5f, // toX
Animation.RELATIVE_TO_SELF, 0f, // fromY
Animation.RELATIVE_TO_SELF, 0f // toY
).apply {
duration = 600
fillAfter = true
interpolator = OvershootInterpolator()
}
view.startAnimation(translateAnim)
小技巧:我个人习惯用 Animation.RELATIVE_TO_SELF 做相对自身的动画,用 Animation.RELATIVE_TO_PARENT 做相对父容器的动画。比如"从屏幕外滑入"这种效果,用 RELATIVE_TO_PARENT 配合 1.0f 就很方便。
1.4 动画监听器
动画监听器是实际开发中绕不开的。说白了,就是让你知道动画什么时候开始、结束、重复。来看一个完整的监听示例:
animation.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation?) {
// 动画开始:可以在这里禁用点击,防止用户重复触发
Log.d("Animation", "动画开始了")
button.isEnabled = false
}
override fun onAnimationEnd(animation: Animation?) {
// 动画结束:恢复点击,或者启动下一个动画
Log.d("Animation", "动画结束了")
button.isEnabled = true
// 可以在这里启动链式动画
startNextAnimation()
}
override fun onAnimationRepeat(animation: Animation?) {
// 动画重复:比如无限旋转的加载动画,每次循环可以更新UI
Log.d("Animation", "动画重复了")
updateProgress()
}
})
我记得有一次做引导页的动画序列,需要三个动画依次播放。当时就是用 onAnimationEnd 来触发下一个动画,形成了一个链式调用。效果还不错,就是代码有点嵌套——后来用 AnimatorSet 重构了。
重要提醒:View动画的 onAnimationEnd 回调有个坑——如果动画被取消,它也会被调用。所以判断动画是否真正结束,最好加一个标志位。我曾经因为这个bug,导致动画结束后重复执行了两次后续逻辑。
1.5 实战:组合动画与参数详解
实际项目中很少只用单一动画,基本都是组合拳。来看一个"卡片翻转"效果的XML定义:
<!-- res/anim/card_flip.xml -->
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<rotate
android:fromDegrees="0"
android:toDegrees="360"
android:pivotX="50%"
android:pivotY="50%"
android:duration="1000"
android:interpolator="@android:anim/accelerate_decelerate_interpolator" />
<alpha
android:fromAlpha="1.0"
android:toAlpha="0.3"
android:duration="500"
android:startOffset="250" />
<alpha
android:fromAlpha="0.3"
android:toAlpha="1.0"
android:duration="500"
android:startOffset="750" />
</set>
这里有几个参数值得注意:
- startOffset:延迟启动时间,用来做动画序列
- fillAfter:动画结束后保持最终状态
- fillBefore:动画开始前回到初始状态
- repeatMode:restart(重新开始)或 reverse(反向播放)
- interpolator:插值器,控制动画速率变化
嗯,说到插值器,我建议初学者先掌握这几个常用的:LinearInterpolator(匀速)、AccelerateDecelerateInterpolator(先加速后减速)、OvershootInterpolator(超过目标再回弹)。其他的用到再查也不迟。
个人经验:做弹窗动画时,我习惯用 OvershootInterpolator 配合 scale 动画,效果很自然。但注意 overshoot 的张力值别太大,否则看起来会很假。默认的 2.0 就够用。
最后说一句,View动画虽然"假",但胜在轻量、兼容性好。在不需要交互反馈的纯视觉效果场景下,它依然是很好的选择。比如启动页的Logo动画、列表项的入场动画,用View动画完全够用。
公众号:蓝海资料掘金营,微信deep3321