窗口位置过渡动画
窗口位置过渡动画是 ArkPets 桌面宠物运行时中负责"窗口坐标平滑插值"的子系统:当宠物窗口被移动(如用户拖拽、多开排布)时,窗口并不瞬间跳变到新坐标,而是由 cn.harryh.arkpets.transitions 包中的过渡(Transition)框架按可配置的缓动函数与时长,从当前位置平滑滑向目标位置。
Purpose and Scope
本页完整覆盖窗口位置过渡动画能力的端到端实现,包括:
cn.harryh.arkpets.transitions包的泛型过渡框架:抽象基类Transition<E>、窗口坐标实现TransitionVector2,以及同包的兄弟实现TransitionVector3、TransitionFloat;- 缓动函数体系:函数式接口
TernaryFunction与枚举EasingFunction(LINEAR/EASE_OUT_SINE/EASE_OUT_CUBIC/EASE_OUT_QUINT); - 运行时接入点:
ArkPets实例中的windowPosition字段的构造与配置解析; - 用户可配置项:
transition_duration(过渡时长)与transition_type(缓动类型),以及默认配置与控制面板 UI 控件的对应关系。
以下相关主题有意留给兄弟页面,本页仅做边界提示:
- 动画交叉过渡(
configTransitionAnimation,即 Spine 动画 Clip 之间的淡入淡出)属于角色行为/动画系统,与本文的"属性/坐标插值"是两套机制,请参见动画与行为相关页面; - 窗口的创建、透明、置顶、点击穿透等窗口管理细节,请参见窗口管理相关页面;
- 配置文件的加载、校验与持久化全流程,请参见配置系统页面。
Overview
在桌宠场景中,窗口位置变更十分频繁:用户用鼠标拖拽宠物、多开时窗口排布调整等。若窗口坐标直接跳变,视觉上会显得生硬。ArkPets 的解法是一个可中断、可插值的数值过渡框架:
- 核心抽象:
Transition<E>用"起点值start+ 终点值end+ 当前进度currentProgress+ 总进度totalProgress"四元组描述一次过渡。进度以时间增量推进(addProgress),任意时刻可取插值结果(now()→atProgress())。 - 坐标实现:
TransitionVector2将Transition<E>落到 libGDX 的Vector2(即窗口的 x/y 坐标),对 x、y 两个轴分别套用同一个缓动函数。 - 缓动函数:
EasingFunction枚举以 lambda 形式内置 4 条"ease-out"系曲线,让过渡"先快后慢",模拟物理惯性感,默认EASE_OUT_CUBIC、默认时长 0.3(秒)。 - 可中断性:
reset()是该设计的关键——当目标值变化时,新起点不是旧起点而是当前插值位置now(),因此连续拖拽时动画始终从"此刻画面所在处"继续,不会回跳。
该框架自 ArkPets 2.3 起引入(Transition 类 Javadoc 标注 @since ArkPets 2.3),除窗口位置外还供其它数值属性过渡复用(同包提供了 TransitionFloat、TransitionVector3)。
Architecture
组件关系
图中各组件的职责与连接依据:
- 配置层:默认配置文件提供
transition_duration: 0.3与transition_type: "EASE_OUT_CUBIC"两个键;ArkConfig.getEasingFunctionFrom(config.transition_type)负责把字符串解析为EasingFunction枚举实例。两者在ArkPets初始化第 4 步("Window position setup")一起注入TransitionVector2构造器。 - transitions 包:
Transition<E>是纯状态机式的抽象基类(不感知窗口、不感知渲染线程);TransitionVector2持有一个EasingFunction并在atProgress()中对 x/y 轴分别插值;EasingFunction通过实现TernaryFunction<Float, Float>接口获得统一的apply(begin, end, progress)调用形态。 - 运行时:
ArkPets实例持有唯一的windowPosition过渡对象;位置变更事件调用reset()重设目标;窗口定位消费now()的插值结果。
类型层次
设计意图说明:
- 模板方法模式:
Transition<E>把"进度推进、钳制、重设、查询"等通用状态逻辑全部用final方法封死,只把atProgress()这一个"如何插值"的纯函数留给子类。这保证了任何新增类型(如TransitionFloat、TransitionVector3)都无法破坏进度状态机的不变量(进度永远在[0, totalProgress]内)。 - 策略模式(枚举即策略):缓动曲线被建模为枚举而非类层次,新增一条曲线只需加一行 lambda;
EasingFunction同时实现TernaryFunction<Float, Float>,使其可以脱离枚举类型以函数形态被传递。 - 泛型复用:窗口位置只是该框架的一个消费者;坐标(Vector2/Vector3)与标量(Float)共用同一套状态机。
核心机制:Transition 状态机与可中断插值
状态字段与不变量
Transition<E> 的全部状态由四个字段构成:起点 start、终点 end、当前进度 currentProgress、总进度 totalProgress。两个核心不变量由 final 方法保证:
- 进度非负、不越界:
setCurrentProgress()使用Math.max(0, Math.min(totalProgress, currentProgress))双向钳制,totalProgress本身也被Math.max(0, totalProgress)归一化为非负数。因此即使调用方传入超前的 delta,也只会停在终点而不会越界。 totalProgress <= 0时无动画:这是刻意的退化路径——当用户把transition_duration配置为 0(或负数被钳为 0)时,atProgress()直接返回end,窗口坐标立即等于目标值,等效于"关闭过渡动画"。
逐帧推进:addProgress → now → isEnded
运行时每帧以渲染循环的 delta 时间调用 addProgress(delta),再消费 now() 得到当前应显示的坐标。三者的协作如下:
1public final E now() {
2 return atProgress(currentProgress);
3}
4
5public final boolean isEnded() {
6 return Objects.equals(now(), end());
7}
8
9public final void addProgress(float progress) {
10 setCurrentProgress(currentProgress + progress);
11}Source: Transition.java
isEnded() 的实现值得一提:它比较的是 now() 与 end(),即"当前插值结果等于终点"。对于 TransitionVector2 而言,当进度达到 1 时所有枚举缓动函数都恰好返回 end,因此 isEnded() 自然成立;这也意味着调用方无须自己判断进度,直接语义化地判断"动画是否到位"即可。
可中断的关键:reset 以当前插值位置为新起点
reset() 是整个框架最有设计感的部分:
1public final void reset(E end) {
2 if (Objects.equals(this.end, end))
3 return;
4 this.start = now();
5 this.end = end;
6 currentProgress = 0;
7}Source: Transition.java
三个细节决定了"连续拖拽不回跳、不抖动"的体验:
- 幂等短路:若新目标与当前目标相同(
Objects.equals),直接返回,不做任何状态改动——避免渲染循环里每帧重复reset引起的进度归零抖动。 - 新起点 =
now()而非旧start:这保证了过渡永远从"用户此刻看到的画面位置"继续。若新起点取旧start,快速连续改变目标会导致窗口先向后退再前进。 - 进度清零:每次目标变化都重开一段全新时长的过渡,配合 ease-out 曲线形成"追手"的跟手感。
TransitionVector2 额外提供了标量形式的便利重载:
public void reset(float x, float y) {
reset(new Vector2(x, y));
}Source: TransitionVector2.java
缓动函数:EasingFunction
EasingFunction 是一个实现 TernaryFunction<Float, Float> 的枚举,签名统一为 apply(begin, end, progress)。其全部四条曲线均为"ease-out"族(先快后慢),契合桌宠被"抛出"后减速停靠的直觉:
1public enum EasingFunction implements TernaryFunction<Float, Float> {
2 /** Linear function. */
3 LINEAR((b, e, p) -> b + p * (e - b)),
4
5 /** <a href="https://easings.net/#easeOutSine">Sine easing out function</a> */
6 EASE_OUT_SINE((b, e, p) -> b + (float) Math.sin(p * Math.PI / 2) * (e - b)),
7
8 /** <a href="https://easings.net/#easeOutCubic">Cubic easing out function</a> */
9 EASE_OUT_CUBIC((b, e, p) -> b + (1 - (float) Math.pow(1 - p, 3)) * (e - b)),
10
11 /** <a href="https://easings.net/#easeOutQuint">Quint easing out function</a> */
12 EASE_OUT_QUINT((b, e, p) -> b + (1 - (float) Math.pow(1 - p, 5)) * (e - b));
13
14 private final TernaryFunction<Float, Float> function;
15
16 EasingFunction(TernaryFunction<TernaryFunction<Float, Float>... function) { /* 见下文说明 */ }
17}Source: EasingFunction.java
(上段为节选示意,实际构造器签名为 EasingFunction(TernaryFunction<Float, Float> function),见源文件第 24–26 行;apply 委托给持有的 lambda,见第 28–31 行。)
数学上所有曲线都写成统一形式 b + f(p) * (e - b),其中 f(0) = 0、f(1) = 1:
| 枚举值 | f(p) | 手感 |
|---|---|---|
LINEAR | p | 匀速直线,无惯性感 |
EASE_OUT_SINE | sin(p·π/2) | 前段极快,长尾极缓 |
EASE_OUT_CUBIC(默认) | 1-(1-p)³ | 快出慢停的均衡曲线 |
EASE_OUT_QUINT | 1-(1-p)⁵ | 起步更猛、收尾更拖 |
设计意图:把缓动建模为枚举 + 函数式接口,而非类继承——曲线集合是封闭的(运行期不需要第三方注入新曲线),而调用侧(TransitionVector2.atProgress)只依赖 TernaryFunction 的 apply(b, e, p) 形态,未来若要支持自定义曲线,只需提供别的 TernaryFunction 实现即可,无需改动过渡框架。
坐标插值实现:TransitionVector2
1public class TransitionVector2 extends Transition<Vector2> {
2 protected final EasingFunction easing;
3
4 public TransitionVector2(EasingFunction easingFunction, float totalProgress) {
5 super(totalProgress);
6 easing = easingFunction;
7 start = new Vector2(0, 0);
8 end = new Vector2(0, 0);
9 }
10
11 @Override
12 public Vector2 atProgress(float progress) {
13 if (totalProgress <= 0)
14 return end;
15 float ratio = currentProgress / totalProgress;
16 return new Vector2(
17 easing.apply(start.x, end.x, ratio),
18 easing.apply(start.y, end.y, ratio)
19 );
20 }
21}Source: TransitionVector2.java
实现要点:
- 逐轴独立插值:x、y 使用同一进度比例
ratio = currentProgress / totalProgress,但分别代入缓动函数。这意味着二维轨迹在 ease-out 下是"先大步后小步"的对角收敛,而不是弧线。 - 每次返回新对象:
atProgress()每次都new Vector2(...),避免把内部start/end泄露给消费方(libGDX 的Vector2是可变对象,返回内部引用会破坏Objects.equals的幂等短路与状态一致性)。 - 构造初值:起点/终点都初始化为
(0, 0);由于reset()的幂等短路,首次把目标设为(0, 0)时不会触发重置,这是无害的。 - 注意一个微妙点:方法签名接收
progress参数,但实现读取的是字段currentProgress;框架内所有调用路径(now())传的就是currentProgress,因此行为一致。阅读源码时不要被参数名误导。
核心流程:一次窗口移动的完整时序
流程解读:
- 初始化:
ArkPets启动流程的第 4 步 "Window position setup" 构造windowPosition(见下节配置接入)。 - 目标变更:拖拽/排布逻辑调用
reset(x, y);若与上次目标相同则被短路,否则以当前插值位置为新起点、清零进度。 - 逐帧收敛:渲染循环以真实帧间隔推进进度并取插值结果应用到窗口坐标;由于进度被钳制,动画到
totalProgress即自然结束。 - 再次变更:任意时刻再次
reset都从"当前画面位置"继续,实现可中断的跟手动画。
运行时接入与配置
ArkPets 中的构造
1public TransitionVector2 windowPosition; // Window Position Easing
2
3// ... 初始化第 4 步 Window position setup:
4windowPosition = new TransitionVector2(
5 ArkConfig.getEasingFunctionFrom(config.transition_type),
6 Math.max(0, config.transition_duration)
7);Source: ArkPets.java
(构造调用见第 105–108 行;Math.max(0, ...) 在传入前再次把时长归一化为非负,与 Transition.setTotalProgress 的钳制构成双重保险。)
默认配置
"transition_duration":0.3,
"transition_type":"EASE_OUT_CUBIC",Source: ArkPetsConfigDefault.json
用户界面入口
控制面板"行为模块"(BehaviorModule.fxml)中与本能力直接相关的三行控件为:configTransitionDuration(常规属性过渡时长)、configTransitionFunction(缓动函数)及其帮助按钮:
1<HBox>
2 <Label fx:id="configTransitionDurationLabel" text="常规属性过渡"/>
3 <JFXComboBox fx:id="configTransitionDuration" prefWidth="120.0"/>
4 <JFXButton fx:id="configTransitionDurationHelp"/>
5</HBox>
6<HBox>
7 <Label fx:id="configTransitionFunctionLabel" text="缓动函数"/>
8 <JFXComboBox fx:id="configTransitionFunction" prefWidth="120.0"/>
9 <JFXButton fx:id="configTransitionFunctionHelp"/>
10</HBox>Source: BehaviorModule.fxml
Configuration Options
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
transition_duration | number(float,秒) | 0.3 | 过渡总时长;≤ 0 时钳制为 0,atProgress() 直接返回终点,等效关闭过渡 |
transition_type | string(枚举名) | "EASE_OUT_CUBIC" | 缓动曲线,可选 LINEAR / EASE_OUT_SINE / EASE_OUT_CUBIC / EASE_OUT_QUINT;由 ArkConfig.getEasingFunctionFrom() 解析 |
Source: ArkPetsConfigDefault.json
注意区分:同一 UI 面板中的 configTransitionAnimation("动画交叉过渡")控制的是角色 Spine 动画 Clip 的交叉淡化,与本页的属性/坐标过渡是不同机制,本页不展开。
API Reference
Transition<E>(抽象基类)
构造器:Transition(float totalProgress) —— 初始化时调用 setTotalProgress,进度从 0 开始。
| 方法签名 | 返回 | 说明 |
|---|---|---|
atProgress(float progress) | E(抽象) | 在给定进度处取插值值;约定 totalProgress ≤ 0 时必须返回 end |
start() / end() | E(final) | 读取起点 / 终点 |
now() | E(final) | atProgress(currentProgress) 的语义化封装 |
isEnded() | boolean(final) | Objects.equals(now(), end()) |
addProgress(float progress) | void(final) | 推进进度并自动钳制到 [0, totalProgress] |
reset(E end) | void(final) | 更新终点、以 now() 为新起点、清零进度;目标相同则短路 |
setCurrentProgress(float) / setTotalProgress(float) | void(final) | 直接设置进度 / 总进度(均做非负钳制;改总进度会清零当前进度) |
setToEnd() / setToStart() | void(final) | 跳到终点 / 起点 |
TransitionVector2
- 构造器:
TransitionVector2(EasingFunction easingFunction, float totalProgress)—— 起止均初始化为(0, 0)。 Vector2 atProgress(float progress):totalProgress ≤ 0时返回end;否则按ratio = currentProgress / totalProgress对 x、y 分别套用easing.apply(start.axis, end.axis, ratio),返回新建的Vector2。void reset(float x, float y):标量重载,委托给reset(new Vector2(x, y))。
EasingFunction implements TernaryFunction<Float, Float>
Float apply(Float a, Float b, Float c):即apply(begin, end, progress),返回插值分量;无异常抛出,浮点精度范围内apply(b, e, 0) = b、apply(b, e, 1) = e。
失败模式、边界情况与并发
- 时长为 0 / 负数:
Math.max(0, ...)在ArkPets接入处与setTotalProgress内双重钳制;TransitionVector2.atProgress的totalProgress <= 0分支直接返回end,不会出现除以 0 产生NaN坐标的情况。 - 进度越界:
setCurrentProgress的双向钳制保证ratio ∈ [0, 1],缓动函数在该区间内输出有界,窗口不会越过目标点。 - NaN / 非法坐标输入:源码未对
reset的入参做数值合法性校验(如NaN、超屏坐标),依赖上游窗口管理逻辑提供合法目标;属于已知边界,未做防御。 - 连续高频 reset:
reset的Objects.equals幂等短路 + 以now()为新起点,保证了高频目标变更不会回跳或抖动;但每帧目标都不同(真拖拽)时,进度永远清零、曲线永远处于起步段,等效为"贴近直线的快速跟动",这是刻意的行为而非缺陷。 - 并发:
Transition及其子类的字段均无synchronized/volatile保护,start/end也没有锁。其线程安全前提是单线程(渲染循环)内推进与消费。若在非渲染线程调用reset而渲染线程同时在addProgress,可能出现撕裂读(读到新end配旧start)。就现有接入方式(ArkPets.windowPosition由渲染循环驱动)而言是安全的;扩展复用该框架时必须保持"单一驱动线程"约定。 - 对象分配压力:
atProgress()每次new Vector2,在 60 FPS 下的每帧一次分配量级对 JavaFX/libGDX 场景可忽略,但这是为"可变性隔离"付出的有意识代价(见上文设计说明)。
性能与运维注意点
- 过渡时长与曲线均由用户在控制面板调整并持久化到配置文件;排查"窗口跟手不顺滑/太拖沓"类问题时,优先检查
transition_duration(是否被设为 0 或过大)与transition_type(EASE_OUT_QUINT收尾明显比LINEAR长)。 setTotalProgress会清零当前进度,运行期动态修改时长会打断进行中的过渡(画面停在当前插值位置,然后从该处重新起跳——因为下次reset仍以now()为起点)。若需要"热改时长不打断",需先取now()再手动恢复进度,当前 API 未提供该能力。- 该框架无 I/O、无系统调用,开销集中在每帧一次的浮点运算与一次
Vector2分配,性能敏感度低。
Extension Points
- 新增值类型:继承
Transition<E>,只需实现atProgress();进度状态机由基类final方法统一保证。包内已有TransitionFloat、TransitionVector3作为范例。 - 自定义缓动曲线:
TransitionVector2的字段类型是EasingFunction(具体枚举而非接口),因此新增曲线需在枚举中添加一个 lambda 分支——一行改动即可全站生效;若要支持用户自定义函数对象,可将字段放宽为TernaryFunction<Float, Float>(枚举本身已实现该接口,向后兼容)。 - 新的窗口属性过渡:透明度、缩放等标量属性可复用
TransitionFloat,接入方式与windowPosition相同(构造 → reset 目标 → 每帧 addProgress/now)。
Related Links
- Transition.java —— 泛型过渡基类(状态机与
finalAPI) - TransitionVector2.java —— 窗口坐标过渡实现
- EasingFunction.java —— 缓动函数枚举
- ArkPets.java ——
windowPosition字段与初始化接入点 - ArkPetsConfigDefault.json —— 默认配置
- BehaviorModule.fxml —— 控制面板过渡设置 UI
角色 Spine 动画之间的交叉过渡(动画交叉过渡)属于行为/动画系统,不在本页范围;窗口创建与样式管理请参见窗口管理相关页面。