ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

HarmonyOS 7 FlexDesk:Tabs与SideBar导航选中态连续性

2026/10/7 15:10:05 拓冰建站 浏览量
HarmonyOS 7 FlexDesk:Tabs与SideBar导航选中态连续性 01 把 FlexDesk 的主内容区做成了真正的“窗口适配”。同一组六张卡片已经能在430vp COMPACT 单列 720vp MEDIUM 两列 920vp EXPANDED 三列之间切换而且task_1042、128 字草稿和滚动锚点都不会因为 DynamicLayout 变化被重建。真正继续拉宽窗口以后第二个问题马上出现内容已经像平板了导航还像手机。如果 920vp 宽度下仍然把Today Projects Calendar Settings四个入口压在底部功能当然能用但横向空间完全没有参与导航结构。HarmonyOS 当前跨设备 Code Linter 已经有一条很直接的规则对于 tablet 和 2in1Tabs 应考虑使用侧边导航即vertical(true)。FlexDesk 02 就把这条建议做成一套可连续切换的导航壳。但这一篇真正要解决的不是“把底栏旋转 90°”。而是导航位置变化以后用户正在看的项目、任务、滚动位置和详情状态都必须继续留在原处。本轮统一数据taskId: adapt_20261003_02 project: FlexDesk navItems: 4 navLabels: Today Projects Calendar Settings selectedIndex: 1 selectedDestination: Projects selectedProjectId: pj_harmony_26 selectedTaskId: task_1042 windowSequence: 430 → 720 → 920 → 430 vp navSwitches: 3 sidebarBreakpoint: 600vp (FlexDesk 项目策略) sideBarWidth: 88vp bottomBarHeight: 64vp scrollOffset: 384vp selectionRetained: 4 / 4 detailStateRetained: true contentRebuildCount: 0 duplicateNavigationBlocked: 2 avgSwitchCost: 2.8ms currentWidth: 430vp currentNav: BOTTOM_TABS status: NAV_CONTINUOUS一、最容易写错的是“宽屏换一个新页面”第一版我写过这种结构if width 600 PhoneHome() else TabletHome()结果窗口从 430 拉到 720 时PhoneHome 销毁 TabletHome 新建于是Projects 选中态没了 列表滚动位置归零 正在看的 task_1042 丢了 详情面板重新请求数据。视觉上导航确实从底部变侧栏了。业务上却像重新打开了一次应用。所以 02 的第一条规则是导航容器只换形态不换业务内容实例。二、导航状态先从页面里抽出来第一段代码解决的是让选中态和内容状态独立于 Tabs 的纵横方向。exportclassNavSelectionStore{selectedIndex:number1selectedProjectId:stringpj_harmony_26selectedTaskId:stringtask_1042scrollOffsetVp:number384detailExpanded:booleantrue}当前四次关键状态读取430vp 720vp 920vp 430vp全部得到selectedIndex1因此selectionRetained4/4三、600vp 只是 FlexDesk 的侧栏切换策略01 已经有COMPACT 600 MEDIUM 600–839 EXPANDED 84002 直接复用同一组规则。FlexDesk 当前定义 600vp 底部 Tabs 600vp 侧边 Tabs这个 600vp 是项目阈值。官方 Code Linter 的建议是tablet / 2in1 使用侧边导航更合适但它不会替 FlexDesk 决定你的页面必须在精确 600vp 切。具体断点仍然要根据内容密度验证。四、不要写两套 Tabs直接让同一个 Tabs 改 vertical第二段代码解决的是 02 最核心的问题同一个 Tabs 容器从底部变成侧边。EntryComponentstruct AdaptiveTabsShell{StorageLink(winWidthVp)winWidthVp:number430StateselectedIndex:number1privateisSideBar():boolean{returnthis.winWidthVp600}build(){Tabs({index:this.selectedIndex,barPosition:this.isSideBar()?BarPosition.Start:BarPosition.End}){TabContent(){TodayPage()}.tabBar(Today)TabContent(){ProjectsPage()}.tabBar(Projects)TabContent(){CalendarPage()}.tabBar(Calendar)TabContent(){SettingsPage()}.tabBar(Settings)}.vertical(this.isSideBar()).onChange((index:number){this.selectedIndexindex})}}真正发生变化的是vertical(false) → vertical(true)以及barPosition End → Start四个 TabContent 并没有换成另一套页面。五、为什么侧栏宽度固定 88vp而内容宽度仍然自适应本轮sideBarWidth88vp侧栏只放icon short label selected state主内容继续占剩余空间。720vp 时FlexDesk 更像SideBar Projects List920vp 时右侧还能再出现Task Detail Pane也就是说导航宽度相对稳定 业务内容吃掉剩余窗口。这样侧栏不会随着窗口无限变宽。六、EXPANDED 里出现详情面板也不能让 ProjectsPage 重新创建920vp 时当前结构SideBar Project List Task Detail430vp 时则只有Bottom Tabs Project ListTask Detail 从“同页右侧”退回成点击后进入详情但详情状态仍然属于selectedTaskIdtask_1042 detailExpandedtrue不是属于 920vp 那一层 UI。所以缩回 430vp 后再次拉宽到 920vptask_1042还会自动回到右侧详情。本轮detailStateRetainedtrue七、滚动位置 384vp 也要跟着内容不跟着导航形态项目列表已经滚到384vp如果导航切到侧栏时重新初始化 ProjectsPage用户会被弹回0vpFlexDesk 当前把列表滚动快照放到页面业务状态里。导航切换以后仍然恢复scrollOffset384vp这也是 02 为什么要记录contentRebuildCount0如果内容没有被重建滚动状态天然更容易保持。八、重复导航也要做幂等快速连续点击Projects Projects Projects不应该三次重复push reload request当前 Store 先判断comingIndex selectedIndex相同就直接拦截。本轮故意重复点击两次duplicateNavigationBlocked2导航切换不是越“灵敏”越好。同一个目标重复触发应该尽量不产生额外业务动作。九、第三段代码把 Project / Task / Scroll 一起当成导航上下文如果只保存selectedIndex还不够。Projects 里面还有自己的上下文。exportinterfaceNavigationSnapshot{selectedIndex:numberprojectId:stringtaskId:stringscrollOffsetVp:numberdetailExpanded:boolean}exportfunctionrestoreNav(snapshot:NavigationSnapshot):void{navStore.selectedIndexsnapshot.selectedIndex navStore.selectedProjectIdsnapshot.projectId navStore.selectedTaskIdsnapshot.taskId navStore.scrollOffsetVpsnapshot.scrollOffsetVp navStore.detailExpandedsnapshot.detailExpanded}这套 Snapshot 后面 03 做多窗口时还会继续用。如果应用进入悬浮窗导航可能重新回到底栏但用户上下文仍然不应该丢。十、为什么不把 tablet / PC 直接写成不同路由因为真正多形态应用里窗口尺寸会连续变化。920vp 的 PC 窗口拖到 500vp它突然就“像手机”了。如果代码逻辑仍然是PC 路由 Tablet 路由 Phone 路由多窗口时会非常难维护。FlexDesk 继续坚持 01 的原则窗口宽度 决定布局和导航形态 业务路由 只决定用户去哪。这两层不混。十一、DevEco 图里只切 vertical不切业务页面开发图统一日志taskId adapt_20261003_02 430vp navbottom selectedIndex1 project pj_harmony_26 task task_1042 720vp navside verticaltrue retainedtrue scrollOffset 384vp 920vp navside detailStateRetained true 430vp navbottom contentRebuildCount 0 duplicateNavigationBlocked 2 navSwitches 3 selectionRetained 4/4 avgSwitchCost 2.8ms status NAV_CONTINUOUS这条链比“侧栏看起来很好看”更重要。十二、手机运行图里为什么仍然有平板 / PC 预览最终运行图当前设备宽度430vp所以底部真实显示Today Projects Calendar Settings其中Projects处于选中状态。页面中间同时展示720vp 侧边导航 920vp 侧边导航 详情面板它的作用是把“当前导航上下文在三档窗口里如何连续”一次性说明白。十三、Code Linter 的 sidebar-navigation 适合当防回退工具当前官方 Code Linter 已有cross-device-app-dev/sidebar-navigation规则说明非常直接对于 2in1 和 tablet Tabs 应设置为侧边导航栏。FlexDesk 真正上线前会把它加入项目规则。这样后续有人改页面时如果又把宽屏 Tabs 写回底部CI 至少会给出提醒。十四、侧栏并不意味着所有宽屏页面都要显示完整文字88vp 很窄。FlexDesk 当前侧栏采用icon 短 label不会把“今日所有待处理工作”这种长文本硬塞进去。多形态适配不是“宽屏就展示更多字”。而是每个布局区域都应该有明确的信息密度。这一点会在 03 多窗口缩放里更明显。十五、02 最后固定七组状态测试第一组430vp底部 Tabs。第二组720vp侧栏 Tabs。第三组920vp侧栏 详情面板。第四组selectedIndex 始终为 1。第五组pj_harmony_26 / task_1042始终一致。第六组滚动位置保持 384vp。第七组920 → 430 → 920详情状态仍然恢复内容重建次数为 0。全部通过以后NAV_CONTINUOUS才成立。十六、下一篇真正进入“窗口可以被用户连续拖动”的场景01 和 02 都在用几个固定宽度做验证430 720 920但真正多窗口里用户会连续拖动窗口边界920 870 811 730 642 598 520还会出现分屏 悬浮窗 比例变化03 会继续 FlexDesk进入真正的 Multi-WindowwindowSizeChange 高频事件 布局切换节流 局部信息密度 宽高比变化 滚动 / 编辑状态保持同一个项目会从“固定断点演示”进入“连续窗口变化”。十七、导航栏位置变化时焦点语义也不能变化用户看到的是底部 Projects → 左侧 Projects但辅助功能和键盘焦点应该仍然把它理解成同一个导航目标。FlexDesk 不会因为侧栏化就把Projects改成另一个内部路由名。导航项的稳定身份仍然是today projects calendar settings图标、位置、排列方向可以变化语义 id 不变。这对后面 05 的键盘方向键和快捷键适配也很重要。十八、侧栏切换不能把“当前详情”塞回列表数据里920vp 下有Project List Task Detail Pane最容易偷懒的写法是为了显示详情 把 task_1042 再插入列表头部。窗口缩回手机以后这条临时数据很可能继续残留。FlexDesk 始终把列表数据和详情选择分成两个状态。因此task_1042只是selectedTaskId不是“列表里的特殊第一项”。这种模型在宽屏变窄时会干净很多。十九、scrollOffset 的 Owner 应该是 Projects 内容而不是 Tabs384vp属于 ProjectsPage。如果把它放到AdaptiveTabsShell后面切到 Calendar 又切回来Tabs Shell 就需要理解每个页面自己的滚动语义。FlexDesk 当前做法是Tabs Shell 只保存 selectedIndex ProjectsPage 保存 projectId / taskId / scrollOffset 详情面板 保存 detailExpanded。02 的诊断 Snapshot 只是把这些信息汇总展示。Owner 仍然各归各。这样后续页面继续增加自己的筛选条件也不会让导航 Store 变成一个巨大的全局垃圾桶。二十、底栏高度 64vp 和侧栏宽度 88vp 都是视觉策略运行图里bottomBarHeight64vp sideBarWidth88vp这两个数字只是 FlexDesk 当前视觉稿的结果。不是 HarmonyOS Tabs 的系统固定尺寸。真实项目还需要结合系统安全区 图标大小 文字语言 字体放大 可访问性重新验证。尤其多语言后英文和中文标签长度不同。不能因为 88vp 在当前四个短词上够用就推导出所有业务侧栏都应该 88vp。二十一、Code Linter 是“提醒”不是运行时适配器cross-device-app-dev/sidebar-navigation很有价值。但它做的是静态检查 / Warning不会在应用运行时帮你自动执行vertical(true)更不会帮你保存selectedIndex taskId scrollOffset所以正确理解是Linter 防止开发者忘记宽屏导航规范 FlexDesk 的状态架构 保证真正运行时的连续性。两者是两层能力。二十二、导航切换也需要和断点策略共用同一个 Resolver如果 Dashboard 用600 / 840Tabs 自己却写640那么窗口处在620vp时就会出现内容已经 MEDIUM 两列 导航却还保持手机底栏。界面不是一定不能用但整体体验会明显割裂。所以 02 没有重新定义另一套宽度体系。它直接复用 01 的BreakpointResolver。只是把COMPACT → bottom MEDIUM / EXPANDED → side映射到导航策略。二十三、临界宽度附近也要防止侧栏和底栏高频来回切和 01 一样598 601 599 602如果窗口尺寸每次都直接触发导航形态变化视觉跳动会比卡片列数变化更明显。因为整个页面的主轴都会变。FlexDesk 后续准备把layoutMode hysteresis统一放进 BreakpointResolver。导航和 Dashboard 共同消费同一个稳定后的 Mode。不会一个先切、另一个后切。本轮固定宽度测试暂时没有触发这种抖动但设计上已经留出位置。二十四、内容重建次数 0 是这一篇最关键的隐藏指标封面更容易看到底部 → 侧栏 → 侧栏 → 底部真正决定体验质量的是contentRebuildCount0因为只要内容页没有被销毁重建选中态 滚动位置 临时编辑状态 请求缓存就更容易自然保持。所以 FlexDesk 02 的优先级是先保证同一 Content 生命周期 再调整 Tabs 的方向和位置 最后补视觉细节。这和很多“先做三套 UI、再想办法同步状态”的做法完全相反。二十五、NAV_CONTINUOUS 的最终边界本轮NAV_CONTINUOUS至少表示四个导航项身份稳定 Projects 始终 selectedIndex1 pj_harmony_26 没变化 task_1042 没变化 scrollOffset384vp 没变化 详情状态保持 底栏与侧栏之间切换 3 次 业务内容没有重新创建 重复导航被幂等拦截。它并不表示 FlexDesk 已经完成所有大屏体验。03 还要面对更难的一类变化窗口不是跳到几个固定宽度而是被用户连续拖动。到那一步信息密度、动画节流和多窗口比例会成为新的主矛盾。二十六、侧栏与底栏切换以后返回行为也必须保持一致用户从 Projects 进入 task_1042 详情后430vp 下可能表现为完整页面跳转920vp 下则表现为右侧详情面板。两种形态虽然视觉不同但“返回”都应该回到同一个 Projects 上下文而不是宽屏返回一次关闭详情、再返回一次才回项目列表窄屏却直接退出应用。FlexDesk 因此把返回意图也绑定到导航上下文先关闭当前详情再回退导航层级。这样窗口形态变化不会改变用户对返回键的预期。这条规则目前只做状态设计真正和系统返回事件、键盘 Escape、鼠标侧键联动会继续放到 05 的 PC/2in1 输入适配里。补充一个容易忽略的细节导航形态变化时埋点事件名称也应该保持稳定。FlexDesk 统一记录nav_select_projects不会因为底栏或侧栏切换就拆成两套埋点。否则同一个用户动作会被数据平台误判成两个功能入口长期分析会失真。参考资料HarmonyOS 多窗口布局适配https://developer.huawei.com/consumer/en/doc/harmonyos-guides-V14/multi-window-layout-adapt-V14Code Lintersidebar-navigationhttps://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/ide_sidebar-navigationDynamicLayouthttps://developer.huawei.com/consumer/cn/doc/doccenter-references/api/ts-container-dynamiclayoutHarmonyOS 7 / API 26 升级适配https://developer.huawei.com/consumer/en/doc/harmonyos-releases/upgrade-adaptation