【阅读源码--Android】动画之AnimatorSet--1

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 基准)                          │
└─────────────────────────────────────────────────────────────────────┘

核心机制

  1. DAG 依赖图:Node 间的 parent-child 关系构成有向无环图,决定播放顺序
  2. 事件列表 :将所有 Node 的 START/DELAY_ENDED/END 事件按时间排序,运行时通过 findLatestEventIdForTime 增量处理
  3. 帧注入 :AnimatorSet 注册到 AnimationHandler,每帧收到 frameTime 后计算 unscaledPlayTime,通过 pulseAnimationFrame 注入到 mPlayingSet 中的子动画
  4. 子动画不自行 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;               // 是否已结束
}

构建过程

  1. 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 集合

  2. 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

  3. DFS 时间计算updatePlayTime):

    mRootNode.mStartTime = 0
    mRootNode.mEndTime = mStartDelay

    updatePlayTime(parent, visited=[]):
    visited.add(parent)
    for child in parent.mChildNodes:
    if child in visited: ← 环检测
    标记环上所有 Node 为 DURATION_INFINITE(永不播放)
    continue

    复制代码
     if 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):

  1. 生成事件:为每个 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()) 开始计算值) 结束)

  2. 排序 :按时间升序。同时间事件的顺序为 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 结束

  3. 零时长修正 :当 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):

    1. 移除 START, 插入到 END 之前
    2. 移除 DELAY_ENDED, 插入到 END 之前

    修正后:
    ... C.START(1500), C.DELAY_ENDED(1500), C.END(1500) ...

    含义: 先 start → delay 结束 → end (正确的生命周期顺序)

  4. 插入 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 阶段

  5. 校验:首事件必须是 START,末事件必须是 END。

运行时用途doAnimationFramefindLatestEventIdForTime(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;
}

构建过程

  1. getStartAndEndTimes 遍历 mEvents,对每个 ANIMATION_DELAY_ENDED 事件对应的 Node,收集其 mStartTimemEndTime(递归处理嵌套 AnimatorSet)

  2. 排序去重

    图示: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
相关推荐
千里马学框架1 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台1 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone1 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
哈基米南北1 天前
ViewRootImpl 事件分发责任链分析
源码
致远ccc1 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo1 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077001 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼1 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone2 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen2 天前
属性动画原理与高级动画实现
android·kotlin·canvas