A
第 31 章JAVA45 分钟

动画与过渡

Android 动画体系:View 补间动画(alpha/scale/translate/rotate)、属性动画(ObjectAnimator/ValueAnimator/AnimatorSet)、Transition API 共享元素、MotionLayout 入门与动画性能注意事项。

学习目标

  • 掌握 View 补间动画四类与 XML 资源定义
  • 理解属性动画原理并熟练使用 ObjectAnimator 与 ValueAnimator
  • 能够用 AnimatorSet 组合动画
  • 了解 Transition API 与共享元素过渡
  • 了解 MotionLayout 入门用法与动画性能注意事项

学习目标

  • 掌握 View 补间动画四类与 XML 资源定义;
  • 理解属性动画原理并熟练使用 ObjectAnimator 与 ValueAnimator;
  • 能够用 AnimatorSet 组合动画;
  • 了解 Transition API 与共享元素过渡;
  • 了解 MotionLayout 入门用法与动画性能注意事项。

动画与过渡

Android 动画体系经过多年演进,目前主要分为三大类:View 补间动画(Tween Animation)、属性动画(Property Animation) 与 Transition / Motion。三者层次与适用场景不同,掌握它们的边界是写出流畅动画的前提。

三种动画体系对比

体系 引入版本 改变什么 是否影响真实属性 性能
View 补间动画 API 1 View 的绘制 否(仅视觉) 中
属性动画 API 11(Honeycomb) 对象属性 是 中高
Transition / Motion API 21 / MotionLayout 1.0 场景过渡 是 高

View 补间动画

补间动画通过给出“起始值”和“结束值”,由系统插值出中间帧。支持四种基本变换:alpha(透明度)、scale(缩放)、translate(平移)、rotate(旋转)。

XML 定义动画

res/anim/fade_in.xml:

<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromAlpha="0.0"
    android:toAlpha="1.0"
    android:duration="300"
    android:interpolator="@android:interpolator/accelerate_decelerate" />

res/anim/scale_in.xml:

<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromXScale="0.5"
    android:toXScale="1.0"
    android:fromYScale="0.5"
    android:toYScale="1.0"
    android:pivotX="50%"
    android:pivotY="50%"
    android:duration="300" />

res/anim/set_combo.xml:用 <set> 组合多种变换:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:shareInterpolator="true">

    <alpha
        android:fromAlpha="0.0"
        android:toAlpha="1.0"
        android:duration="300" />

    <translate
        android:fromXDelta="0%"
        android:toXDelta="0%"
        android:fromYDelta="-20%"
        android:toYDelta="0%"
        android:duration="300" />
</set>

代码加载与启动

public class FadeActivity extends AppCompatActivity {
    private ImageView imageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_fade);
        imageView = findViewById(R.id.iv_logo);

        Animation fadeIn = AnimationUtils.loadAnimation(this, R.anim.fade_in);
        imageView.startAnimation(fadeIn);
    }
}

也可以纯代码构建:

AlphaAnimation alpha = new AlphaAnimation(0f, 1f);
alpha.setDuration(300);
alpha.setInterpolator(new AccelerateDecelerateInterpolator());
imageView.startAnimation(alpha);

补间动画的局限

补间动画 只改变 View 的绘制,并不改变 View 的真实位置和属性:

  • 一个 Button 平移后,点击区域仍在原位置;
  • 不支持 3D 变换;
  • 不支持动画背景色等非绘制属性。

这就是属性动画被引入的原因。

属性动画

属性动画通过 不断调用对象 setter 改变属性真实值。核心类是 ValueAnimator(计算器)与 ObjectAnimator(自动 setter 调用器)。

ValueAnimator:手动取值

ValueAnimator 在指定时长内,按插值器和类型评估器,从起始值过渡到结束值:

ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
animator.setDuration(300);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.addUpdateListener(animation -> {
    float value = (float) animation.getAnimatedValue();
    imageView.setAlpha(value);
    imageView.setScaleX(value);
});
animator.start();

ObjectAnimator:自动绑定

ObjectAnimator 通过反射调用目标对象的 setter(如 setAlpha、setTranslationX),更简洁:

ObjectAnimator alpha = ObjectAnimator.ofFloat(imageView, "alpha", 0f, 1f);
alpha.setDuration(300);
alpha.start();

// 同时多个属性
ObjectAnimator scaleX = ObjectAnimator.ofFloat(imageView, "scaleX", 0.5f, 1f);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(imageView, "scaleY", 0.5f, 1f);

View 提供了 animate() 链式 API,背后就是 ObjectAnimator:

imageView.animate()
        .alpha(1f)
        .scaleX(1f)
        .scaleY(1f)
        .setDuration(300)
        .setInterpolator(new DecelerateInterpolator())
        .start();

