
1. 前言
点击桌面上的花盆,瓢虫从叶后探出来。倾斜手机,它沿着叶片走动;过一会儿,它退回去,卡片继续显示最近的观察。这串动作有一个朴素的预期:无论瓢虫走到哪里,花盆都应该留在原处。
上一篇处理了瓢虫怎样移动和停住。现在把注意力移到动作两端:普通态交给激活态的那一刻,以及激活态退出的那一刻。如果两套页面各自把花盆画得很漂亮,却使用了不同的缩放或叶片基点,切换仍会让人觉得换了一盆植物。
工程继续使用绑定薄荷 plant-001 的2×2桌面动态卡片。普通页面是 MintCard,激活页面是 MintLive,对应扩展仍为 MintFormAbility 和 MintLiveAbility。花盆、叶片、瓢虫资源,以及上一篇的路线、起点和倾斜映射,都保持原有约定。

讨论衔接之前,需要分清几种现象。白屏意味着没有可见内容;跳位需要检查坐标和尺寸;颜色突变要追查资源、样式及过渡混合;动作没有播完则可能来自系统中断。它们发生在相近时刻,却需要不同的处理。
本文关注两套画面的约定,以及正常结束和提前结束各自怎样返回普通态。观察日记依然由用户手动维护,装饰动作不会产生新的植物状态。
2. 方案
一种直接的实现方式,是分别调整两套页面,直到肉眼看起来相似。这在第一次制作时很方便,维护起来却容易出现偏差。普通态换了花盆图片,激活态仍引用旧资源;一边修改圆角,另一边保留常量。每项变化都很小,组合起来便形成了接缝。
我更愿意把这些约定集中下来,称为视觉契约:两态使用哪些资源,背景有多大,花盆相对背景放在哪里,瓢虫以什么姿态出现。契约不要求共用页面实例,也不要求两个进程共享内存。它让两份渲染代码有同一套核对依据。
这里有一个容易混淆的细节。普通态里,瓢虫藏在叶后;上一篇所说的起点,是它进入倾斜互动时的路线位置。两者之间还隔着一个探出的动作。因此,激活首帧应先延续藏在叶后的基准姿态,再移动到互动起点。若一加载就把瓢虫完整放在路线中间,后面再平滑也无法修复最初的跳变。
正常路径于是分成进入、互动、收束三段。进入段让叶片伸展、瓢虫探出;互动段固定叶片形态,沿用上一篇的控制器;收束段将瓢虫送回起点,再退到叶后,叶片回到基准形态。花盆和叶片基点始终不动。

提前中断需要另一种预期。用户在瓢虫走到半途时翻页,系统可以立即结束动效。此时没有理由要求用户等它走回来。普通态必须随时能够接手,显示植物名称、摘要和日记入口。装饰姿态可能突然结束,但业务内容不能依赖回程动画才能恢复。
场景动效规定单次申请最长为3500ms,桌面操作也可能打断动效。这个上限约束了预算,却没有承诺每次都能获得完整播放时间。
完成资源与几何统一后,再比较 useDefaultAnimation 开关。两组使用相同资源、输入和时间安排,才能观察系统默认切换动效的影响。若同时改了花盆尺寸和动画配置,最终画面即使更好,也难以知道哪个变化起了作用。
3. 实现
视觉契约可以先写成一张很小的表。它的价值在于约束那些容易被复制到两套页面里的值。
| 对象 | 共同约定 |
|---|---|
| 植物与资源 | plant-001,同一套花盆、叶片和瓢虫资产 |
| 原卡片背景 | 相同尺寸、颜色及实际圆角 |
| 花盆与叶片 | 相同基点、基准缩放和缩放中心 |
| 瓢虫 | 相同尺寸、遮挡层级、基准姿态与互动起点 |
| 业务内容 | 相同名称、摘要状态规则和日记入口位置 |

