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的并行与顺序
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)这种形式。
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:动画集合知识体系
避坑指南
最后分享几个我实际踩过的坑:
- AnimatorSet的复用问题:一个AnimatorSet执行完后,如果想再次start(),需要调用
set.clone()或者重新创建。直接复用会导致动画状态异常。我因为这个bug排查了半天。 - AnimationSet的fillAfter:如果你希望动画结束后View保持在最终状态,记得设置
setFillAfter(true)。但要注意,这只是视觉上的保留,View的点击区域还是在原位置。 - 性能考量:不要在AnimatorSet里塞太多ObjectAnimator,特别是同时操作多个View。建议控制在10个以内,否则掉帧明显。如果动画复杂,可以考虑用ViewPropertyAnimator或者Compose动画。
嗯,动画集合这块内容就这些。说白了就是学会用工具把零散的动画组织起来,让它们按照你想要的节奏执行。掌握了AnimationSet和AnimatorSet,大部分UI动画需求都能搞定。