前两期把 InsightBoard 的两条基础边界定了下来:窗口宽度决定布局,折叠状态只描述设备形态。

01 用 windowSizeChange 把 COMPACT / MEDIUM / EXPANDED 三档断点跑通;02 再把 foldStatusChange 和窗口事件拆开,并通过 80ms 的状态协调窗口避免一次开合动作触发两次页面重排。

做到这里,普通“展开 / 折叠”已经比较稳定。真正把设备停在半折叠角度以后,新的问题才出现:页面虽然没丢状态,但内容会压到折痕附近,操作区域也并不自然。

这一轮没有再改断点,而是开始使用 HarmonyOS 7 / API 26 新增的 FoldSplitContainer 思路,专门处理悬停态和折痕区域。

本轮固定数据:

taskId: adapt_20261002_03
foldStatus: HALF_FOLDED
isHoverMode: true
appRotation: 0
windowStatus: FULL_SCREEN

primaryRegion: 392vp
hingeSafeGap: 36vp
secondaryRegion: 487vp

primary: 今日概览 + 趋势图
secondary: 任务列表 + 操作区
extra: 关闭

selectedItem: insight_042
scrollOffset: 1280vp
transitionCost: 38ms
status: HOVER_READY

一、半折叠不是“把屏幕高度除以二”

最开始我做过一个很粗暴的版本。检测到设备进入半折叠后,上半屏 50%,下半屏 50%。

看起来像是完成了悬停态,实际体验很差。折痕本身不是普通内容区域,标题、按钮、列表项压在折痕附近时,视觉上会被截断,触控也不舒服。另一方面,上下区域的任务权重不同。InsightBoard 上半屏更适合保留概览、图表和结果,下半屏更适合放列表、筛选和操作控件。机械对半切无法表达这种优先级。

另外,半折叠状态下的可用区域还会受到窗口状态、旋转方向影响。不能只拿一个“折叠角度”就直接决定全部布局。

HarmonyOS 7 / API 26 的 ArkUI 变更中新增了 FoldSplitContainer,提供 primary / secondary / extra 三个区域,以及展开态、悬停态和折叠态各自的布局参数;HoverModeStatus 里还能拿到 foldStatus、isHoverMode、appRotation 和 windowStatusType。这正好比我自己在页面里硬算上下高度更合适。

二、先让 FoldSplitContainer 只负责“区域”,不要再接管业务状态

这一篇新建:

components/
├── HoverOverviewPane.ets
├── HoverTaskPane.ets
└── HoverFoldShell.ets

managers/
└── HoverLayoutCoordinator.ets

FoldSplitContainer 只负责区域关系。

当前示意代码解决的是“把折叠状态和业务内容继续耦合”的问题:

import {
  FoldSplitContainer,
  PresetSplitRatio
} from '@ohos.arkui.advanced.FoldSplitContainer'

@Component
export struct HoverFoldShell {
  @Prop selectedItemId: string = 'insight_042'

  @Builder
  primaryRegion() {
    HoverOverviewPane({
      taskId: 'adapt_20261002_03'
    })
  }

  @Builder
  secondaryRegion() {
    HoverTaskPane({
      selectedItemId: this.selectedItemId
    })
  }

  build() {
    FoldSplitContainer({
      primary: () => {
        this.primaryRegion()
      },
      secondary: () => {
        this.secondaryRegion()
      },
      expandedLayoutOptions: {
        horizontalSplitRatio:
          PresetSplitRatio.LAYOUT_2V3
      },
      hoverModeLayoutOptions: {
        horizontalSplitRatio:
          PresetSplitRatio.LAYOUT_1V1,
        showExtraRegion: false
      },
      foldedLayoutOptions: {
        verticalSplitRatio:
          PresetSplitRatio.LAYOUT_1V1
      }
    })
  }
}

这里最重要的不是 LAYOUT_1V1。而是 primary / secondary 里面传入的仍然是业务组件。选中的 insight_042 继续来自 Store,不因为容器从展开态切到 HoverMode 就重新创建业务数据。

如果后面退出悬停态,容器可以重排区域,但业务 ID 仍然存在。

三、折痕安全间距要成为布局参数,而不是 UI 里临时塞 Margin

真正跑真机以后,我发现折痕附近最难处理的不是“区域高度”,而是安全间距。

第一版我在上区域底部写 .margin({ bottom: 18 }),下区域顶部再写 .margin({ top: 18 })。单个页面能解决,换一个内容页又要重新写。

所以现在 HoverLayoutCoordinator 统一生成:

primaryRegionHeight
hingeSafeGap
secondaryRegionHeight

本轮最终是:

392vp
36vp
487vp

这一段代码解决的是“安全距离散落在多个页面”的问题:

export interface HoverRegionSnapshot {
  taskId: string
  primaryVp: number
  hingeGapVp: number
  secondaryVp: number
  hoverReady: boolean
}

export class HoverLayoutCoordinator {
  private readonly hingeGapVp: number = 36

  resolve(
    totalHeightVp: number
  ): HoverRegionSnapshot {
    const usable =
      totalHeightVp - this.hingeGapVp

    const primary =
      Math.floor(usable * 0.446)

    const secondary =
      usable - primary

    return {
      taskId: 'adapt_20261002_03',
      primaryVp: primary,
      hingeGapVp: this.hingeGapVp,
      secondaryVp: secondary,
      hoverReady: true
    }
  }
}

