摘要: 折叠屏展开那一刻,布局不是"被放大",而是被重构——单栏切双栏、列表变分栏、顶部 Tab 变侧边导航。本文用邮件类应用适配折叠屏的真实过程为案例,讲清展开/折叠状态监听、双栏重构、半折叠悬停态处理,以及最容易被忽略的一件事:状态在布局重建后不能丢。附 5 个真实踩坑(现象→根因→解决→教训)与自查清单。

适用版本: HarmonyOS NEXT 7.x / ArkUI / API 14+(2026 年稳定版,接口以当前 SDK 文档为准)

环境说明: 本文案例来自一款邮件类应用适配折叠屏的真实过程,在 Mate X 系列真机上验证;模拟器不触发折叠态回调(原因见坑 1 的教训),所有结论均来自真机实测。

开篇:以为只是"单栏换双栏",结果踩了 5 个坑

“折叠屏展开后,列表和详情能不能并排显示?”

产品在需求评审会上问出这句话的时候,我心里想的是:这不就是把 Column 换成 Row 的事吗。真机到手第一天我就知道想简单了——展开瞬间布局闪烁、分栏联动状态丢失、折叠回来列表滚回顶部,三个问题在第一轮自测里全冒出来了。

这轮适配的业务背景是这样的:应用折叠态是邮件列表单栏,点击进详情页跳转;展开态产品要求列表 + 详情双栏并排,左边选中右边直接刷新,不再跳转。交互模式的差异比布局差异更大——折叠态是"导航式"的,展开态是"分栏式"的,两套交互对应的组件结构、状态归属完全不同。

适配做完回头看,折叠屏真正的工作量根本不在布局,而在状态管理:选中了哪封邮件、列表滚到了哪一项、详情区当前显示什么——这些状态在布局重建时全都会丢,丢一个用户就能感知到。本文按当时的排查顺序展开,5 个坑都是真机上一个一个撞出来的。

图:折叠屏展开/折叠状态切换效果 单栏↔双栏 + 布局闪烁现象


一、状态监听:先搞清楚系统给了你什么

折叠屏适配的入口是 display 模块的 foldDisplayModeChange 回调,它告诉你设备当前处于哪种显示模式。先看要处理的三种状态:

显示模式物理形态应用布局
FOLDED折叠闭合/外屏单栏列表 + 跳转详情
UNFOLDED完全展开左列表 + 右详情双栏
HALF_FOLDED半折叠悬停上下分屏(悬停态布局)

FOLDED

UNFOLDED

HALF_FOLDED

foldDisplayModeChange
回调

isFolded?

折叠态
单栏列表 + 跳转详情

展开态
左列表 + 右详情

悬停态
上下分屏布局

状态必须跨越
布局重建存活

F

基础监听代码不长,但有一个当时没意识到的隐患(后面坑 1 会讲):回调可能连发多次。

import { display } from '@kit.ArkUI';

@Entry
@Component
struct MailApp {
  @State foldMode: display.FoldDisplayMode = display.FoldDisplayMode.UNKNOWN;
  @State isFolded: boolean = true;

  aboutToAppear() {
    display.on('foldDisplayModeChange', (mode: display.FoldDisplayMode) => {
      this.foldMode = mode;
      this.isFolded = mode === display.FoldDisplayMode.FOLDED;
    });
  }

  build() {
    if (this.isFolded) {
      this.foldedLayout()   // 折叠态:单栏
    } else {
      this.unfoldedLayout() // 展开态:双栏
    }
  }
}

这里刻意把 isFolded 做成 @State 挂在页面根组件上,而不是放在列表或详情子组件里——原因是布局切换时子组件会整体重建,状态挂在谁身上,决定了它能不能活过重建。这是整个适配里最重要的一条设计决策,第二节展开。

折叠屏适配实战——展开/折叠状态下的 UI 重构(HarmonyOS 7.x)|图 1


二、两套布局:折叠态单栏,展开态双栏

两套布局的差异不只是容器方向,交互模型也不同:折叠态点列表是跳转,展开态点列表是刷新右侧。所以列表组件的 onSelect 回调在两种布局里干的事完全不一样。

折叠态:单栏 + 路由跳转,和手机版完全一致:

@Builder foldedLayout() {
  Navigation() {
    MailList({ onSelect: (mail: Mail) => {
      this.router.pushUrl({ url: 'pages/MailDetail', params: { id: mail.id } });
    }})
  }
}

