FlexDesk 做到 05,功能层已经不缺东西了。

当前同一个项目已经跑通:

手机单栏
平板两栏
大屏三栏
底部 Tabs / 侧栏 Tabs
分屏 / 悬浮窗
连续窗口缩放
折叠 / 展开 / HALF_FOLDED
鼠标 Hover
右键菜单
Tab 焦点
键盘快捷键

如果最后一篇继续加一个新组件,反而会破坏整个系列的工程主线。

所以 06 固定做最终验收:五种输入 / 窗口 Profile,各跑 8 轮,共 40 次。

五个 Profile:

PHONE_TOUCH
430 × 932
TOUCH

TABLET_TOUCH
720 × 900
TOUCH

FLOATING_MOUSE
480 × 610
MOUSE

FOLDABLE_HOVER
720 × 720
TOUCH + KEYBOARD
HALF_FOLDED

PC_MOUSE_KEYBOARD
920 × 760
MOUSE_KEYBOARD

最终要验收的也不只是“页面能打开”。

而是:

布局
导航
折叠状态
焦点
快捷键
编辑状态
监听器
定时器
内存

都能在反复切换以后收口。

本轮统一数据:

taskId:
adapt_accept_20261003_06

profiles:
5

cycles:
8

totalRuns:
40

passed:
40 / 40

layoutStateMismatch:
0

navigationMismatch:
0

foldStateMismatch:
0

focusMismatch:
0

shortcutMismatch:
0

contentRebuildMismatch:
0

windowListenerLeak:
0

foldListenerLeak:
0

activeTimersAfterFinish:
0

retainedStates:
40 / 40

avgLayoutCommit:
2.2ms

p95LayoutCommit:
4.6ms

avgNavSwitch:
2.6ms

p95NavSwitch:
4.1ms

avgFoldTransition:
4.3ms

p95FoldTransition:
6.8ms

avgInputDispatch:
0.9ms

p95InputDispatch:
1.7ms

baselineMemory:
128.6MB

after8Cycles:
129.8MB

memoryDelta:
+1.2MB

maxMemory:
186.4MB

status:
PASS

一、06 不是随机拖 40 次窗口,而是固定 Profile

随机测试适合探索。

版本回归更适合固定输入。

第一段代码把五种形态写成可重复的 Profile:

export interface AdaptProfile {
  name: string

  widthVp: number
  heightVp: number

  input:
    'TOUCH' |
    'MOUSE' |
    'TOUCH_KEYBOARD' |
    'MOUSE_KEYBOARD'

  fold:
    'NONE' |
    'HALF_FOLDED'
}

export const profiles:
  AdaptProfile[] = [
    {
      name:
        'PHONE_TOUCH',
      widthVp:
        430,
      heightVp:
        932,
      input:
        'TOUCH',
      fold:
        'NONE'
    },
    {
      name:
        'TABLET_TOUCH',
      widthVp:
        720,
      heightVp:
        900,
      input:
        'TOUCH',
      fold:
        'NONE'
    },
    {
      name:
        'FLOATING_MOUSE',
      widthVp:
        480,
      heightVp:
        610,
      input:
        'MOUSE',
      fold:
        'NONE'
    },
    {
      name:
        'FOLDABLE_HOVER',
      widthVp:
        720,
      heightVp:
        720,
      input:
        'TOUCH_KEYBOARD',
      fold:
        'HALF_FOLDED'
    },
    {
      name:
        'PC_MOUSE_KEYBOARD',
      widthVp:
        920,
      heightVp:
        760,
      input:
        'MOUSE_KEYBOARD',
      fold:
        'NONE'
    }
  ]

5 个 Profile × 8 轮:

40

最终:

40 / 40

全部通过。

二、每个 Profile 都必须走自己的“真实主矛盾”

PHONE_TOUCH 不需要验证鼠标右键。

PC_MOUSE_KEYBOARD 也不应该只验证触摸点击。

所以 Runner 针对 Profile 开启不同断言:

PHONE_TOUCH
底部导航 / 单栏 / 触控点击

TABLET_TOUCH
侧栏 / 双栏 / 触控

