6、View动画之动画集合:AnimationSet与AnimatorSet

动画集合,说白了就是把多个动画打包在一起执行。你想想看,一个按钮要同时缩放、旋转、淡出,总不能写三遍start()吧?这时候就需要AnimationSet和AnimatorSet登场了。

我刚开始做Android开发那会儿,经常被这两个名字搞混。后来踩了几个坑才明白——它们虽然都叫"Set",但一个是View动画时代的产物,一个是属性动画时代的升级版。今天咱们就把它们彻底捋清楚。

AnimationSet:老派动画的打包方式

AnimationSet是View动画体系里的集合类。它继承自Animation,可以容纳多个子动画。用法很简单:

AnimationSet set = new AnimationSet(true); // true表示共享插值器
AlphaAnimation alpha = new AlphaAnimation(1.0f, 0.3f);
ScaleAnimation scale = new ScaleAnimation(1, 1.5f, 1, 1.5f);
TranslateAnimation translate = new TranslateAnimation(0, 200, 0, 0);

set.addAnimation(alpha);
set.addAnimation(scale);
set.addAnimation(translate);
set.setDuration(1000);
view.startAnimation(set);

这里有个关键点——共享插值器。构造函数里的true,意味着所有子动画共用同一个插值器。如果传false,每个动画可以单独设置自己的插值器。

我曾经踩过的坑:AnimationSet的duration设置会覆盖子动画的duration。如果你给每个子动画分别设了不同时长,最后发现全都不生效——因为AnimationSet的duration优先级更高。解决办法是不要在子动画上单独设duration,统一在set上设置。

AnimationSet的并行与顺序

AnimationSet默认是并行执行的。所有子动画同时开始,同时结束(受duration控制)。

那如果想顺序执行呢?抱歉,AnimationSet本身不支持。我当时的做法是手动设置startOffset:

AnimationSet set = new AnimationSet(false);
AlphaAnimation alpha = new AlphaAnimation(1.0f, 0.5f);
alpha.setDuration(500);

TranslateAnimation translate = new TranslateAnimation(0, 200, 0, 0);
translate.setDuration(500);
translate.setStartOffset(500); // 等alpha执行完再开始

set.addAnimation(alpha);
set.addAnimation(translate);
view.startAnimation(set);

说白了就是硬算时间偏移。这种方式在小规模动画里还行,一旦动画多了,维护起来简直要命。

AnimatorSet:属性动画的编排利器

到了属性动画时代,Google终于给了我们一个正经的编排工具——AnimatorSet。它比AnimationSet强大得多,支持真正的顺序、并行、延迟控制。

ObjectAnimator alpha = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f);
ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 2f);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 2f);

AnimatorSet set = new AnimatorSet();
set.play(alpha).with(scaleX).with(scaleY); // 并行执行
set.setDuration(800);
set.start();

看到没?play().with()这种链式调用,语义清晰多了。我个人习惯用这种方式组织复杂动画,代码可读性高,后期改起来也方便。

AnimatorSet的核心API

方法 作用 示例
play(Animator) 指定一个动画作为"锚点" set.play(anim1)
.with(Animator) 与锚点动画并行执行 .with(anim2)
.before(Animator) 在锚点动画之前执行 .before(anim3)
.after(Animator) 在锚点动画之后执行 .after(anim4)
.after(long) 延迟指定毫秒后执行 .after(500)

这四种组合方式,基本覆盖了所有编排需求。我举个例子:

ObjectAnimator fadeIn = ObjectAnimator.ofFloat(view, "alpha", 0f, 1f);
ObjectAnimator moveRight = ObjectAnimator.ofFloat(view, "translationX", 0f, 300f);
ObjectAnimator rotate = ObjectAnimator.ofFloat(view, "rotation", 0f, 360f);

AnimatorSet set = new AnimatorSet();
set.play(fadeIn).before(moveRight); // 先淡入,再移动
set.play(rotate).with(moveRight);   // 移动的同时旋转
set.setDuration(600);
set.start();

