HarmonyOS ArkUI onAreaChange × TextArea:折叠屏双栏草稿保活与尺寸回跳抑制【鸿蒙心迹】
Demo:FoldDraft | 示例任务:FD-205 | 当前条目:T-205
方向:折叠屏多形态 / ArkUI 容器尺寸 / 编辑器草稿
声明:全部截图、HiLog 与宽度数据属于构造演示;未声称真实折叠屏、特定机型或编译测试结果。

比起布局变化,我更在意那句没写完的话
做折叠屏适配时,人们很容易先关注屏幕展开之后多出多少列表、多出几个按钮。编辑类业务真正让用户难受的问题其实更朴素:在窄窗里刚输入一半的备注,展开屏幕变成双栏以后突然回到了空白;或者临时拖动分屏边界,右侧编辑器反复被重建,焦点乱跳,连输入法组合状态也受到影响。视觉上漂亮的双栏如果不能保住草稿,便算不上体验连续。FoldDraft 选择一个最小的素材复核场景,把“保留已经输入的七个汉字”放到比“切得多快”更高的优先级。
本项目的构造业务只有一条任务 T-205,草稿固定为“请补拍逆光样片”,共七个汉字。可用空间宽的时候左侧显示素材任务列表,右侧显示 TextArea 编辑区域;宽度较窄时退化成单栏,但草稿仍与同一个页面级状态绑定。用编号 FD-205 将 UI、控制台和图片串起来,便于复核。页面的模式名 DUAL、SINGLE 只是业务内部枚举,不是系统提供的折叠状态常量,也不能直接拿它判断铰链角度。
不直接写一个“宽度大于某数就双栏”
第一版常见写法是 width >= 650 ? DUAL : SINGLE。一旦组件测量值在 648 和 652 之间反复波动,就会连续切换两个分支。哪怕草稿数据没有丢,输入框结构不断替换也可能导致选择范围与焦点被刷新。解决思路是给模式切换留一个缓冲带:只有宽度达到 680 才从单栏进入双栏,缩到 620 以下才从双栏回到单栏;处于中间区域时保持原模式。这两个数字纯属本 Demo 的布局阈值,不是华为规定的折叠屏标准值,真正工程应结合设计稿、字号和设备预览测量调整。
// entry/src/main/ets/model/PanePolicy.ets
export enum PaneMode { SINGLE = 'SINGLE', DUAL = 'DUAL' }
export function decideMode(width: number, current: PaneMode): PaneMode {
if (!Number.isFinite(width) || width <= 0) return current;
if (current === PaneMode.SINGLE && width >= 680) return PaneMode.DUAL;
if (current === PaneMode.DUAL && width <= 620) return PaneMode.SINGLE;
return current;
}
这个纯函数和具体设备、编辑器、页面生命期没有耦合,可以单独喂入预设宽度序列。文中故意不用 deviceWidth 当作当前窗口宽度;在折叠屏、分屏和悬浮窗等场景,设备物理尺寸与组件实际可用宽度不是一个量。官方多窗口布局资料明确指出,应用需要根据窗口改变进行适配。对于当前页面,我们更需要的其实是编辑区域外层容器的实时尺寸,而不是一次性缓存的设备宽度。
把输入数据放在布局分支外面
ArkUI 的 onAreaChange 可以在组件区域变化时收到前后测量值。使用时要注意:Area.width 是布局信息,先经过 Number(...) 转换并验证有效性再交给决策函数;它报告的数值不能简单拿去与照片像素宽度比较。最关键的一笔是把 draft 放在页面组件的 @State 中,而不是临时声明在 if (DUAL) 或 else 的某一个子组件里。否则模式切换导致子树替换时,局部状态的生命周期会跟着变化。下面是便于阅读的关键页面片段,方法名称和 TextArea 写法均按 ArkUI 声明式组件整理。
// entry/src/main/ets/pages/Index.ets(示意页面片段)
import { PaneMode, decideMode } from '../model/PanePolicy';
@Entry
@Component
struct Index {
@State private mode: PaneMode = PaneMode.SINGLE;
@State private measuredWidth: number = 380;
@State private draft: string = '请补拍逆光样片';
private taskId: string = 'T-205';
@Builder editorPane() {
Column({ space: 12 }) {
Text(`任务 ${this.taskId} | 草稿 ${this.draft.length} 字`)
TextArea({ text: this.draft, placeholder: '填写复核意见' })
.height(220)
.onChange((value: string) => { this.draft = value; })
Button('保存草稿').onClick(() => {
console.info(`FD-205 save task=${this.taskId} len=${this.draft.length}`);
})
}.width('100%')
}
build() {
Column({ space: 14 }) {
Text(`FoldDraft · FD-205 · ${this.mode}`)
if (this.mode === PaneMode.DUAL) {
Row({ space: 12 }) {
Column() { Text('任务列表'); Text('T-205 已选中') }.width('35%')
Column() { this.editorPane() }.layoutWeight(1)
}.width('100%')
} else {
this.editorPane()
}
}
.padding(18)
.width('100%').height('100%')
.onAreaChange((oldArea: Area, newArea: Area) => {
const width = Number(newArea.width);
if (!Number.isFinite(width) || width <= 0) return;
this.measuredWidth = width;
const next = decideMode(width, this.mode);
if (next !== this.mode) {
this.mode = next;
console.info(`FD-205 width=${Math.round(width)} mode=${this.mode}`);
}
})
}
}