FLOATING_MOUSE
窄窗口 / Hover / 信息密度降级

FOLDABLE_HOVER
FoldStatus / HOVER Layout / 状态连续

PC_MOUSE_KEYBOARD
侧栏 / 焦点 / 快捷键 / 右键菜单

这样“40/40”不是把同一个测试重复四十遍。

三、布局错误不能只看最终页面截图

假设窗口从:

920
→ 480
→ 920

最终又回到正常。

中间如果发生一次:

selectedTaskId 被清空

随后又从缓存恢复,

最终截图可能完全看不出异常。

所以 Regression Runner 每个阶段都采集:

NavigationSnapshot
TaskEditSnapshot
WindowMetrics
FoldWindowSnapshot
InputSnapshot

状态不一致立即记:

layoutStateMismatch
navigationMismatch
...

本轮所有 mismatch 都是:

0

四、第二段代码把“状态连续”变成硬断言

export class AdaptiveStateAssert {
  verify(
    before:
      FlexDeskState,
    after:
      FlexDeskState
  ): boolean {
    return (
      before.selectedIndex ===
        after.selectedIndex &&
      before.projectId ===
        after.projectId &&
      before.taskId ===
        after.taskId &&
      before.draftChars ===
        after.draftChars &&
      before.cursorOffset ===
        after.cursorOffset &&
      before.scrollOffsetVp ===
        after.scrollOffsetVp
    )
  }
}

40 次运行里:

retainedStates=40/40

没有一轮把:

pj_harmony_26
task_1042
draft=128
cursor=96
scroll=384

弄丢。

五、contentRebuildMismatch=0 比“页面没崩”更严格

FlexDesk 前五篇一直强调:

形态变化
不等于内容重建

所以最终回归还统计:

contentRebuildCount

期望是:

0

如果某一轮窗口变化导致 ProjectsPage 被重新创建,即使所有状态后来都恢复成功,仍然记为:

contentRebuildMismatch++

本轮:

0

六、导航、折叠、输入三类状态分别验收

最终 mismatch 拆开:

navigationMismatch=0

foldStateMismatch=0

focusMismatch=0

shortcutMismatch=0

不合并成一个:

uiMismatch=0

因为如果未来某版只有快捷键回退,开发者应该第一眼就知道问题属于输入层,而不是重新排查所有窗口策略。

七、Layout Commit 平均 2.2ms,P95 4.6ms

布局类操作包括:

BreakpointResolver
DensityPolicy
Tabs 方向切换
DynamicLayout Algorithm
Detail Pane 显隐

最终:

avgLayoutCommit=
2.2ms

p95LayoutCommit=
4.6ms

这只是当前测试环境的项目基线。

它的用途不是宣传“布局只要 2.2ms”,而是下一版本可以直接对照。

八、导航切换和折叠切换必须单独计时

导航:

avg=
2.6ms

p95=
4.1ms

折叠状态提交:

avg=
4.3ms

p95=
6.8ms

两者不混。

Fold Transition 会同时处理:

FoldStatus
WindowMetrics
FoldLayoutPolicy
编辑状态连续

比普通 Tabs 方向变化稍重,很正常。

九、输入事件平均 0.9ms,P95 1.7ms

05 已经建立:

Pointer
Hover
Focus
Shortcut
Key Event

的输入分发基线。

06 在 PC_MOUSE_KEYBOARD 和 FLOATING_MOUSE Profile 里继续采样。

最终:

avgInputDispatch=
0.9ms

p95InputDispatch=
1.7ms

HarmonyOS 当前全局输入监听文档也提醒:输入监听发生在 UI 线程,不应该做耗时操作。

FlexDesk 的 Command Dispatcher 仍然只做:

轻量判断
命令分发

十、监听器泄漏是多形态 App 很容易漏掉的一类资源问题

整个系列注册过:

windowSizeChange

foldStatusChange

hover / key / focus callbacks

节流 timer

单次运行没有问题,不代表反复进入页面没问题。

06 最后明确统计:

windowListenerLeak=0

foldListenerLeak=0

activeTimersAfterFinish=0