展开态:双栏分栏,点击只更新 selectedMail,右侧响应式刷新:

@Builder unfoldedLayout() {
  Row() {
    Column() {
      MailList({ onSelect: (mail: Mail) => {
        this.selectedMail = mail;  // 不跳转,直接更新右侧
      }})
    }.width('40%')

    Column() {
      if (this.selectedMail) {
        MailDetail({ mail: this.selectedMail })
      } else {
        EmptyState()  // 未选择时占位
      }
    }.width('60%')
  }
}

注意 selectedMail 声明在页面根组件上(第一节的结构体里),不在 MailList 或 MailDetail 内部。这样无论布局怎么切、组件怎么重建,"用户当前看的是哪封邮件"这个事实始终由根组件持有——坑 2 的解法就是从这里来的。

宽度用 40% / 60% 的百分比而不是固定断点值,是当时踩过一次小坑后的选择:展开态在不同型号折叠屏上的物理宽度差异不小,固定值在大屏上留白难看,百分比在分屏多窗场景下也更稳。


三、5 个踩坑案例(现象 → 根因 → 解决 → 教训)

子组件内

父组件内

折叠/展开切换

选中态放在哪

布局重建后选中态丢失

布局切换不影响选中态

用户看到右侧详情变空白

切换后仍显示原选中项

修复:selectedId 上提到父组件

滚动位置同样需 scroller 记忆

1. 展开瞬间布局闪烁,单栏双栏交替闪现

现象:真机上展开折叠屏,屏幕先闪一下单栏布局,再切成双栏,肉眼可见的跳变。概率性出现,大约三四次展开能撞到一次。

根因:打日志发现 foldDisplayModeChange 在一次展开动作里触发了 2-3 次(传感器采样抖动),每次回调都直接改 isFolded,布局跟着连切。

解决:对回调做 200ms 防抖,只认最后一次状态:

private debounceTimer: number = -1;
display.on('foldDisplayModeChange', (mode) => {
  clearTimeout(this.debounceTimer);
  this.debounceTimer = setTimeout(() => {
    this.isFolded = mode === display.FoldDisplayMode.FOLDED;
  }, 200);
});

教训:物理状态回调都不该裸用。200ms 是实测下来的平衡值——再大会让展开响应显得迟钝,再小挡不住抖动。另外防抖只解决"连切",不解决"切错",所以兜底逻辑里还保留了按实际窗口宽度二次校验(见边界一节的分屏场景)。

2. 分栏联动状态丢失,展开选的邮件折叠回来就没了

现象:展开态选了一封邮件正在右侧看详情,折叠再展开,详情区变回空占位。

根因:布局切换时 if/else 走了不同分支,MailDetail 子组件被销毁重建,它内部持有的邮件状态跟着没了。

解决:状态上提到父组件。选中项 selectedMail 用 @State 挂在页面根组件(第一节已示),子组件只通过构造参数接收,自己不持有"哪封邮件"的事实:

@State selectedMail: Mail | null = null;  // 父组件持有,不随布局重建丢

教训:折叠屏适配的第一原则是先理状态归属,再动布局。凡是需要跨越折叠/展开存活的状态——选中项、表单草稿、播放进度——一律上提到布局切换点之上,靠组件层级而不是组件生命周期来保状态。

3. 折叠回来列表滚动位置跳回顶部

现象:列表滚到第 80 封,展开看详情,折回来列表回到第 1 封。

根因:和坑 2 同源——List 随布局分支销毁重建,滚动位置是组件内部状态,重建即重置。

解决:用 Scroller 在切换前记录、重建后恢复。保存动作挂在布局切换的回调里而不是组件的 aboutToDisappear,因为后者在重建时序上不可靠:

private listScroller: Scroller = new Scroller();
@State savedScrollIndex: number = 0;

// 布局切换前保存
onFoldModeWillChange(mode: display.FoldDisplayMode) {
  this.savedScrollIndex = this.listScroller.getItemIndex();
}
// 重建后恢复
aboutToAppear() {
  this.listScroller.scrollToIndex(this.savedScrollIndex);
}

教训:滚动位置和选中项一样属于"跨重建状态",但它的恢复时机更讲究——要在新布局完成首帧布局之后再滚,滚早了会被当成无效调用。这里用 scrollToIndex 而不是按像素偏移恢复,因为不同形态下列表项高度一致,按索引恢复更稳。

