一、动画基础与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变换矩阵。

Android动画体系结构 Android动画体系 View动画(补间动画) 属性动画 Compose动画 Alpha/Scale Translate/Rotate ValueAnimator ObjectAnimator animate*AsState Animatable API Level 1+ API Level 11+ API Level 30+ (Compose)

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