范围:
Animator从起动到停止的过程、渲染路径中不调用get()时动画不推进的原因、八项缓动曲线的取舍、真实组件中的悬停与按压动画。
前置:核心概念 的链式 API。
Animator
Animator 是由 System.currentTimeMillis() 驱动的标量插值器:调用方传入起点、终点、毫秒数与缓动曲线,它随真实时间推进,每次取值返回当前值。Animator 不依赖游戏 tick,tick 快慢不影响一段 120ms 动画的实际耗时。
Animator 提供五个对调用方可见的方法:
| 方法 | 作用 |
|---|---|
animate(from, to, ms, easing) | 从 from 插值到 to,历时 ms 毫秒,按 easing 曲线推进;立即开始 |
to(target, ms, easing) | 从当前值动画到 target,是重启动画的便捷写法 |
set(v) | 直接跳到 v 并停止动画 |
get() | 取当前值;取值前先推进一步(懒更新) |
isRunning() | 动画是否仍在进行;查询前同样先推进一步 |
默认缓动是 Easing.EASE_OUT_CUBIC(字段初始化固定在 Animator 内),不传曲线时表现为“快启动、慢停住“。
悬停高亮用法
一个组件维护悬停态,用 Animator 把背景色从常态插值到悬停色。
private final Animator hoverAnim = new Animator();
// 在输入回调里(悬停状态翻转时)
hoverAnim.animate(0f, 1f, 120, Easing.EASE_OUT_CUBIC);
// 在渲染里
float t = hoverAnim.get(); // 每帧取值 = 每帧推进
int color = Colors.lerp(normalColor, hoverColor, t);
animate 只负责“定下这次动画的起止与曲线“,真正的推进发生在每次调用 get() 时:get() 内部先计算 t = min(1, 已耗时/总时长),按缓动算出新值后返回。因此渲染路径必须每帧取一次值;组件不渲染的帧内动画不会自行推进,下一帧取值时按真实流逝的时间一次性补齐(时间源是墙钟,不是帧数)。set(v) 用于不应有过渡的场合(例如初始化、程序性赋值),它直接设置值并停止进行中的动画。
懒更新
Animator 没有自己的更新循环。它只有在 get() 或 isRunning() 被调用时才检查墙钟、重算插值,该机制即懒更新。对调用方有两点约束:
- 动画依赖调用方每帧调用
get()。 只调用animate()而不调用get(),值始终停在起点;get()位于渲染路径而渲染被整体跳过(元素不可见、屏幕未重绘)时,动画停留在最后取到的值,恢复渲染时跳到按真实时间应处的位置。 isRunning()也推进。 用isRunning()判断动画是否结束是安全的,因为调用它同样会将值推进到当前时刻;但该调用并非无副作用,与get()一样会修改内部状态。
进度 t 的算法是 t = min(1, (now - startTime) / durationMs):到时封顶为 1,将值精确落在终点,然后把运行标志置回停止。时间不足一帧、时长为零以外的边界均由这个 min 处理。
缓动函数
缓动取值对照 Easing.java 如下。Easing 恰好八项,没有第九项,表外的枚举名在源码中不存在。缓动取值仅此一张表,其他文档(含贡献者侧动画页)链接本表,不另维护第二张取值不同的表。
| 枚举名 | 曲线行为 | 适用场景 |
|---|---|---|
LINEAR | 匀速直进,无加减速 | 进度条、不需要缓动的程序性插值 |
EASE_IN_QUAD | 慢起步,二次方加速离开起点 | 想强调“起势“的进入、按压蓄力 |
EASE_OUT_QUAD | 快起步,二次方减速逼近终点 | 轻量的落位、淡出收尾 |
EASE_IN_CUBIC | 慢起步,三次方加速,比 EASE_IN_QUAD 更沉 | 按下反馈(真实 Button 的 bgAnim 按下用它) |
EASE_OUT_CUBIC | 快起步,三次方减速停住,即 Animator 的默认曲线 | 悬停高亮、开关滑动、通用进出场(最常用) |
EASE_OUT_QUINT | 快起步,五次方强力减速,收尾拖得很长 | 需要“重重落下“感的归位、强调型出场 |
EASE_IN_OUT_CUBIC | 前半慢加速、后半慢减速,中段最快 | 往返移动、折叠展开,两头都要柔的过渡 |
EASE_OUT_BACK | 减速到位且越过终点再回弹一次 | 弹出菜单、徽章出现,需要一点活泼的过冲感 |
八项按“阶次递进 + 一个回弹特例“组织:QUAD/CUBIC/QUINT 控制减速段的陡度,IN/OUT 决定慢的一头在起点还是终点,BACK 是唯一带过冲的。曲线选择先确定慢速端位于起点还是终点,再确定减速强度。
真实组件中的悬停与按压动画
库内组件将 Animator 与 InteractionManager.hovering(悬停查询见交互与事件)配对使用,可参照以下模式:
Button:渲染时查hovering,悬停态翻转驱动borderAnim;按下时bgAnim.animate(0, 1, 60, Easing.EASE_IN_CUBIC)快速进入按压态,松开时bgAnim.animate(1, 0, 100, Easing.EASE_OUT_CUBIC)回到常态。按下用慢起步的EASE_IN_CUBIC,松开用慢收尾的EASE_OUT_CUBIC,按下与松开的不对称来自曲线选择。ItemView:字段hoverAnim对应悬停透明度(120msEASE_OUT_CUBIC),按压时把值从 1 推到 1.4(60msEASE_IN_CUBIC)做放大反馈,松开再推回(100msEASE_OUT_CUBIC)。SelectBox:同款hoverAnim字段,悬停进出各 100msEASE_OUT_CUBIC。
共性:动画起动发生在输入回调或悬停翻转处,取值发生在渲染路径中,曲线按“进 IN、出 OUT“配对。
不变量
- 时间源与游戏 tick 解耦。
Animator只读取System.currentTimeMillis(),不读 tick、不读帧时长。改时间源(例如换成帧累积、System.nanoTime()、游戏内时钟)会波及所有内置动画组件:Button、ItemView、SelectBox及一切自建Animator的动画表现一起改变。需要“暂停游戏时动画也暂停“的场景无法由现有实现满足,属于另一机制,不应通过修改Animator默认值表达该需求。 - 懒更新依赖调用方每帧调用
get()。 没有后台线程负责推进,框架的每帧驱动链(生命周期系统)只转发Element.tick()、不碰Animator;animate()之后不取值,动画不会产生任何可见变化。新组件必须在渲染路径中调用get()。 - 进度封顶在 1,终点精确到达。
t = min(1, elapsed/ms)保证动画不会越过终点漂走,也不会因除法超过 1 而将apply调出定义域(八条曲线的公式都按[0,1]设计)。 - 缓动枚举恰好八项。 八项取值全在上述表中;表外的任何枚举名在任何文档与示例中均不得出现,因为源码中不存在这些名字。
连带影响
- 改时间源(
animate/update里那两处System.currentTimeMillis()):波及所有内置动画组件与用户自建动画;与游戏 tick 的解耦关系随之改变,动画系统的设计论证需要同步修订。 - 改懒更新策略(例如给
Animator加主动 tick 推进):波及所有将get()写在渲染路径中的组件,同一帧可能被推进两次,动画速度加倍;旧代码“渲染中取一次“的假设将不再成立。需要每帧推进的状态请挂载到Element.tick()(那条链见 生命周期系统),不应改为由Animator自行推进。 - 改默认缓动(字段初始化从
EASE_OUT_CUBIC换成别的):波及所有不显式传曲线的调用点,库中to(...)与省略曲线的写法表现一起改变。 - 往
Easing增删枚举项:增删后必须同步修改缓动表;其他文档只链接本表,不各自维护取值。文档中若出现表外名字,即该文档自造,Easing源码中没有该名字。