这段示例有意把 TextArea 的值继续回写到同一个 @State draft。UI 在两种模式下可以重新生成不同的组件子树,但下一次创建出来的输入框仍然读取相同文本。保存按钮当前只打印任务编号和长度,不伪装成已经写入磁盘;真实项目应该把“保存到持久化存储”和“页面内保活”拆开,否则应用被系统回收以后,单靠 @State 并不能保证草稿仍在。还要单独处理自动保存失败、用户切换任务时草稿归属和敏感输入的本地保护,这些都不属于尺寸适配 API 自身承担的职责。
用故障序列验证,不用口头说“挺稳”
下面的宽度序列是人为构造的,并非某台折叠屏的实测采样。让模式从 SINGLE 开始,依次输入 380、700、650、645、610、660、720、380。预期模式会在 700 进入 DUAL,650 和645 保持双栏,在610 切回单栏,660 保持单栏,720 再进入双栏,380 最后恢复单栏。最后核对 draft.length 仍然为 7,才能证明这组模拟交互中业务草稿状态没有随布局分支丢失。
// 构造宽度输入的纯函数检查(可以写入独立单元测试)
const widths: number[] = [380, 700, 650, 645, 610, 660, 720, 380];
let mode: PaneMode = PaneMode.SINGLE;
for (const w of widths) {
mode = decideMode(w, mode);
console.info(`FD-205 width=${w} mode=${mode}`);
}
// 构造序列最终:FD-205 width=380 mode=SINGLE
// 页面 draft 预期仍为:请补拍逆光样片(7字)

真正调试时应把事件日志分成三类:尺寸回调记录 width,模式切换记录 from/to,用户输入记录草稿字符数,且绝不在生产日志里打印真实的用户备注全文。若只输出“切换成功”,后面很难追踪到底是 UI 回调太频繁、根容器尺寸被错误设置成自适应,还是业务状态被重置。也不建议每次 onAreaChange 都写持久化存储;这会把高频几何变化误当成业务提交动作,增加不必要的 IO 和竞争条件。更好的分工是:组件尺寸只决定模式,文本更改只更新草稿,明确的保存事件才负责持久化。
关于悬停、键盘和真实设备的界限
折叠屏可能具有展开、折叠、悬停等状态,但这份 Demo 没有读取铰链角度,也没有调用硬件姿态传感器。把“宽度小”说成“已经折叠”是过度解释;分屏和悬浮窗也可能导致组件变窄。同样,布局阈值不能拿来猜测系统输入法是否正在组合输入。输入框文本连续性不等于焦点、光标和选区都能无损恢复;如果产品对编辑体验要求高,还需要围绕焦点控制、输入法遮挡、滚动位置和键盘出现后的可见区域做针对性测试。
另一个常见陷阱是只在首次进入页面时读取宽度。用户把窗口缩窄后,旧的宽度缓存已经失效,界面仍然以为自己处于大屏。使用 onAreaChange 的价值是关注当前实际布局,而不把折叠形态和窗口尺寸强行绑定。正式适配时应按华为多窗口与折叠屏体验设计建议,在手机、展开态、分屏、悬浮窗和不同字体缩放下测试容器变化,并记录真实设备型号与版本,不能把这里的示意图作为测试报告提交。
设计一次不依赖真机的回归记录
正式写回归用例时,不妨把尺寸、模式、选中任务和草稿长度写在同一条结构化日志里,再按触发原因分组。例如何时展开、何时拖动分屏、何时打开输入法,都能在真实测试时作为观察标签,但不必把这些标签伪装成由 onAreaChange 自动识别的硬件事件。还需要增加“切换当前任务后再返回”的用例:同一个页面如果复用草稿状态,却没有按任务 ID 隔离内容,那么第二条任务可能错误地显示第一条任务的备注。生产版本应该按业务 ID 管理草稿快照,并在提交成功之后才清理对应快照。
小结与参考
FoldDraft 的核心结论很有限,却很实用:控制布局的模式状态与承载草稿的业务状态应该分离;组件实时宽度驱动布局,迟滞阈值抑制边界反复抖动;用户已输入的内容保存在页面级状态中,不随着某个布局分支销毁。这样一个小 Demo 可以成为编辑类应用的适配起点,但不能替代真实硬件上的光标、键盘、窗口生命期和进程恢复测试。
核对资料:华为官方 ArkUI FAQ《关于获取组件宽高和位置》,包含 onAreaChange((oldValue: Area, newValue: Area) => ...) 示例(https://developer.huawei.com/consumer/en/doc/harmonyos-faqs-V14/faqs-arkui-9-V14);华为《Adapting the App Layout to Multi-Window》(https://developer.huawei.com/consumer/en/doc/harmonyos-guides-V14/multi-window-layout-adapt-V14);华为折叠屏体验设计原则(https://developer.huawei.com/consumer/cn/doc/doccenter-ux-design/design-principles-0000001957023989)。
更多推荐




所有评论(0)