【阅读源码--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
相关推荐
2501_9159214313 小时前
appuploader-cli 命令行上传 IPA 到 App Store Connect upload CI 集成
android·ci/cd·小程序·https·uni-app·iphone·webview
怪奇云呼军15 小时前
从声音特征到 CRM 回流:闪电智能 Voice Agent 沟通策略自适应系统 v1 实战
android·人工智能·python·音视频·语音识别
哦哦~92117 小时前
北理工|Composites Part‑A 综述:纤维‑增强复合材料传统成型与增材制造全解析
android·制造·复合材料·增材制造
一笑的小酒馆18 小时前
Android视频直播播放器简单封装
android
工会代表19 小时前
安卓手机搭建SOCKS5代理教程
android
雨白19 小时前
Android AOP 切面编程实战:优雅地处理全局断网拦截
android·架构
huluang21 小时前
专业级安卓现场取证利器:拍照取证(PaiZaoXue),补齐行业取证工具核心短板
android·数码相机
奇特認21 小时前
MySQL集群技术 4.高可用之组复制 (MGR)
android
阿pin21 小时前
Android随笔-SurfaceFlinger
android·surfaceflinger