激活页面的根容器可以比普通卡片大,背景却不能跟着放大。第03篇已经给出这层关系:使用 liveFormInfo.rect 放置原背景,花盆和叶片基点相对原背景排列,装饰层使用额外空间。
下面的函数只表达这一步坐标转换。传入的锚点以原卡片左上角为原点,输入输出均使用vp:
ts
interface Point {
x: number;
y: number;
}
interface CardRect {
left: number;
top: number;
width: number;
height: number;
}
function inLivePage(rect: CardRect, anchor: Point): Point {
return {
x: rect.left + anchor.x,
y: rect.top + anchor.y
};
}
调用方从当前回调取得矩形,不能传入窗口绝对位置。仍以第03篇的演算为例:原卡片160×160vp,申请区域为 (-20,-20,200,200)vp,原背景在激活页面中的偏移便是 (20,20)。这组数字用来说明关系,布局时应读取当前值。
圆角也属于这份契约。背景保留自己的圆角裁剪,出框叶片则放在独立装饰层里。否则,同一套资源可能因为裁剪层级不同,在两态里呈现出不同轮廓。位置相同只是视觉一致的一部分。
加载顺序沿用第01篇:准备当前会话的矩形、圆角和基准姿态,在 onLiveFormCreate 中同步加载 pages/MintLive,然后读取资料,要求同步调用 loadContent。资料读取与传感器初始化都不应阻塞默认画面。
默认内容仍然是已知身份的薄荷和本地资源。摘要读取期间显示"正在读取最近观察",确认没有记录后才显示"还没有观察记录"。资料到达只更新内容,不重新播放进入动画,也不把正在移动的瓢虫重置。
普通卡片与应用之间存在进程边界,LocalStorage 服务当前 LiveForm 会话。这里继续使用已有持久化读取路径,传递植物身份和摘要,不传递瓢虫每一帧的位置。两态资料到达时间不同,应作为内容更新问题单独观察,不能指望统一图片资源顺便解决它。
接下来处理时间。设申请时长为 D,进入、互动和收束分别为 E、I、R。设计预算应满足 E + I + R ≤ D ≤ 3500ms。实际安排还要考虑加载与系统切换占用;请求时刻、页面创建时刻和可见首帧不是同一个时间点。

