ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

HarmonyOS 7 FlexDesk:windowSizeChange与DynamicLayout窗口断点

HarmonyOS 7 FlexDesk:windowSizeChange与DynamicLayout窗口断点 SceneForge 系列结束以后这一轮换到一个完全不同的工程问题同一个 HarmonyOS 7 应用窗口从手机宽度拉到平板、PC再缩回去页面到底应该“换布局”还是“重做一套页面”。新 Demo 叫FlexDesk。它是一个轻量生产力面板里面有今日任务、重点项目、会议安排、知识笔记、灵感收集和回收站六张核心卡片。后面几篇会继续增加侧栏导航、多窗口、折叠屏和 PC/2in1 交互但 01 只做一件事让同一组业务组件在窗口宽度变化时切换布局算法同时保持组件状态不被重建。这个系列固定 6 篇01 windowSizeChange × DynamicLayout窗口断点、布局算法切换与状态不重建 02 Tabs × SideBar手机底栏、平板侧栏与导航选中态连续性 03 Multi-Window × Window分屏/悬浮窗连续缩放与局部信息密度 04 Foldable × State Continuity折叠/展开后的详情面板与编辑状态保持 05 PC/2in1 × Pointer/Keyboard鼠标、键盘、悬停与快捷操作 06 Metrics × Regression多尺寸、多窗口、多输入设备回归验收HarmonyOS 当前多窗口适配文档的核心建议很明确进入悬浮窗、分屏以后应用窗口尺寸会改变应监听windowSizeChange再基于实际窗口尺寸调整布局API 24 开始提供的DynamicLayout则允许运行时切换 Row / Column / Grid 等布局算法并且不用替换原有子组件。FlexDesk 01 就把这两件事接在一起。本轮统一数据taskId: adapt_20261003_01 project: FlexDesk page: DashboardPage.ets windowSequence: 430 → 720 → 920 → 720 → 430 vp windowEvents: 5 layoutSwitches: 4 projectBreakpoints: COMPACT 600vp MEDIUM 600–839vp EXPANDED 840vp cardCount: 6 stateRetained: 6 / 6 selectedCardId: task_1042 draftChars: 128 scrollAnchor: task_1036 avgSwitchCost: 3.6ms currentWidth: 430vp currentHeight: 932vp currentMode: COMPACT status: ADAPTIVE_READY一、第一篇先把“设备适配”改成“窗口适配”最开始的判断很常见if phone 用手机布局 else if tablet 用平板布局 else 用 PC 布局实际一跑多窗口这个思路马上会出问题。同一台平板全屏时可能 920vp 分屏以后可能只剩 460vp 悬浮窗继续缩窄还会更小。设备类型没变窗口却已经完全进入手机级宽度。所以 FlexDesk 的适配输入不使用deviceType而是统一使用window width in vp这和官方多窗口适配指导的思路一致布局变化应该跟随窗口实际尺寸而不是把“平板”当作固定大屏。二、Window 事件只负责提供事实不直接决定页面长什么样第一段代码解决的是在 Stage 模型里怎么把主窗口尺寸稳定地同步给 UI。import{UIAbility}fromkit.AbilityKitimport{window}fromkit.ArkUIexportdefaultclassEntryAbilityextendsUIAbility{onWindowStageCreate(windowStage:window.WindowStage):void{windowStage.getMainWindow().then((mainWindow){constupdate(widthPx:number,heightPx:number){AppStorage.setOrCreate(winWidthVp,px2vp(widthPx))AppStorage.setOrCreate(winHeightVp,px2vp(heightPx))}constrectmainWindow.getWindowProperties().windowRectupdate(rect.width,rect.height)mainWindow.on(windowSizeChange,(size){update(size.width,size.height)})})}}这里 Window 层只做两件事拿当前尺寸 监听尺寸变化。它不会直接写width 600 就两列因为“窗口事实”和“项目布局策略”应该分开。后面 03 进入分屏、悬浮窗时这层代码仍然可以继续用。三、600 / 840 是 FlexDesk 项目断点不是 HarmonyOS 固定阈值本轮项目先定义COMPACT 600vp MEDIUM 600–839vp EXPANDED 840vp这两个值是 FlexDesk 当前页面密度、卡片最小宽度和导航策略跑出来的项目断点。不是系统规定600 一定是平板 840 一定是 PC第二段代码专门把断点集中管理exportenumWindowMode{COMPACT,MEDIUM,EXPANDED}exportclassBreakpointResolver{resolve(widthVp:number):WindowMode{if(widthVp600){returnWindowMode.COMPACT}if(widthVp840){returnWindowMode.MEDIUM}returnWindowMode.EXPANDED}}断点集中以后后面导航、详情面板、Grid 列数都引用同一份规则。不会出现Dashboard 600 Navigation 620 DetailPane 800这种各写各的情况。四、DynamicLayout 的价值不是“少写几个 if”而是子组件不用换一套API 24 的DynamicLayout支持ColumnLayoutAlgorithm RowLayoutAlgorithm GridLayoutAlgorithm StackLayoutAlgorithm CustomLayoutAlgorithmFlexDesk 当前三档COMPACT Column MEDIUM 2 列 Grid EXPANDED 3 列 Grid第三段代码解决的是只切布局算法不替换六个 TaskCard。import{DynamicLayout,ColumnLayoutAlgorithm,GridLayoutAlgorithm,LayoutAlgorithm,LengthMetrics}fromkit.ArkUIexportclassDashboardLayoutPolicy{getAlgorithm(widthVp:number):LayoutAlgorithm{if(widthVp600){returnnewColumnLayoutAlgorithm({space:LengthMetrics.vp(12)})}if(widthVp840){returnnewGridLayoutAlgorithm({columnsTemplate:1fr 1fr,rowsGap:LengthMetrics.vp(12),columnsGap:LengthMetrics.vp(12)})}returnnewGridLayoutAlgorithm({columnsTemplate:1fr 1fr 1fr,rowsGap:LengthMetrics.vp(12),columnsGap:LengthMetrics.vp(12)})}}页面里真正的六个 TaskCard 仍然是同一组数据源。这和if compact CompactDashboard() else TabletDashboard()是两种完全不同的工程思路。后者很容易把页面状态拆成三份。前者只是改变“这些子组件怎么排”。五、状态应该属于业务对象而不是属于某一种布局FlexDesk 当前选中的任务task_1042草稿128 chars滚动锚点task_1036这些状态都不应该存到CompactDashboardState MediumDashboardState ExpandedDashboardState而是统一属于DashboardSnapshot窗口变宽只是视觉变化。业务状态本身没有改变。因此430 → 720 → 920 → 720 → 430跑完整个序列后仍然得到selectedCardIdtask_1042 draftChars128 scrollAnchortask_1036六、为什么 6/6 的“状态保持”比布局截图更重要如果只看截图手机 1 列 平板 2 列 PC 3 列很容易觉得适配已经完成。真正使用时更容易暴露的是窗口拉宽以后编辑框内容没了 任务选中态变回第一项 滚动位置跳到顶部 某张卡片内部计时器重新开始。所以 FlexDesk 01 的验收项里专门记录stateRetained6/6六张卡片内部状态全部没有因为布局切换被重置。七、断点切换也要做迟滞思维避免临界值反复抖动窗口拖动时可能连续出现598 601 599 602如果每一次都立即切Column ↔ Grid界面会抖。当前 Demo 为了便于展示直接按 600 / 840 切换真正产品我会再加hysteresis例如在临界区保留上一布局几 vp 的缓冲。这一点暂时没有写进 01 的最终代码但已经在 LayoutPolicy 里预留。因为断点本质上属于项目策略而不是散落在 UI build 里的魔法数字。八、平均 3.6ms 只统计“策略切换与页面重排”不代表整页渲染耗时本轮五次窗口事件430 720 920 720 430真正发生布局模式切换4 次项目侧记录的平均切换耗时3.6ms这个数字只作为 FlexDesk 当前设备和 Demo 的基线。它不是系统承诺。后面 06 会在更多尺寸上继续跑同一指标。九、DevEco 图里要看的是同一份状态在三种布局里都能对上开发图HiLogtaskIdadapt_20261003_01 430vp COMPACT Column 720vp MEDIUM Grid2 920vp EXPANDED Grid3 retained6/6 selectedtask_1042 draftChars128 anchortask_1036 windowEvents5 layoutSwitches4 avgCost3.6ms statusADAPTIVE_READY从日志上就能看出来变化的是布局算法不是任务数据。十、手机运行图里故意放了三档预览而当前仍然停在 430vp最终运行图当前真实状态430vp COMPACT下面同时保留三档预览COMPACT 600 MEDIUM 600–839 EXPANDED 840这样运行图不是在“模拟一台平板”而是在手机诊断页里证明同一个 FlexDesk 数据模型已经有三套窗口级布局策略。十一、为什么这一篇没有用设备型号和折叠状态因为它们不是 01 的主矛盾。01 先解决最通用的一层窗口宽度变化 → 布局算法变化 → 状态不变化等这层稳定以后折叠屏姿态只是影响“窗口为什么变了”的一个输入。04 才会专门处理折叠 / 展开后的详情面板和编辑状态。十二、DynamicLayout 也不是所有页面都应该用如果页面本身只有一个标题 一张详情卡 一个按钮普通 Row / Column 自适应就够了。FlexDesk 之所以用DynamicLayout是因为同一组 6 个业务卡片 需要在运行时从 1 列切到 2 列 / 3 列 又希望子组件身份保持稳定。这种场景才真正能体现 DynamicLayout 的价值。十三、Code Linter 可以作为后续多设备适配的第二道保险HarmonyOS 当前已经提供cross-device-app-dev/*一组跨设备 Code Linter 规则。里面包括window-size-change-listener-check one-multi-breakpoint-check sidebar-navigation grid-columns-spanFlexDesk 01 先把工程架构建好。后面的 02、03 会继续把这些规则真正落进页面。十四、01 最后固定六组反向测试第一组430vp 打开必须是 COMPACT。第二组720vp必须切 MEDIUM两列。第三组920vp必须切 EXPANDED三列。第四组920 → 430task_1042仍然选中。第五组来回拖动以后草稿仍然 128 字符。第六组五次窗口事件结束后六张 TaskCard 都没有重新初始化业务状态。全部通过以后ADAPTIVE_READY才成立。十五、下一篇不是继续“加一列”而是处理导航形态主内容区已经能稳定从1 列 → 2 列 → 3 列变化。接下来真正不协调的是导航。手机上底部四个 Tab 很自然。平板 / 2in1 如果仍然把四个 Tab 放在底部会浪费横向空间而且用户视线移动也更远。02 会继续同一个 FlexDesk430vp 底部 Tabs 720vp / 920vp 侧边 Tabs重点不是“换位置”而是selectedIndex projectId taskId scrollOffset 详情状态全部连续。十六、窗口事件和布局切换要分成两个口径windowSizeChange连续触发时并不是每次都意味着布局模式真的变了。例如窗口从430 450 480 520 560一直拖到 590vp仍然都属于COMPACT这时页面宽度当然需要持续更新但DynamicLayout不应该每次都重新创建一个新算法实例。FlexDesk 因此把两组指标分开windowEvents 真实尺寸事件数量 layoutSwitches 真正跨断点的次数本轮五次关键事件里只发生四次模式变化。后续进入真正连续拖窗时窗口事件会远多于布局切换次数。这种区分能避免把“监听正常工作”误写成“布局频繁重建”。十七、TaskCard 的身份必须用业务 id 固定不能用数组下标即使用了DynamicLayout如果子组件身份本身不稳定状态一样会丢。例如ForEach(tasks, (item, index) TaskCard(item), (item, index) index.toString() )任务顺序一变组件身份也跟着变。FlexDesk 六张卡片统一使用task_1042 task_1036 ...这类业务 id 作为稳定 key。所以窗口变化时布局位置变化并不会被框架误认为这是另一张新卡片。这也是stateRetained6/6能稳定成立的基础。十八、GridLayoutAlgorithm 下不要继续依赖 Flex 属性DynamicLayout 官方文档对这一点有明确说明Row / Column 算法 Flex 属性生效 Grid 算法 子组件位置由 GridLayoutAlgorithm 参数控制 Flex 布局属性不生效。所以 FlexDesk 切到 MEDIUM / EXPANDED 时不会继续依赖layoutWeight flexGrow去决定 TaskCard 列宽。真正的列数、间距交给columnsTemplate rowsGap columnsGap控制。如果同时写两套约束最终问题往往不是“完全报错”而是不同窗口宽度下出现难以解释的卡片尺寸差异。十九、断点变化时数据请求不能跟着重跑一个很典型的反例是layoutMode change → aboutToAppear 再走一次 → 重新请求 Dashboard 数据窗口被用户来回拖动时就会出现430 → 请求一次 720 → 又请求一次 920 → 再请求一次FlexDesk 把数据加载和布局策略完全拆开。任务数据只由 Repository / ViewModel 生命周期控制。DynamicLayout的变化只负责重新测量和摆放已有 TaskCard。因此本轮contentReloadCount0虽然没有单独放进封面主指标但已经加入调试日志。后面的 02 也沿用同样原则导航变形不等于重新请求业务内容。二十、窗口监听也要有明确释放点01 的示例为了聚焦主线只展示了注册逻辑。真实工程里windowSizeChangeCallback 不能“注册完就忘”。FlexDesk 在 Ability 销毁时会持有原 Callback 引用并执行off(windowSizeChange, callback)避免多次创建 WindowStage 后监听器叠加。否则调试时最典型的现象是拖一次窗口 HiLog 打两次、三次同样日志。这种问题非常像 UI 自己重复构建实际却是事件监听泄漏。二十一、宽度优先不代表高度永远不重要01 当前 Dashboard 是纵向滚动页面所以第一版断点主要由widthVp决定。但多窗口以后高度也会直接影响首屏卡片数量 底部操作区是否需要收起 详情面板是否还能同时展示所以WindowSizeStore同时保存winWidthVp winHeightVp只是 01 的 LayoutPolicy 暂时只消费 width。03 真正进入悬浮窗和分屏以后会把宽度 高度 宽高比三者一起纳入信息密度策略。二十二、第一篇最终建立的是“窗口变化不等于页面换代”的底层约束到这里FlexDesk 01 固定下来的并不只是三档样式。真正的工程约束是Window 提供尺寸事实 BreakpointResolver 提供项目策略 DynamicLayout 提供排布变化 TaskCard 保留稳定身份 业务状态独立于布局模式 窗口监听有注册和释放边界。后面无论是Tabs 变侧栏 分屏变窄 折叠屏展开 PC 窗口自由缩放都继续建立在这套约束上。这比为 phone / tablet / PC 分别复制三份页面更容易长期维护。二十三、这套断点策略还要留给后续设计系统复用FlexDesk 不会让每个页面各自维护一份 600 / 840。除了 Dashboard 和 Navigation后续的详情面板、工具栏密度、弹窗宽度也都要从同一份 BreakpointResolver 读取。这样设计稿调整断点时只修改一处策略整个应用都能同步演进。多形态适配真正困难的不是写一个判断而是保证所有页面长期使用同一套判断依据。参考资料HarmonyOS 7 / API 26 升级适配https://developer.huawei.com/consumer/en/doc/harmonyos-releases/upgrade-adaptation多窗口布局适配https://developer.huawei.com/consumer/en/doc/harmonyos-guides-V14/multi-window-layout-adapt-V14DynamicLayouthttps://developer.huawei.com/consumer/cn/doc/doccenter-references/api/ts-container-dynamiclayout栅格与分栏https://developer.huawei.com/consumer/cn/doc/doccenter-references/api/grid-and-column-layout
返回列表