这里的 36vp 和比例都是 InsightBoard 当前内容下的工程值,不是系统强制参数。官方 API 提供容器和分区能力,具体哪一块内容需要多少空间,仍然要根据自己的产品信息密度做验证。

四、悬停态里,上半屏和下半屏不应该做同一种事

InsightBoard 的 HoverMode 最后没有把原来的 MEDIUM 双栏上下旋转 90 度。

我重新看了一遍用户动作。普通展开态更接近“看列表 → 点内容 → 看详情”,悬停态更接近“上屏持续看状态,下屏持续操作”。

因此最终拆成两层。

1. Primary

放今日任务、趋势图、当前洞察摘要、任务进度。这块更像观察区。

2. Secondary

放任务列表、筛选、继续 / 暂停、详情入口。这块更像交互区。

以前 insight_042 的详情大图在右侧;进入悬停态后,我没有把它塞进上屏,而是保留一张简化摘要卡,完整详情仍然通过下屏入口打开。

实际用下来,这比“尽量把原页面内容全部保留”更自然。多形态适配真正增加的应该是场景效率,不是把同一张页面切成两块。

五、onHoverStatusChange 只更新姿态快照

FoldSplitContainer 提供 onHoverStatusChange,回调状态包括是否进入悬停模式、折叠状态、应用旋转和窗口状态。

我没有在这个回调里做网络请求、重新拉列表或者清空 Navigation。

代码只更新姿态:

private onHoverStatusChange = (
  status: HoverModeStatus
): void => {
  this.hoverSnapshot = {
    taskId: 'adapt_20261002_03',
    isHoverMode: status.isHoverMode,
    foldStatus: status.foldStatus,
    appRotation: status.appRotation,
    windowStatusType:
      status.windowStatusType
  }

  this.scheduleLayoutApply()
}

它和 02 的原则完全一致:系统事件先更新系统状态,Coordinator 统一计算,UI 只消费 Snapshot。以后窗口退出全屏、进入自由窗口时,悬停姿态虽然还在,但当前窗口可能已经不适合使用上下分区,Coordinator 可以根据 windowStatusType 决定是否退回普通 MEDIUM。

六、这次我专门测了“悬停态打开以后再转屏”

第一轮测试只覆盖 EXPANDED → HALF_FOLDED,没有问题。

把设备在悬停态里改变方向以后,页面第一次出现了内容抖动。

原因是 appRotation 发生变化后,旧的 region snapshot 还在使用,上下区域会短暂拿到上一方向的尺寸。

最后我把 Apply 条件收紧:

isHoverMode
+
appRotation
+
windowStatusType
+
viewport snapshot

四个值都稳定以后才应用一次。

这和前一篇的 80ms 合并思路类似,但不是简单复制 Timer。03 更关注“状态是否完整”。如果少一个字段,就先不重排。

七、DevEco 图里,折痕区域必须能从日志解释出来

03 的 DevEco 图不再看 412 / 824 两个普通断点,而是固定看:

taskId=adapt_20261002_03
isHoverMode=true
foldStatus=HALF_FOLDED
primary=392vp
hingeGap=36vp
secondary=487vp
status=HOVER_READY

当前 HiLog:

hoverStatus isHoverMode=true
foldStatus=HALF_FOLDED
appRotation=0
windowStatus=FULL_SCREEN
resolveRegion primary=392vp hingeGap=36vp secondary=487vp
selectedItem preserved insight_042
scrollOffset preserved 1280vp
transitionCost=38ms
status=HOVER_READY

如果页面上屏高度正常,但详情跳回第一条,说明不是 FoldSplitContainer 的问题,而是 Store 恢复出了问题。如果业务状态正常,但按钮贴着折痕,才回头查 region snapshot。

八、最终运行结果不是“分成上下两块”,而是上下两块各自有明确职责

最终运行图:

本轮统一状态:

taskId: adapt_20261002_03
device: Foldable
foldStatus: HALF_FOLDED
isHoverMode: true
windowStatus: FULL_SCREEN
primary: 392vp
hingeSafeGap: 36vp
secondary: 487vp
primaryRole: OVERVIEW
secondaryRole: ACTION
selectedItem: insight_042
scrollOffset: 1280vp
transitionCost: 38ms
status: HOVER_READY

这一篇做完以后,InsightBoard 的折叠屏适配终于不只是“展开 / 折叠”。悬停态开始变成真正不同的交互场景。

下一篇会从折叠屏离开,继续同一个 InsightBoard,但是把应用放到平板、PC / 2in1 和自由窗口里。重点不是再加一个断点,而是回答一个更实际的问题:窗口已经足够大以后,到底应该增加什么信息,哪些导航应该常驻,哪些内容应该继续保持克制。

也就是说,04 会处理“大屏信息密度”,而不是简单把 EXPANDED 拉得更宽。

参考资料

  • HarmonyOS 多设备通用适配指南:https://developer.huawei.com/consumer/cn/multidevice/adaptive-apps/
  • ArkUI API 26 FoldSplitContainer 变更:https://developer.huawei.com/consumer/cn/doc/doccenter-release-notes/js-apidiff-arkui-b031
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