PropertyValuesHolder:单对象多属性

PropertyValuesHolder pvhAlpha = PropertyValuesHolder.ofFloat("alpha", 0f, 1f);
PropertyValuesHolder pvhScaleX = PropertyValuesHolder.ofFloat("scaleX", 0.5f, 1f);
PropertyValuesHolder pvhScaleY = PropertyValuesHolder.ofFloat("scaleY", 0.5f, 1f);

ObjectAnimator.ofPropertyValuesHolder(imageView, pvhAlpha, pvhScaleX, pvhScaleY)
        .setDuration(300)
        .start();

这种方式比创建多个 ObjectAnimator 更高效,内部只注册一个动画回调。

插值器与估值器

  • 插值器(Interpolator):决定时间流逝比例到“动画进度”的映射。LinearInterpolator 匀速;AccelerateDecelerateInterpolator 两端慢中间快;DecelerateInterpolator 先快后慢;AnticipateOvershootInterpolator 超出再回弹。
  • 估值器(TypeEvaluator):决定从起始值到结束值的过渡算法。FloatEvaluator / IntEvaluator 是默认;ArgbEvaluator 用于颜色过渡:
ObjectAnimator colorAnim = ObjectAnimator.ofObject(view, "backgroundColor",
        new ArgbEvaluator(),
        Color.parseColor("#FF5722"),
        Color.parseColor("#3F51B5"));
colorAnim.setDuration(800);
colorAnim.start();

AnimatorSet:组合编排

AnimatorSet 用于将多个 Animator 按顺序或同时执行:

ObjectAnimator fadeOut = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f);
ObjectAnimator moveUp = ObjectAnimator.ofFloat(view, "translationY", 0f, -100f);
ObjectAnimator fadeIn = ObjectAnimator.ofFloat(view, "alpha", 0f, 1f);

AnimatorSet set = new AnimatorSet();
set.play(fadeOut).before(fadeIn);          // fadeOut 先于 fadeIn
set.playTogether(fadeOut, moveUp);          // fadeOut 和 moveUp 同时
set.setDuration(300);
set.start();

也可以用 playSequentially:

AnimatorSet set = new AnimatorSet();
set.playSequentially(fadeOut, fadeIn);
set.start();

监听器

animator.addListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
        Toast.makeText(MainActivity.this, "动画结束", Toast.LENGTH_SHORT).show();
    }
});

用 XML 定义属性动画

res/animator/scale_with_alpha.xml:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:ordering="together">
    <objectAnimator
        android:propertyName="alpha"
        android:duration="300"
        android:valueFrom="0"
        android:valueTo="1"
        android:valueType="floatType" />
    <objectAnimator
        android:propertyName="scaleX"
        android:duration="300"
        android:valueFrom="0.5"
        android:valueTo="1"
        android:valueType="floatType" />
</set>

加载:

AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(this, R.animator.scale_with_alpha);
set.setTarget(imageView);
set.start();

Transition API 与共享元素

Transition API(API 21+)用于描述“场景变化”,框架自动捕获前后两个 Scene 的属性差异并做动画。

Activity 过渡

启用过渡动画:

<!-- styles.xml -->
<style name="AppTheme" parent="Theme.Material3.DayNight">
    <item name="android:windowActivityTransitions">true</item>
</style>

在调用方 Activity 中:

// 给共享 View 设置 transitionName
View sharedView = findViewById(R.id.iv_avatar);
sharedView.setTransitionName("avatar");

Intent intent = new Intent(this, DetailActivity.class);
ActivityOptions options = ActivityOptions
        .makeSceneTransitionAnimation(this, sharedView, "avatar");
startActivity(intent, options.toBundle());

在被调用方 Activity 中,给对应的 View 设置相同的 transitionName:

<ImageView
    android:id="@+id/iv_avatar"
    android:transitionName="avatar"
    android:layout_width="200dp"
    android:layout_height="200dp" />

自定义 Transition

public class ChangeColor extends Transition {
    private static final String PROPNAME_BACKGROUND = "android:changeColor:background";

    @Override
    public void captureStartValues(TransitionValues values) {
        values.values.put(PROPNAME_BACKGROUND,
                ((View) values.view).getBackground());
    }

    @Override
    public void captureEndValues(TransitionValues values) {
        values.values.put(PROPNAME_BACKGROUND,
                ((View) values.view).getBackground());
    }

    @Override
    public Animator createAnimator(ViewGroup sceneRoot, TransitionValues start, TransitionValues end) {
        if (start == null || end == null) return null;
        // 简化示例:背景色 Argb 过渡
        return ObjectAnimator.ofObject((View) end.view, "backgroundColor",
                new ArgbEvaluator(), Color.RED, Color.BLUE);
    }
}

MotionLayout 入门

