AnimatorSet(android-35)
前言
在看源码之前是没想到AnimatorSet这么复杂的,这就是看源码的乐趣吧,当然也比较累。这个复杂的设计带来的结果就是很强的通用性,能够编排复杂的动画逻辑。想想也挺搞笑的,之前为了多段动画自己写了个动画控制器,简直多余。。。
1. 概要设计
功能
AnimatorSet 是 Android 属性动画系统中的动画编排器 ,负责协调多个 Animator 的播放顺序。它支持:
- 并行播放:多个动画同时开始
- 顺序播放:一个动画结束后下一个才开始
- 延迟播放 :通过
after(delay)或startDelay延后启动 - 混合编排 :通过
Builder灵活组合上述关系
框架设计
Animator (abstract)
│
├── ValueAnimator ← 核心时序引擎
│ └── ObjectAnimator ← 自动赋值
│
└── AnimatorSet ← 动画编排器(本类)
│
│ 持有 mNodes (Node 列表),每个 Node 封装一个子 Animator
│ 通过 DAG 依赖图管理播放顺序
│ 通过 mEvents 事件列表驱动帧调度
│ 通过 mPlayingSet 跟踪当前活跃的子动画
│ 自身注册到 AnimationHandler 接收 vsync 帧回调
│ 每帧通过 pulseAnimationFrame() 驱动子动画
│
└── 实现 AnimationHandler.AnimationFrameCallback
AnimatorSet 自身不计算动画值 ,仅负责调度:决定何时 start/end 子动画、每帧向子动画注入 playTime。子动画通过 startWithoutPulsing() 启动(不自行注册帧回调),由 AnimatorSet 统一 pulse。
2. 对外接口(Builder 模式)
2.1 便捷方法
| 方法 | 说明 |
|---|---|
playTogether(Animator...) |
所有动画同时开始 |
playSequentially(Animator...) |
依次播放,前一个结束才启动下一个 |
2.2 Builder API
java
AnimatorSet set = new AnimatorSet();
set.play(anim1).with(anim2); // anim1 和 anim2 同时播放
set.play(anim2).before(anim3); // anim2 结束后 anim3 开始
set.play(anim4).after(anim3); // anim3 结束后 anim4 开始(等价于 play(anim3).before(anim4))
set.play(anim5).after(1000); // 延迟 1000ms 后 anim5 开始
2.3 Builder 方法详解
| 方法 | 语义 | 建立的关系 |
|---|---|---|
play(anim) |
创建 Builder,指定基准动画 | 返回 Builder |
with(anim) |
与基准动画同时播放 | sibling 关系(双向) |
before(anim) |
基准动画结束后 anim 播放 | child 关系(基准 → anim) |
after(anim) |
anim 结束后基准动画播放 | parent 关系(anim → 基准) |
after(delay) |
延迟 delay ms 后基准动画播放 | 创建临时 ValueAnimator 作为 parent |
注意 :play(a1).before(a2).before(a3) 表示 a2 和 a3 都在 a1 结束后开始,a2 和 a3 之间无依赖。
2.4 通用设置
| 方法 | 说明 |
|---|---|
setDuration(long) |
设置所有子动画的时长 |
setStartDelay(long) |
设置 AnimatorSet 自身的延迟 |
setInterpolator(TimeInterpolator) |
设置所有子动画的插值器 |
setTarget(Object) |
设置所有 ObjectAnimator 子动画的目标对象 |
setCurrentPlayTime(long) |
Seek 到指定时间 |
pause() / resume() |
暂停/恢复 |
cancel() / end() |
取消/结束 |
reverse() |
反向播放 |
3. 详细设计
3.1 实现原理与机制
┌─────────────────────────────────────────────────────────────────────┐
│ AnimatorSet │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Node A │───→│ Node B │───→│ Node C │ │
│ │ (anim1) │ │ (anim2) │ │ (anim3) │ │
│ │ mStartTime │ │ mStartTime │ │ mStartTime │ │
│ │ mEndTime │ │ mEndTime │ │ mEndTime │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ mEvents (按时间排序的事件列表,注意排序后不是这样规则的): │
│ [Root.START, Root.DELAY_END, Root.END, │
│ A.START, A.DELAY_END, A.END, │
│ B.START, B.DELAY_END, B.END, │
│ C.START, C.DELAY_END, C.END] │
│ │
│ mPlayingSet (当前活跃的子动画): │
│ [Node A, Node B] ← 正在播放的动画 │
│ │
│ mLastEventId: 指向 mEvents 中已处理到的位置 │
│ mFirstFrame: 首帧时间(含 startDelay 基准) │
└─────────────────────────────────────────────────────────────────────┘
核心机制:
- DAG 依赖图:Node 间的 parent-child 关系构成有向无环图,决定播放顺序
- 事件列表 :将所有 Node 的 START/DELAY_ENDED/END 事件按时间排序,运行时通过
findLatestEventIdForTime增量处理 - 帧注入 :AnimatorSet 注册到 AnimationHandler,每帧收到 frameTime 后计算 unscaledPlayTime,通过
pulseAnimationFrame注入到 mPlayingSet 中的子动画 - 子动画不自行 pulse :子动画通过
startWithoutPulsing()启动,mSelfPulse = false,不注册自己的帧回调
3.2 通用设置的作用
setDuration(long)
java
// 设置所有子动画的时长(不影响 mDelayAnim)
private void updateAnimatorsDuration() {
if (mDuration >= 0) {
for (node : mNodes) node.mAnimation.setDuration(mDuration);
}
mDelayAnim.setDuration(mStartDelay); // delay 动画时长 = AnimatorSet 的 startDelay
}
mDuration = -1(默认)表示不覆盖子动画自身的时长。
setCurrentPlayTime(long)
Seek 到指定时间,内部调用 animateBasedOnPlayTime 计算所有子动画在该时刻的值。未 start 时 seek 仅设置值不启动播放,start 后首帧通过 SeekState 对齐。
setStartDelay(long)
设置 AnimatorSet 自身的延迟。通过 mDelayAnim(一个 0 时长的 ValueAnimator)作为 Root Node 的动画实现。mRootNode.mEndTime = mStartDelay,所有顶层子动画的 mStartTime = mStartDelay。
3.3 初始化流程
start()
└─ initAnimation()
├─ updateAnimatorsDuration() ← 设置子动画时长
└─ createDependencyGraph() ← 构建 DAG + 事件列表
├─ 1. 传播 sibling 的 parents(findSiblings 递归)
├─ 2. 为无 parent 的 Node 添加 mRootNode 作为 parent
├─ 3. updatePlayTime(mRootNode, visited) ← DFS 计算每个 Node 的 start/end 时间
└─ 4. sortAnimationEvents() ← 构建排序的 mEvents 列表
3.3.1 DAG 依赖图构建
图示:Builder API 到 DAG 的映射
代码: play(A).with(B).before(C).after(D)
Builder 操作:
play(A) → 创建 Builder, 基准 = A
.with(B) → A.siblings += B, B.siblings += A
.before(C) → A.childNodes += C, C.parents += A
.after(D) → A.parents += D, D.childNodes += A
DAG 结构:
┌───────┐
│ Root │ (mDelayAnim, 时长 = mStartDelay)
│ start=0, end=startDelay
└───┬───┘
│ (无 parent 的 Node 挂到 Root 下)
┌───────────┼───────────┐
▼ ▼ ▼
┌───────┐ ┌───────┐ ┌───────┐
│ Node D │ │Node A │←──│Node B │ (sibling: A ↔ B)
│ │ │ │ │ │
└───┬───┘ └───┬───┘ └───────┘
│ │
└─────┬─────┘
▼
┌───────┐
│Node C │ (child of A; parent: A)
│ │
└───────┘
时间轴 (假设 startDelay=0, 各动画 1000ms):
D: 0 ──────── 1000
A: 0 ──────── 1000 (A 和 B 同时开始)
B: 0 ──────── 1000
C: 1000 ───── 2000 (A 结束后开始)
数据结构:
java
class Node {
Animator mAnimation;
ArrayList<Node> mChildNodes; // 后继节点(本动画结束后启动)
ArrayList<Node> mSiblings; // 同步节点(同时启动)
ArrayList<Node> mParents; // 前驱节点(前驱结束后本动画启动)
Node mLatestParent; // 结束时间最晚的 parent
long mStartTime; // 计算出的起始时间
long mEndTime; // 计算出的结束时间
long mTotalDuration; // 缓存的总时长
boolean mEnded; // 是否已结束
}
构建过程:
-
sibling 传播 :
findSiblings递归收集所有传递性 sibling,确保 sibling 间共享相同的 parents。使用mParentsAdded标志防止重复处理。图示:sibling 传递性传播
初始状态 (play(A).with(B), play(B).with(C)):
A.siblings = [B]
B.siblings = [A, C]
C.siblings = [B]findSiblings(A) 递归展开:
A → B → C → (B 已访问, 停止)
结果: A.siblings = [B, C]传播 parents:
A.parents = A.parents ∪ B.parents ∪ C.parents
B.parents = B.parents ∪ A.parents ∪ C.parents (mParentsAdded=true)
C.parents = C.parents ∪ A.parents ∪ B.parents (mParentsAdded=true)最终: A, B, C 共享相同的 parents 集合
-
Root 挂载 :没有 parent 的 Node(顶层动画)自动挂到
mRootNode下,确保所有动画都从 Root 可达。图示:Root 挂载
挂载前: 挂载后:
(无 parent 的 Node) Root (mDelayAnim)
│
Node A Node D ├── A
├── D
└── (其他无 parent 的 Node)A.addParent(Root) → Root.childNodes += A
D.addParent(Root) → Root.childNodes += D -
DFS 时间计算 (
updatePlayTime):mRootNode.mStartTime = 0
mRootNode.mEndTime = mStartDelayupdatePlayTime(parent, visited=[]):
visited.add(parent)
for child in parent.mChildNodes:
if child in visited: ← 环检测
标记环上所有 Node 为 DURATION_INFINITE(永不播放)
continueif parent.mEndTime == INFINITE: child.mStartTime = INFINITE ← 父永不结束,子永不开始 else: child.mStartTime = max(child.mStartTime, parent.mEndTime) ← 多 parent 取最晚 child.mEndTime = child.mStartTime + child.mTotalDuration updatePlayTime(child, visited) ← 递归visited.remove(parent) ← 回溯
环检测 :使用 visited 列表记录当前 DFS 路径。若发现 child 已在路径中,说明存在循环依赖,将环上所有 Node 标记为 DURATION_INFINITE。
图示:环检测
代码: play(A).before(B), play(B).before(C), play(C).before(A)
→ A→B→C→A 循环依赖
DFS 路径: Root → A → B → C → (发现 A 已在 visited 中!)
visited = [Root, A, B, C]
↑
C 的 child 是 A, A 在 visited[1] 中
标记环: visited[1..3] 全部设为 DURATION_INFINITE
A.mStartTime = INFINITE
B.mStartTime = INFINITE
C.mStartTime = INFINITE
结果: A, B, C 永不播放 (Log.w("Cycle found"))
Root 不受影响 (index 0, 在环外)
多 parent 取最晚 :一个 child 可能有多个 parent(通过 with() 或 after() 建立多条依赖)。DFS 会多次访问同一 child,每次用 parent.mEndTime >= child.mStartTime 判断是否需要更新为更晚的起始时间。
图示:多 parent 取最晚
代码: play(A).before(C), play(B).before(C)
→ C 有两个 parent: A 和 B
假设: A.endTime=1000, B.endTime=1500
DFS 第一次访问 C (从 A):
C.mStartTime = max(0, A.endTime) = 1000
C.mEndTime = 1000 + C.duration
DFS 第二次访问 C (从 B):
B.endTime(1500) >= C.mStartTime(1000) → 需要更新
C.mStartTime = 1500 ← 取更晚的
C.mEndTime = 1500 + C.duration
最终: C 在 A 和 B 都结束后才开始
3.3.2 mEvents 事件列表构建
事件类型:
java
class AnimationEvent {
static final int ANIMATION_START = 0; // 动画应被 start()
static final int ANIMATION_DELAY_ENDED = 1; // startDelay 结束,开始计算值
static final int ANIMATION_END = 2; // 动画应结束
Node mNode;
int mEvent;
long getTime() {
if (mEvent == ANIMATION_START) return mNode.mStartTime;
if (mEvent == ANIMATION_DELAY_ENDED) return mNode.mStartTime + mNode.mAnimation.getStartDelay();
return mNode.mEndTime; // ANIMATION_END
}
}
构建过程 (sortAnimationEvents):
-
生成事件:为每个 Node(跳过 Root)生成 START、DELAY_ENDED、END 三个事件
图示:事件生成
Node A (start=0, end=1000, startDelay=200):
ANIMATION_START time = mStartTime = 0
ANIMATION_DELAY_ENDED time = mStartTime + startDelay = 200
ANIMATION_END time = mEndTime = 1000时间轴:
0 200 1000
│ │ │
START DELAY_ENDED END
(调用 (startDelay 结束, (动画
start()) 开始计算值) 结束) -
排序 :按时间升序。同时间事件的顺序为
END → START → DELAY_ENDED(利用常量值降序实现),保证前一个动画先结束后一个再开始。特殊处理 START vs DELAY_ENDED 为升序(先 start 再等 delay)。图示:同时间事件排序规则
场景: A 在 1000ms 结束, B 在 1000ms 开始 (play(A).before(B))
排序前 (按生成顺序):
... A.END(1000), B.START(1000), B.DELAY_ENDED(1000) ...同时间比较 (t1 == t2):
END(2) vs START(0): 降序 → END 在前 → A.END, B.START
START(0) vs DELAY(1): 升序 → START 在前 → B.START, B.DELAY_ENDED排序后:
... A.END(1000), B.START(1000), B.DELAY_ENDED(1000) ...含义: A 先结束 → B 再 start → B 的 delay 结束
-
零时长修正 :当
mStartTime == mEndTime时,三个事件时间相同,排序后顺序不确定。遍历修正为START → DELAY_ENDED → END紧邻有序。图示:零时长修正
场景: Node C 的 mStartTime == mEndTime == 1500 (0 时长动画)
排序后 (三个事件时间相同, 顺序可能错误):
... C.END(1500), C.START(1500), C.DELAY_ENDED(1500) ...修正 (遍历找到 END, 向前查找同 Node 的 START 和 DELAY_ENDED):
- 移除 START, 插入到 END 之前
- 移除 DELAY_ENDED, 插入到 END 之前
修正后:
... C.START(1500), C.DELAY_ENDED(1500), C.END(1500) ...含义: 先 start → delay 结束 → end (正确的生命周期顺序)
-
插入 Root 事件:在列表头部插入 Root 的三个事件(index 0-2)。
图示:Root 事件插入
修正后的 mEvents:
[A.START, A.DELAY_END, A.END, B.START, ...]插入 Root 事件后:
[Root.START(0), Root.DELAY_END(0), Root.END(startDelay),
A.START, A.DELAY_END, A.END, B.START, ...]
↑--------------- index 0-2 ---------------↑Root 事件表示 AnimatorSet 自身的 startDelay 阶段
-
校验:首事件必须是 START,末事件必须是 END。
运行时用途 :doAnimationFrame 中 findLatestEventIdForTime(unscaledPlayTime) 线性扫描找到当前时间对应的最新事件 ID,handleAnimationEvents(mLastEventId, latestId, ...) 处理增量事件。
3.3.3 mChildStartAndStopTimes 数组构建
java
private long[] ensureChildStartAndEndTimes() {
if (mChildStartAndStopTimes == null) {
LongArray startAndEndTimes = new LongArray();
getStartAndEndTimes(startAndEndTimes, 0);
long[] times = startAndEndTimes.toArray();
Arrays.sort(times);
mChildStartAndStopTimes = times;
}
return mChildStartAndStopTimes;
}
构建过程:
-
getStartAndEndTimes遍历mEvents,对每个ANIMATION_DELAY_ENDED事件对应的 Node,收集其mStartTime和mEndTime(递归处理嵌套 AnimatorSet) -
排序去重
图示:mChildStartAndStopTimes 构建
mEvents 中的 DELAY_ENDED 事件:
Root.DELAY_END → start=0, end=startDelay
A.DELAY_END → start=0, end=1000
B.DELAY_END → start=1000, end=2000收集 start/end 时间:
LongArray = [0, startDelay, 0, 1000, 1000, 2000]
↑ Root ↑ A ↑ B排序去重后:
mChildStartAndStopTimes = [0, 1000, 2000] (假设 startDelay=0)时间轴:
0 1000 2000
│ │ │
└── A 活跃 ──┘
└── B 活跃 ──┘
用途 :animateBasedOnPlayTime 中用于 seek 时的二分查找 。给定一个 playTime,findNextIndex 通过 Arrays.binarySearch 快速定位当前时间落在哪个子动画的区间内,避免遍历所有事件。
图示:seek 时的二分查找
mChildStartAndStopTimes = [0, 1000, 2000]
seek 到 1500ms:
findNextIndex(1500, [0, 1000, 2000])
binarySearch → 未找到, 返回 -3
index = -(-3) - 1 = 2 (指向 2000)
findNextIndex(500, [0, 1000, 2000])
binarySearch → 未找到, 返回 -2
index = -(-2) - 1 = 1 (指向 1000)
index=1 < endIndex=2 → 需要处理区间 [1000, 2000):
animateSkipToEnds(1000, 500) → A 跳到结束值
animateValuesInRange(1000, 500) → A 的区间内值 (此时 A 已结束, 跳过)
最后:
animateSkipToEnds(1500, 1000) → B 未跨越边界, 跳过
animateValuesInRange(1500, 1000) → B 在活跃区间, 计算 alpha=0.5