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,看起来就像卡片在飞出去。

我的习惯:如果同时改变的属性超过两个,我优先用PropertyValuesHolder。代码更紧凑,而且动画的插值器、时长、监听器都统一管理,不容易出错。

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。有点像弹簧的感觉。

我曾经踩过的坑:Keyframe的时间值必须从0到1,而且最好包含0和1两个端点。少写了端点,动画可能会从奇怪的位置开始。另外,插值器只对当前帧到下一帧生效,不是全局的。

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()。

我的建议:只要动画不涉及自定义属性,不涉及Keyframe,不涉及复杂的AnimatorSet编排,一律用ViewPropertyAnimator。代码量减少一半,可读性提升一倍。

5.5 知识体系总览

下面这张图帮你理清这三个工具的关系和适用场景:

属性动画进阶工具对比 PropertyValuesHolder 多属性并行修改 • 同时改变多个属性 • 代码紧凑统一管理 • 适合2个以上属性 • 可配合Keyframe使用 推荐指数:★★★★☆ AnimatorSet 复杂动画序列编排 • play().with() 同时 • play().before() 顺序 • play().after() 延迟 • 支持嵌套组合 推荐指数:★★★★★ ViewPropertyAnimator 最简洁的动画写法 • 链式调用一行搞定 • 内部性能优化 • 适合简单View动画 • 不支持自定义属性 推荐指数:★★★★★ 选择建议:简单View动画 → ViewPropertyAnimator | 多属性并行 → PropertyValuesHolder | 复杂序列 → AnimatorSet

5.6 实际项目中的选择策略

说了这么多,到底什么时候用哪个?我总结了一个简单的判断逻辑:

场景 推荐方案 理由
单个View的平移动画 ViewPropertyAnimator 代码最少,性能最优
同时改变3个以上属性 PropertyValuesHolder 避免创建多个ObjectAnimator
动画需要中间回弹效果 Keyframe 精确控制时间线上的每个点
A动画结束后执行B动画 AnimatorSet 语义清晰,支持嵌套
自定义属性的动画 ObjectAnimator ViewPropertyAnimator不支持

避坑指南:我曾经在一个列表动画中混用了ViewPropertyAnimator和ObjectAnimator,结果发现动画冲突——一个在修改translationX,另一个也在改,导致View跳来跳去。后来我统一用PropertyValuesHolder管理所有属性变化,问题就解决了。记住:同一个属性不要同时被两个动画对象控制。

好了,这三个工具你心里应该有数了。实际开发中,我通常这样搭配:简单交互用ViewPropertyAnimator,复杂效果用PropertyValuesHolder+Keyframe,多动画协同用AnimatorSet。灵活组合,基本能覆盖99%的动画需求。

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