MotionLayout 是 ConstraintLayout 子类,专为“复杂状态过渡”设计。它通过一个布局文件 + 一个场景文件(MotionScene)描述两个状态之间的动画。

依赖

dependencies {
    implementation "androidx.constraintlayout:constraintlayout:2.1.4"
}

布局文件

res/layout/motion_scene_start.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/motion_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/motion_scene">

    <ImageView
        android:id="@+id/iv_ball"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:src="@drawable/ic_ball"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.motion.widget.MotionLayout>

场景文件

res/xml/motion_scene.xml:

<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <ConstraintSet android:id="@+id/start">
        <Constraint android:id="@id/iv_ball">
            <Transform
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintStart_toStartOf="parent" />
        </Constraint>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint android:id="@id/iv_ball">
            <Transform
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent" />
        </Constraint>
    </ConstraintSet>

    <Transition
        app:constraintSetStart="@id/start"
        app:constraintSetEnd="@id/end"
        app:duration="500"
        app:motionInterpolator="easeInOut">
        <OnClick app:targetId="@id/iv_ball"
            app:clickAction="toggle" />
    </Transition>
</MotionScene>

代码控制过渡:

MotionLayout motionLayout = findViewById(R.id.motion_layout);
// 跳到 end 状态
motionLayout.transitionToEnd();
// 跳回 start 状态
motionLayout.transitionToStart();

性能注意事项

动画是性能问题的重灾区。以下几个原则务必遵守:

  1. 优先硬件层:复杂动画对目标 View 调用 setLayerType(View.LAYER_TYPE_HARDWARE, null)(或 view.animate().withLayer()),让 GPU 缓存绘制结果。动画结束后关闭,避免额外内存:
imageView.animate()
        .alpha(0f)
        .scaleX(0.5f)
        .scaleY(0.5f)
        .setDuration(300)
        .withLayer()  // 期间使用硬件层
        .withEndAction(() -> {
            // 完成后清理
        });
  1. 避免每帧布局:translationX/Y、scaleX/Y、rotationX/Y、alpha 走的是 GPU 合成,不会触发重新布局(onMeasure / onLayout);而改 width、height、margin 会导致每帧布局,性能差很多。能用 translationX 就不要改 left/right。
  2. 慎用 requestLayout / invalidate:自定义 View 动画中频繁触发重新测量会导致掉帧。
  3. 节流属性动画监听:addUpdateListener 里只更新必要的 View,不要在每帧分配新对象。
  4. 不要在动画中做 IO:数据库、网络操作都不应在动画回调中执行。

常见坑与最佳实践

  1. 补间动画后点击位置不变:补间动画不改变真实位置。需要响应新位置的应用 ObjectAnimator.ofFloat(view, "translationX", ...) 或直接改 x/y。
  2. ObjectAnimator setter 不存在:反射找不到 setter 会静默失效。给自定义属性务必提供 setName 方法,或使用 PropertyValuesHolder.ofObject + TypeEvaluator 配合 View.set*。
  3. animation 没设置插值器时按线性:人眼对线性动画感知生硬,默认建议 DecelerateInterpolator 或 AccelerateDecelerateInterpolator。
  4. 硬件层未释放:LAYER_TYPE_HARDWARE 会占用 GPU 内存。短暂动画结束后应恢复 LAYER_TYPE_NONE,除非该 View 持续有动画。
  5. MotionScene 中属性覆盖了 layout:MotionLayout 的 ConstraintSet 中约束会覆盖原 layout 中的 layout_* 属性,让人误以为布局没生效。两者要协调一致。
  6. 大量动画共用一个监听器分配大对象:在 addUpdateListener 内 new 对象会造成 GC 抖动,应预先创建并复用。
  7. 在 onPause 中忘记取消动画:Activity 销毁时仍在跑的 Animator 应调用 cancel(),避免持有 Context 导致泄漏。
  8. 对 BackgroundColor 用补间动画:补间动画不支持颜色变化,必须用属性动画 + ArgbEvaluator。

章节小结

本章覆盖了 Android 三大动画体系:View 补间动画(alpha/scale/translate/rotate,仅改变绘制)、属性动画(ValueAnimator / ObjectAnimator / AnimatorSet,真实修改属性、性能与表达力更强)、Transition 与共享元素过渡(API 21+ 场景驱动动画)、MotionLayout 入门(ConstraintLayout 子类,用 MotionScene 描述状态过渡)。性能要点:优先 GPU 友好属性(translation*、scale*、rotation*、alpha),合理使用硬件层,避免每帧布局与对象分配。

下一章预告

下一章将进入深色模式与主题领域:Material 主题、颜色资源定义、DayNight 主题(Theme.Material3.DayNight)、Force Dark、Android 12+ 动态颜色(Dynamic Color),以及主题切换的持久化方案。