这比只看 JavaScript 堆内存更贴近本系列真正的资源风险。

十一、第三段代码专门做资源收口断言

export class AdaptiveResourceProbe {
  verifyReleased(
    state:
      RuntimeResourceState
  ): boolean {
    return (
      state.windowListeners ===
        0 &&
      state.foldListeners ===
        0 &&
      state.pendingTimers ===
        0 &&
      state.transientMenus ===
        0
    )
  }
}

每一轮测试结束都执行。

如果第 5 轮开始多一个 Window Listener:

第 5 轮直接 FAIL

不会等 40 次全部跑完才发现日志越来越多。

十二、内存回归看“稳定窗口”,不看瞬时峰值

当前基线:

128.6MB

8 轮结束:

129.8MB

变化:

+1.2MB

最大瞬时:

186.4MB

这组数据属于当前 FlexDesk 测试环境。

关键不是“186.4MB 很低”。

而是:

反复窗口 / 折叠 / 菜单 / 输入以后
稳定基线没有阶梯上升。

十三、五种 Profile 为什么不包含“所有设备”

因为回归矩阵必须可执行。

当前五种 Profile 覆盖的是本系列真正实现过的主路径:

phone
tablet
floating
foldable
PC/2in1

并不代表:

TV
wearable
car

也已经适配。

多设备适配最忌讳“同一套 ArkUI 就默认支持所有终端”。

06 只对自己真正验证过的能力负责。

十四、Code Linter 用来防止静态规则回退

除了运行时回归,FlexDesk 还会保留:

@cross-device-app-dev/sidebar-navigation

window-size-change-listener-check

one-multi-breakpoint-check

grid-columns-span

font-size

size-unit

等跨设备规则。

例如官方 sidebar-navigation 明确建议 2in1 / tablet 的 Tabs 使用侧边导航。

Linter 是静态防线。

40 次 Runtime Regression 是动态防线。

两者互补。

十五、DevEco 图只展示最终回归矩阵

开发图:

HiLog:

taskId=
adapt_accept_20261003_06

profiles=
5

cycles=
8

totalRuns=
40

passed=
40

layoutMismatch=
0

navMismatch=
0

foldMismatch=
0

focusMismatch=
0

shortcutMismatch=
0

rebuildMismatch=
0

avgLayout=
2.2ms

p95Layout=
4.6ms

avgNav=
2.6ms

p95Nav=
4.1ms

avgFold=
4.3ms

p95Fold=
6.8ms

avgInput=
0.9ms

p95Input=
1.7ms

memory=
128.6 → 129.8MB

windowListenerLeak=
0

foldListenerLeak=
0

activeTimers=
0

RESULT PASS

如果失败,会同时记录:

profile
cycle
window
fold
input
selectedTaskId
currentFocus
active listeners

而不是只输出一个 FAIL。

十六、运行图最终只回答“这五种形态现在是否一致”

最终运行图:

五个 Profile:

PHONE_TOUCH
PASS

TABLET_TOUCH
PASS

FLOATING_MOUSE
PASS

FOLDABLE_HOVER
PASS

PC_MOUSE_KEYBOARD
PASS

一致性检查:

布局 0

导航 0

折叠 0

焦点 0

快捷键 0

内容重建 0

最终:

PASS

十七、40/40 不代表“任何窗口都永远正确”

这个边界必须说清楚。

PASS 只代表:

当前 FlexDesk 版本

当前 5 个 Profile

当前 8 轮

当前项目断点与密度策略

当前测试设备

全部符合预期。

它不代表:

任意尺寸
任意外设
任意未来设备
任意语言

都已经覆盖。

工程回归真正有价值的是:

下一次修改以后,能够重复执行同一套证据链。

十八、FlexDesk 六篇真正形成的是一条“状态连续”主线

回头看整个系列:

01
窗口变化
但 TaskCard 状态不重建

02
导航变形
但选中态不重建

03
多窗口连续缩放
但内容不重建

04
折叠形态变化
但编辑状态不重建

05
输入设备变化
但业务状态不重建

06
固定矩阵
反复验证状态连续

所以这个系列真正解决的并不是:

