5、属性动画进阶:PropertyValuesHolder与Keyframe、AnimatorSet编排复杂动画、ViewPropertyAnimator便捷用法
属性动画学到这里,你已经掌握了基础用法。但说实话,真正做项目时,很少只让一个属性做匀速直线运动。更多时候,我们需要同时改变多个属性,或者让动画按特定顺序执行。今天我就把这三个进阶工具掰开揉碎讲清楚。
5.1 PropertyValuesHolder:多属性并行利器
先问个问题:你想让一个View同时平移、旋转、缩放,怎么做?
最笨的办法是写三个ObjectAnimator,然后分别start()。但这样代码冗余,而且不好管理。PropertyValuesHolder就是专门解决这个问题的。
val translationX = PropertyValuesHolder.ofFloat("translationX", 0f, 300f)
val rotation = PropertyValuesHolder.ofFloat("rotation", 0f, 360f)
val scaleX = PropertyValuesHolder.ofFloat("scaleX", 1f, 1.5f)
val animator = ObjectAnimator.ofPropertyValuesHolder(view, translationX, rotation, scaleX)
animator.duration = 2000
animator.start()
你看,一个动画对象就搞定了三个属性的变化。我在项目中经常用它做卡片翻转效果——同时改变rotationY和translationX,看起来就像卡片在飞出去。
5.2 Keyframe:精确控制动画节奏
PropertyValuesHolder虽然好用,但它只能控制起点和终点。如果我想让动画先快后慢再快,或者中间某个时刻做特殊处理,怎么办?
Keyframe就是干这个的。它允许你在动画时间线上设置多个关键帧,每个帧可以指定不同的值和插值器。
val frame0 = Keyframe.ofFloat(0f, 0f)
val frame1 = Keyframe.ofFloat(0.5f, 100f)
val frame2 = Keyframe.ofFloat(0.8f, 80f) // 先冲过头再回弹
val frame3 = Keyframe.ofFloat(1f, 200f)
// 给中间帧设置不同的插值器
frame1.interpolator = AccelerateInterpolator()
frame2.interpolator = DecelerateInterpolator()
val holder = PropertyValuesHolder.ofKeyframe("translationX", frame0, frame1, frame2, frame3)
val animator = ObjectAnimator.ofPropertyValuesHolder(view, holder)
animator.duration = 3000
animator.start()
这段代码的效果是:前半段加速冲到100,然后稍微回弹到80,最后减速到达200。有点像弹簧的感觉。
5.3 AnimatorSet:编排复杂动画序列
单个动画再花哨,也解决不了多动画协同的问题。比如:先让卡片翻转,翻转完成后弹出详情,同时背景渐变。这种场景,AnimatorSet就是标准答案。
val flipAnim = ObjectAnimator.ofFloat(cardView, "rotationY", 0f, 180f).apply {
duration = 600
}
val detailAnim = ObjectAnimator.ofFloat(detailView, "alpha", 0f, 1f).apply {
duration = 400
}
val bgAnim = ObjectAnimator.ofFloat(backgroundView, "alpha", 1f, 0.6f).apply {
duration = 1000
}
AnimatorSet().apply {
play(flipAnim).before(detailAnim) // 翻转完成后弹出详情
play(bgAnim).with(flipAnim) // 背景渐变和翻转同时进行
start()
}
AnimatorSet提供了三种编排方式:play().with()同时执行,play().before()顺序执行,play().after()延迟执行。组合起来几乎能实现任何动画序列。
实际项目经验:我做列表展开动画时,用AnimatorSet把每个item的展开动画错开100毫秒。效果就像多米诺骨牌一样,视觉上非常流畅。关键代码就一行:play(item2Anim).after(100)。
5.4 ViewPropertyAnimator:最简洁的动画写法
如果你只是对单个View做简单动画,比如平移、缩放、透明度变化,那ViewPropertyAnimator绝对是最爽的API。没有之一。
view.animate()
.translationX(300f)
.rotation(360f)
.scaleX(1.5f)
.alpha(0.5f)
.setDuration(2000)
.setInterpolator(AccelerateDecelerateInterpolator())
.withStartAction { /* 动画开始前的准备 */ }
.withEndAction { /* 动画结束后的回调 */ }
.start()
看到没?链式调用,一行到底。而且它内部自动优化了性能——多个属性变化会合并到同一个帧刷新中,不会重复触发invalidate()。
5.5 知识体系总览
下面这张图帮你理清这三个工具的关系和适用场景:
5.6 实际项目中的选择策略
说了这么多,到底什么时候用哪个?我总结了一个简单的判断逻辑:
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 单个View的平移动画 | ViewPropertyAnimator | 代码最少,性能最优 |
| 同时改变3个以上属性 | PropertyValuesHolder | 避免创建多个ObjectAnimator |
| 动画需要中间回弹效果 | Keyframe | 精确控制时间线上的每个点 |
| A动画结束后执行B动画 | AnimatorSet | 语义清晰,支持嵌套 |
| 自定义属性的动画 | ObjectAnimator | ViewPropertyAnimator不支持 |
避坑指南:我曾经在一个列表动画中混用了ViewPropertyAnimator和ObjectAnimator,结果发现动画冲突——一个在修改translationX,另一个也在改,导致View跳来跳去。后来我统一用PropertyValuesHolder管理所有属性变化,问题就解决了。记住:同一个属性不要同时被两个动画对象控制。
好了,这三个工具你心里应该有数了。实际开发中,我通常这样搭配:简单交互用ViewPropertyAnimator,复杂效果用PropertyValuesHolder+Keyframe,多动画协同用AnimatorSet。灵活组合,基本能覆盖99%的动画需求。