进入互动阶段时,重新建立上一篇控制器的输入时间基准。从这一刻开始累计相对变化,避免把探出过程中的手势积累成首次位移。互动期间仍使用原来的死区、限幅和按时间平滑,本文不改变这些参数。
收束的关键是接管位置。假设传感器目标已经到路线末端,画面因平滑仍停在中间。回程应该从渲染层当前使用的进度开始。如果从目标开始,第一帧就会向前跳一下。
下面用一个独立对象计算回程进度。from 来自上一篇逐帧维护的呈现状态,start 是既定互动起点,elapsed 使用应用单调时钟计算的经过时间。
ts
class ReturnMotion {
private from: number;
private start: number;
private duration: number;
constructor(from: number, start: number, duration: number) {
if (!Number.isFinite(from) || !Number.isFinite(start) ||
!Number.isFinite(duration) || duration <= 0 ||
from < 0 || from > 1 || start < 0 || start > 1) {
throw new Error('Invalid return motion');
}
this.from = from;
this.start = start;
this.duration = duration;
}
progress(elapsed: number): number {
if (!Number.isFinite(elapsed)) {
return this.from;
}
const t = Math.min(1, Math.max(0, elapsed / this.duration));
const eased = t * t * (3 - 2 * t);
return this.from + (this.start - this.from) * eased;
}
}
duration 与 elapsed 使用相同时间单位。这条曲线使进度留在两个端点之间,复用原路线后不会因插值产生超调。回到起点以后,再执行退入叶后的姿态变化;这两部分都占用收束预算,不能各自按完整 R 计时。
开始收束时,先关闭互动输入的写入权,再创建回程对象。位置在任一阶段只由一个控制器更新。若传感器和平滑器还在写位置,回程即使计算正确,也会被下一次输入拉回去。
这种接管保证的是位置连续,切换瞬间的速度仍可能变化。需要更柔和的效果时,可以继续研究速度衔接,但应先把位置跳变与速度变化区分开。当前呈现状态也只是应用提交的状态,屏幕是否及时显示仍需录屏确认。
提前销毁直接结束当前阶段:先使会话失效,再退订传感器,停止应用持有的渲染更新和计时任务。清理允许重复调用,各项清理独立尝试;旧回调即使晚到,也不能更新页面。销毁回调不承担补播回程的职责,再次激活则创建新会话,从基准姿态开始。
4. 检验
我会把几何检查和切换检查分开。先确认两态稳定画面的资源、矩形、圆角与锚点一致,再检查过渡。否则,一张混合中的画面可能被误判为布局偏移,而真正的缩放差异又可能被动画掩盖。
比较 useDefaultAnimation 时,固定设备、系统、SDK、构建、卡片位置和资源版本,同时保留申请时长与阶段参数。每个条件重复三次,保留全部录屏。倾斜输入可以复用上一篇的记录,手动中断发生在什么时刻仍需单独记录。
| 条件 | 要回答的问题 |
|---|---|
| 开始切换 | 激活基准是否接住普通态,花盆或颜色是否突变 |
| 正常结束 | 回程是否从当前姿态开始,退出前是否到达基准 |
| 进入段中断 | 瓢虫尚未探出时,普通态是否完整 |
| 互动段中断 | 在路线起点、中部和边界退出时,背景是否稳定 |
| 收束段中断 | 是否直接退出,有无补播或重复申请 |
| 资料延迟 | 默认植物是否可见,内容到达是否重置动作 |
| 再次激活 | 是否从基准开始,是否受到旧回调干扰 |
首尾取帧也应有固定规则。对开始、正常结束、提前中断分别保留完整片段,定位可见切换边界,再取边界前后各一帧,同时保留最近的稳定参考画面。某个阶段没有出现,就记录原因,不从另一次录屏补一张。

测量时以原卡片区域为参照,检查背景边缘、花盆底部和叶片基点。瓢虫按所属阶段比较,不能把预期中的移动算成接缝。先校正截图裁剪位置,再测锚点差;系统过渡中的整体变换也应与页面内部错位分开。
原始距离用px记录,注明设备像素密度、录屏缩放和测量精度,再换算vp。颜色取样保持区域和录屏条件一致,避免把压缩或透明混合造成的差异直接归为资源错误。
一次记录至少关联 formId、plantId、应用侧 runId、配置值、帧号、录屏相对时间和锚点差。日志里的加载调用只能证明代码执行到那里。没有共同时间基准时,不能据此断言某一帧已经呈现,更不能给出超出录屏精度的响应时间。
翻页中断还有一个特殊情况:卡片离开了屏幕。这时应检查返回该桌面页后的普通态,而不是把切走期间不可见解释成白屏。若会话在收束前结束,则记录为提前中断,不归入正常结束样本。
这些记录最终要支持一个具体判断:接缝来自资源、几何、控制器接管,还是系统切换过程。统一契约可以消除两份配置互相矛盾的来源;实际画面改善多少,需要由同条件录屏回答。
5. 结尾
两态衔接可以从几项小约定开始:使用同一套资源,以原卡片背景定位,区分藏在叶后的基准姿态与互动起点,并让位置在每个阶段只有一个更新者。
正常播放时,收束从当前呈现位置接管,沿原路线返回。提前中断时,普通态直接承接完整的业务内容。这两条路径共同决定了退出行为,不能只把正常回程打磨好,就认为所有结束情况都有了答案。
视觉契约、回程计算和逐帧核对表,让后续修改有了检查依据。下一篇固定这些画面,继续使用同一盆 plant-001,检查日记入口的实际命中范围:瓢虫伸出了边框,用户能点击的区域是否也随之改变,两态又能否打开同一盆薄荷的日记。