如何做三栏布局

而是:

同一个业务对象,能不能跨窗口、跨形态、跨输入设备一直保持“还是它自己”。

十九、FlexDesk 到 06 正式结束

这个系列固定 X=6,到这里完成。

继续写 07,大概率只会继续追加:

另一个尺寸
另一个鼠标动作
另一个折叠角度

新的工程主矛盾已经不在这里。

下一轮应该切换到明显不同的技术方向和新 Demo,从新的 01 开始。

优先可以考虑:

平行视界 / EasyGo

精准碰一碰 / 跨设备投递

三方框架适配

不再继续扩写 FlexDesk。

二十、Profile 切换时还要重新验证“输入提示”有没有跟着环境变化

05 里已经把输入提示从设备类型里拆出来。

所以 06 切换:

PHONE_TOUCH
→ PC_MOUSE_KEYBOARD

时,除了业务状态不变,还要验证 UI 提示本身发生正确变化。

手机触摸模式:

不展示 Ctrl+K 提示;
不强调右键菜单。

PC 模式:

显示快捷键提示;
Hover 反馈启用;
右键菜单可达。

如果状态保持正确,但提示模式错误,也记作 Profile 失败。

多端一致不是“所有设备长得一模一样”,而是每个设备都给当前输入方式最合适的交互提示。

二十一、Focus 回归必须在每种宽度里检查“可见目标”

窗口从 920vp 缩到 480vp 后,详情面板消失。

这时详情里的保存按钮如果仍然留在焦点链里,键盘用户按 Tab 会进入一个“看不见的按钮”。

所以回归断言不仅检查:

focusMoves 能走

还要检查:

focused target 当前可见;
不可见区域不参与 tabStop。

FOLDABLE_HOVER 也一样。

上半区和下半区变化以后,焦点顺序必须匹配当前可见布局。

这类问题截图通常看不出来,必须靠运行时断言。

二十二、窗口监听和折叠监听要按页面 / Ability Owner 对账

FlexDesk 既有:

windowSizeChange

又有:

foldStatusChange

它们的 Owner 不一样。

Window Listener 属于窗口 / Ability 生命周期。

Fold Listener 属于折叠协调器生命周期。

06 不只统计:

总监听器数量=0

还保存:

windowListenerOwner
foldListenerOwner

防止一种更隐蔽的问题:

A 页面把 B 页面的监听器 off 掉。

资源释放正确,还必须释放“自己拥有的那一个”。

二十三、定时器回归要覆盖节流器被页面中途销毁的场景

03 的 WindowEventCoalescer 使用:

48ms timer

正常拖窗结束以后 Timer 会自然执行。

但真实用户可能在拖动窗口时立即:

关闭页面
切换项目
返回上一级

06 专门加入:

pending timer
→ 页面 dispose
→ timer 被 clear

的 case。

最终:

activeTimersAfterFinish=0

才能说明节流逻辑自己的生命周期也闭环。

二十四、折叠 Profile 要同时覆盖 HALF_FOLDED 和恢复后的宽屏状态

FOLDABLE_HOVER 不只是:

720×720
HALF_FOLDED

一次截图。

完整 case 是:

FOLDED
→ EXPANDED
→ HALF_FOLDED
→ EXPANDED
→ FOLDED

然后再进入下一轮。

这样才能验证:

draft
cursor
scroll
detail state

不是只在进入 HOVER 时没丢,而是在来回切换后仍然一致。

06 仍然沿用 04 的 5/5 retained 思路,只是把它放进自动矩阵。

二十五、回归矩阵也要有“环境能力不支持”的 SKIP 语义

未来某台测试设备不支持折叠状态或没有鼠标键盘环境时,不能把:

FOLDABLE_HOVER

直接判成产品 FAIL。

更合理的是:

SUPPORTED
→ 必须 PASS

UNSUPPORTED
→ SKIPPED_CAPABILITY

ERROR
→ FAIL

本轮五种 Profile 的测试环境都准备完毕,所以:

5 profiles
40 runs
40/40 passed

没有出现 Skip。

但 Runner 的状态模型已经把能力缺失和真正失败分开。