4. 半折叠态(悬停)没处理,布局直接错乱

现象:设备半折叠立在桌上时,界面按展开态双栏渲染,下半截被铰链角度遮住,用户看到的是"半个应用"。

根因:第一版只按 isFolded 二分处理,HALF_FOLDED 落进了 else 分支被当成展开态。

解决:补悬停态分支。悬停态天然适合"上屏预览 + 下屏操作"的上下分屏,邮件场景上屏显示正文、下屏放操作按钮:

if (this.foldMode === display.FoldDisplayMode.HALF_FOLDED) {
  this.halfFoldedLayout()  // 悬停态:上下分屏
}

教训:折叠屏不是"折叠/展开"两个态,是三个态。悬停态反而是最容易做出差异化的场景——设备立在桌上免手持,是看视频、看菜谱、视频会议的天然形态,值得单独设计而不是兜底。

5. 展开态点列表项,详情区跟着响应了一次点击

现象:展开态快速点列表项时,右侧详情区偶尔闪一下点击态,像被"穿透"了。

根因:双栏用 Row 嵌套,左右两栏之间没有隔离点击事件的冒泡路径,列表项的点击在快速操作时串到了详情区的手势上。

解决:两栏各自显式声明命中测试行为,切断跨栏冒泡:

Column() { /* 列表 */ }.hitTestBehavior(HitTestMode.Default)
Column() { /* 详情 */ }.hitTestBehavior(HitTestMode.Default)

教训:单栏时代不存在的问题,分栏之后全来了。凡是"一块屏幕上同时活着两个可交互区域"的布局,事件边界都要显式声明,不能依赖默认行为的巧合。


四、折叠屏适配自查清单

排查路径按"异常发生在哪一步"组织,从上往下走:

展开瞬间

展开后

是

否

是

否

否

是

折叠屏 UI 异常

异常发生在哪一步

布局闪烁
状态切换加 200ms 防抖

右侧分栏内容是否空白

选中态未上提
把 selectedId 提到父组件

折叠回来位置是否错乱

未记忆滚动位置
用 scroller 记录并恢复

半折叠态是否正常

漏了 HALF_FOLDED
补悬停态布局

真机复测:Mate X 系列

逐条核对:

  • 监听 foldDisplayModeChange 并做了防抖
  • 折叠态 / 展开态 / 半折叠态三态都有布局
  • 选中项、表单草稿等跨重建状态已上提到父组件
  • 列表滚动位置有记忆与恢复
  • 双栏之间显式隔离了点击事件
  • 真机折叠屏实测过(模拟器不算数,原因见下)

折叠屏适配实战——展开/折叠状态下的 UI 重构(HarmonyOS 7.x)|图 2


五、边界与已知限制

限制项具体表现规避方式
模拟器不可用模拟器不触发 foldDisplayModeChange折叠态必须真机验证(Mate X 系列)
半折叠态不稳悬停态回调在不同机型上触发时机不一致防抖 + 兜底布局,不能只按 isFolded 二分
窗口配置变更展开触发 Ability 配置变更,未声明会重建页面module.json5 声明支持的配置项,状态上提
多窗分屏分屏时宽度可能小于展开态,按折叠状态判断会误判以实际窗口宽度为准,而非折叠状态
铰链遮挡铰链/折痕区域内容被遮挡用安全区 API 避开不可显示区域

其中"多窗分屏"这一条值得强调:分屏模式下应用宽度可能比折叠态还窄,但折叠状态是"展开"。所以布局决策的最终依据应该是窗口实际宽度对应的断点,折叠状态只作为交互模式(跳转 vs 分栏)的参考。两条信息各管一件事,混用就会在分屏场景翻车。


小结

折叠屏适配真正的难点不是"单栏换双栏"的布局切换,而是状态在布局重建后不能丢:选中项、滚动位置、分栏联动,这三处一旦随页面重建就没了,体验立刻崩。

落地顺序建议:先监听(加防抖)→ 状态上提 → 双栏布局 → 补半折叠态和铰链安全区。按这个顺序走,5 个坑里至少 3 个能提前规避。

评论区交流:你的折叠屏适配踩过什么坑?尤其是悬停态,你们的场景里是怎么设计的?欢迎评论区聊聊。

Logo

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

更多推荐