这个效果就是:按钮先淡入,然后边旋转边向右移动。逻辑非常清晰,对吧?

动画顺序与并行的实战场景

我在项目中做过一个登录成功的动画:头像先放大,然后弹出一个对勾,最后整个卡片淡出。用AnimatorSet实现是这样的:

ObjectAnimator avatarScale = ObjectAnimator.ofFloat(avatar, "scaleX", 1f, 1.2f);
ObjectAnimator avatarScaleY = ObjectAnimator.ofFloat(avatar, "scaleY", 1f, 1.2f);
ObjectAnimator checkMark = ObjectAnimator.ofFloat(checkView, "alpha", 0f, 1f);
ObjectAnimator cardFade = ObjectAnimator.ofFloat(cardView, "alpha", 1f, 0f);

AnimatorSet set = new AnimatorSet();
set.play(avatarScale).with(avatarScaleY); // 头像放大(并行)
set.play(checkMark).after(500);           // 0.5秒后弹出对勾
set.play(cardFade).after(1000);           // 1秒后卡片淡出
set.setDuration(400);
set.start();

这里有个小技巧:after(500)after(1000)是相对于动画开始时间的绝对偏移,不是相对于上一个动画结束。如果你想实现"上一个动画结束后再开始下一个",需要用play(anim2).after(anim1)这种形式。

我个人习惯:在写复杂动画序列时,先用纸笔画个时间轴,标清楚每个动画的起止时间。然后对照着写代码,不容易乱。特别是超过5个动画的组合,光靠脑子想很容易漏掉某个偏移量。

AnimationSet vs AnimatorSet:怎么选?

这个问题其实很简单:能用AnimatorSet就别用AnimationSet。除非你还在维护老项目,或者动画内容非常简单(比如只做透明度+位移)。

为什么?因为AnimationSet有几个硬伤:

  • 只能做View动画,不能改变真实属性
  • 不支持真正的顺序控制,全靠startOffset硬算
  • 动画结束后View会回弹到原始位置(除非设置fillAfter)
  • 性能上不如属性动画(View动画会触发频繁的invalidate)

AnimatorSet就没有这些问题。它操作的是对象的真实属性,动画结束后属性值就保留在那里。而且配合PropertyValuesHolder,还能在一个动画里同时改变多个属性:

PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat("translationX", 0f, 200f);
PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat("translationY", 0f, 100f);
PropertyValuesHolder pvhAlpha = PropertyValuesHolder.ofFloat("alpha", 1f, 0.5f);

ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(view, pvhX, pvhY, pvhAlpha);
anim.setDuration(800);
anim.start();

你看,一个ObjectAnimator就能搞定位移+透明度变化,连AnimatorSet都省了。

SVG:动画集合知识体系

Android动画集合知识体系 动画集合 AnimationSet(View动画) 并行执行(默认) startOffset模拟顺序 AnimatorSet(属性动画) play().with() 并行 play().before() 在前 play().after() 在后 结论:优先使用 AnimatorSet,更灵活、更强大

避坑指南

最后分享几个我实际踩过的坑:

  • AnimatorSet的复用问题:一个AnimatorSet执行完后,如果想再次start(),需要调用set.clone()或者重新创建。直接复用会导致动画状态异常。我因为这个bug排查了半天。
  • AnimationSet的fillAfter:如果你希望动画结束后View保持在最终状态,记得设置setFillAfter(true)。但要注意,这只是视觉上的保留,View的点击区域还是在原位置。
  • 性能考量:不要在AnimatorSet里塞太多ObjectAnimator,特别是同时操作多个View。建议控制在10个以内,否则掉帧明显。如果动画复杂,可以考虑用ViewPropertyAnimator或者Compose动画。

嗯,动画集合这块内容就这些。说白了就是学会用工具把零散的动画组织起来,让它们按照你想要的节奏执行。掌握了AnimationSet和AnimatorSet,大部分UI动画需求都能搞定。

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