二十六、静态 Linter 通过也不能替代运行时状态验证

Code Linter 能告诉我们:

有没有监听 windowSizeChange;
tablet Tabs 是否考虑侧栏;
单位和断点写法是否合理。

但它看不到:

task_1042 有没有丢;
cursor=96 有没有归零;
右键菜单关闭后焦点去了哪里;
折叠后 Timer 有没有泄漏。

所以 FlexDesk 最终保留两道门:

Code Linter
检查实现规则;

Runtime Regression
检查真实行为。

两边都通过,才是“多形态适配完成”。

二十七、内存曲线 +1.2MB 也要放到下一版本继续观察

从:

128.6MB

到:

129.8MB

本轮没有阶梯式增长。

但 +1.2MB 也不会直接被描述成“绝对没有任何泄漏”。

系统缓存、GC 时机、图片资源都可能造成轻微变化。

真正有价值的是下一次继续按同样协议采样。

如果变成:

128.6
→ 136
→ 145
→ 154

即使单轮功能还是 40/40,也必须进入资源回归失败。

二十八、最终 PASS 由四类 Gate 共同组成

FlexDesk 不把:

40/40

单独作为最终 PASS。

真实门禁拆成:

FUNCTION_GATE
布局 / 导航 / 折叠 / 输入行为正确

STATE_GATE
project / task / draft / cursor / scroll 连续

PERFORMANCE_GATE
关键提交延迟在项目基线内

RESOURCE_GATE
listener / timer / 稳定内存可收口

四类都通过才:

status=PASS

这样后续哪一类出问题,报告能直接告诉开发者从哪里开始查。

二十九、每次 FAIL 都必须保存可复现的窗口和输入上下文

多形态 Bug 最大的问题是:

“刚才在某个大小的时候闪了一下”

很难复现。

所以 Runner 对失败项必须保存:

profile
cycle
widthVp
heightVp
foldStatus
inputProfile
selectedIndex
taskId
focusId
draftChars
activeListeners
pendingTimers

下一次开发者可以直接把窗口恢复到相同状态。

真正好的回归系统,不只是告诉你“失败了”,还要告诉你“怎么回到失败现场”。

三十、FlexDesk 六篇最终留下的是一个可持续演进的适配底座

系列收口以后,真正能复用的不是六张截图。

而是六个长期模块:

WindowSizeStore
BreakpointResolver
DensityPolicy
NavSelectionStore
FoldContinuityCoordinator
DesktopInputCoordinator

以及最后的:

FlexDeskRegressionRunner

以后新增另一个业务页面,只要接入这些状态源和策略层,不需要重新从“手机 / 平板 / PC 各写一份页面”开始。

这才是多形态工程真正能够规模化的地方。

三十一、系列结束后,回归基线仍然需要版本化

这次的 40/40 不是一份永远不变的答案。FlexDesk 会把断点策略、输入 Profile、测试数据和阈值一起写入 acceptanceVersion。以后如果侧栏宽度、折叠策略或快捷键方案发生调整,新版本应该生成新的基线,而不是硬拿旧数字继续比较。这样“性能变化”和“设计策略变化”不会混在一起,也能明确知道某个回归结果对应哪一代多形态方案。

因此最终验收产物不仅是一张 PASS 截图,还应该包含可重复执行的配置、指标与状态快照,让下一次修改之后能用同一套方法重新证明:FlexDesk 在手机、平板、浮窗、折叠屏和 PC/2in1 上仍然是同一个应用、同一份状态、同一套业务逻辑。

参考资料

  • Keyboard and Mouse Event:
    https://developer.huawei.com/consumer/en/doc/harmonyos-guides-V13/arkts-common-events-device-input-event-V13
  • 组件快捷键:
    https://developer.huawei.com/consumer/cn/doc/harmonyos-references-v5/ts-universal-events-keyboardshortcut-V5
  • 焦点控制:
    https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V14/ts-universal-attributes-focus-V14
  • sidebar-navigation Code Linter:
    https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/ide_sidebar-navigation
  • 全局基础输入事件监听:
    https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/api/ts-inputeventmonitor
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