在折叠屏上做编辑类应用,横屏显示双栏、竖屏收成单栏,看起来是一个布局问题。真正麻烦的地方在于:用户可能已经改了半段草稿,只是转动设备或者调整窗口,右侧编辑组件被重新组织之后,输入框却回到了旧内容。页面依旧漂亮,文字却没了,这种问题比间距错几个像素更值得先解决。本文选择一个小型笔记收件箱 FoldDraft 来说明取舍。演示条目固定为 NOTE-207,标题“会议纪要”,本地未提交草稿是“会议纪要待核对”,操作编号 FD-061。它没有云同步,也没有模拟自动保存成功;界面的 LOCAL_DRAFT 仅表示当前页面持有尚未提交的文字。## 一、布局是视图,草稿才是业务事实### 1. 别让两份编辑器各自记内容最容易想到的写法,是横屏一套详情组件,竖屏另一套详情组件,每个组件用自己的 @State 存文本。这相当于创建了两份不互通的草稿:设备方向改变时,旧组件可能离开树,新组件从初始值起步。即使视觉上显示同一篇笔记,数据也不一定是同一份。FoldDraft 刻意把选中条目、编辑文本和提交状态提升到父页面。响应式布局只决定“在哪里画”,不决定“哪个文字是真的”。数据从同一个 draft 字段流向不同形态中的 TextArea,输入事件也写回同一个字段。这样,左右分栏是否存在,不影响草稿本身。第一段代码定义数据身份。id 是业务主键,不拿列表下标当主键;列表重排、搜索过滤都不会改变它。示例固定两个条目,避免把加载接口和布局问题混在一起。tsinterface DraftNote { id: string title: string initialText: string}const NOTES: DraftNote[] = [ { id: 'NOTE-207', title: '会议纪要', initialText: '会议纪要待核对' }, { id: 'NOTE-208', title: '产品清单', initialText: '等待逐项确认' }]function noteById(id: string): DraftNote | undefined { return NOTES.find((note: DraftNote) => note.id === id)}````NOTE-207` 的草稿是演示输入,不是从数据库恢复的真实记录。实际项目应让草稿对象与条目 ID 成对保存:切换条目之前先保存上一项的本地编辑缓存,切回时再取对应草稿。本文只展示同一条目在窗口变化期间的连续编辑,不把“内存未丢”扩大为“进程被杀后还能恢复”。### 2. 方向不等于折叠姿态媒体查询可监听屏幕方向,也可以用不同条件判断可用空间。这里选 `(orientation: landscape)` 做演示,因为它容易在模拟窗口切换时观察。它并不等同于获取折叠角度,更不能据此断言设备已经处于展开态。正式适配还需结合窗口尺寸、字体放大、分屏和实际内容宽度;横屏的小窗口仍可能放不下两栏。对工程而言,最重要的是把媒体查询监听绑定到当前 `UIContext`,避免依赖全局查询在多窗口场景中指错实例。官方 ArkUI 文档提供了 `getUIContext().getMediaQuery().matchMediaSync()` 和监听 `change` 的方式。我们只借用系统报告的条件结果,不自行定义所谓“HarmonyOS 7 折叠状态 API”。## 二、监听负责布局,不碰输入值第二段代码展示页面状态以及生命周期钩子。方向变化只写 `wide`,选中 ID 和草稿保持原样。保存 `MediaQueryListener` 的目的,是在组件消失时解除订阅;否则旧页面的回调可能还活着,页面重复进入以后出现两次处理。示例代码针对 Stage 模型 ArkUI 组件,片段需要放在实际工程页面对应的 `.ets` 文件内。tsimport { mediaquery } from ‘@kit.ArkUI’@Entry@Componentstruct DraftInboxPage { @State wide: boolean = false @State selectedId: string = ‘NOTE-207’ @State draft: string = ‘会议纪要待核对’ @State status: string = ‘LOCAL_DRAFT’ private listener?: mediaquery.MediaQueryListener private onLayoutChange = (result: mediaquery.MediaQueryResult): void => { this.wide = result.matches // 不在这里重置 selectedId 或 draft } aboutToAppear(): void { const query = this.getUIContext().getMediaQuery() this.listener = query.matchMediaSync(‘(orientation: landscape)’) this.wide = this.listener.matches this.listener.on(‘change’, this.onLayoutChange) } aboutToDisappear(): void { if (this.listener) { this.listener.off(‘change’, this.onLayoutChange) this.listener = undefined } } // 下段代码中的 @Builder 方法与 build() 接在这个 struct 内。}这里没有使用自定义定时器去轮询方向。`matches` 是当前匹配状态,`change` 提供后续变化;两者合起来覆盖了“页面第一次出现”和“页面在场期间旋转”两条路径。若只写回调、不读取初始匹配状态,用户一进页面就可能拿到错误的初始栏数。若只在初始化读一次、没有解绑监听,多次进出页面又容易留下旧订阅。编辑状态还有一个边界:本例只切换同一组件树的显示分支,并不会替用户建立进程级持久化。若产品承诺“断电也保留草稿”,应另设存储层和保存时机,不能把 `@State` 当作磁盘存储。这里把状态名明写成 `LOCAL_DRAFT`,也是为了不制造“已经上云”的错觉。## 三、让一个输入源适配两种画法### 1. 页面的右边只是编辑器的摆放位置在宽布局里,左边留给条目列表,右边是编辑区;窄布局则将列表与编辑区纵向排布。为简化示例,列表只作视觉对照,不开放跨条目切换,避免在尚未设计逐条草稿缓存之前误改业务身份。真实项目可以把 `selectedId -> DraftDocument` 放到父页面或共享状态容器,在点击其他条目时走独立的草稿切换函数。第三段代码只展示 `DraftInboxPage` 内追加的方法,和上一段共同构成一个组件。`TextArea.onChange` 把输入统一写回 `draft`。注意,`TextArea` 的 `text` 参数取值来自父级字段;横竖两种构图不应各自填死初始值。`ForEach` 使用稳定条目 ID 作为 key,避免列表重排时把视觉位置误当数据身份。ts@BuildernoteRail() { Column({ space: 12 }) { ForEach(NOTES, (note: DraftNote) => { Text(note.title) .fontSize(16) .padding(10) }, (note: DraftNote) => note.id) }.width(‘100%’)}@BuildereditorPane() { Column({ space: 12 }) { Text(任务 FD-061 · ${this.selectedId}).fontSize(14) Text(this.wide ? ‘LANDSCAPE_SPLIT’ : ‘PORTRAIT_STACK’) TextArea({ text: this.draft, placeholder: ‘输入草稿’ }) .height(190) .onChange((value: string) => { this.draft = value this.status = ‘LOCAL_DRAFT’ }) Text(this.status) Text(‘未提交:仅保留于当前页面内存’) }.padding(16)}build() { Column() { if (this.wide) { Row() { Column() { this.noteRail() }.width(‘34%’) Column() { this.editorPane() }.layoutWeight(1) } } else { Column() { this.noteRail(); this.editorPane() } } }.width(‘100%’).height(‘100%’)}```这段既不自动选中第一条,也不开放列表切换:如果旋转时重置选中项,用户原本编辑 NOTE-207,突然落回 NOTE-208,会产生更隐蔽的错配。对于多条独立草稿,应先实现 Map<id, draft> 或仓储对象,再允许列表切换;并在状态变更时记录“谁触发了切换”。控制输入源比不断修复跳栏后的文本更省事。上图是合成的 DevEco Studio 风格示意图,用来强调媒体查询回调与 @State 的关系。图片中的日志文本是根据此演示设计的观察点,不是实际 IDE 日志或设备运行证据。示意图中的“保存草稿”控件与 saveDraft success 只是视觉稿占位,示例代码未实现持久化功能,不应由它推断已经完成保存。调试时建议只记录方向条件、选中 ID 与编辑字符数,不要把整段用户笔记打进 HiLog。生产日志应注意敏感内容和脱敏。## 四、把旋转测试写成状态不变量与其验证“旋转后界面看着差不多”,不如列出四个必须成立的条件:横屏时是 LANDSCAPE_SPLIT,竖屏时是 PORTRAIT_STACK;无论怎么切,selectedId 仍为 NOTE-207;输入框仍显示“会议纪要待核对”;状态仍是 LOCAL_DRAFT,不能凭空变成 SYNCED。假设先在竖屏选中 NOTE-207 并修改文本,再变成横屏;正确的预期是双栏重排,草稿不变。接着回到竖屏,仍可继续编辑。如果文本消失,先检查是不是两套组件各自有 @State;如果 ID 改变,查看是否在媒体查询回调中重新初始化选中项;如果日志回调次数叠加,查生命周期的 off('change') 是否和 on('change') 对称。为了让读者看到状态而不是虚构的实测结果,手机配图固定展示竖屏示意:FD-061、NOTE-207、PORTRAIT_STACK、LOCAL_DRAFT、草稿“会议纪要待核对”。手机图中的“保存草稿”和“提交文档”同样属于未接线的演示控件。与横屏开发图相互补充,不将二者写成一次真实运行的前后截图。真实验收还应覆盖分屏拖窄、旋转过程中输入法打开、字体放大、应用进入后台、窗口销毁和进程重建。前两类着重看布局与光标可见性;后台及进程重建必须依靠正式持久化方案。媒体查询只告诉页面条件有没有满足,并不替编辑器做数据恢复。## 五、什么时候不该照搬这个例子适合说明布局状态与业务状态的边界,不能直接当作完整笔记产品。两栏是否出现不应永久绑定横屏;平板与桌面窗口需要基于可用宽度进一步判断。TextArea 的光标、选区和输入法组合状态也未做精细恢复;有富文本、多人协同、撤销栈时,需要在模型层持有更完整的文档编辑上下文。还有一个常见误解:按组件的 aboutToDisappear 解绑回调,和在应用生命周期中保存草稿不是同一回事。前者解决的是资源所有权和重复监听,后者解决的是数据可靠性。把两项拆开看,排查时就不会把数据丢失归咎于分栏 API。本文的代码与两张技术图属于可复核的工程演示,已按公开文档确认媒体查询调用形式,未在 DevEco Studio 中编译或真机验证。实际落地应先在目标 SDK 的声明文件和目标设备上核对生命周期回调,再为进程级恢复补上持久化测试。这一轮真正想保住的不是两栏样式,而是用户已经敲下的那段文字。参考资料: 华为 ArkUI 媒体查询开发指导;UIContext 多实例使用说明。

Logo

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

更多推荐