HarmonyOS社交通讯应用开发19 : 文本编辑区 EditorComponent 文本编辑区 EditorComponent引言发布页的媒体区下面是文本编辑区——EditorComponent。它只做两件事但都做得很有讲究编辑一个TextInput输标题一个TextArea输正文输入内容实时同步到 AppStorage 全局状态拖拽接收支持把外部甚至其他设备拖来的纯文本直接落到标题或正文里。看似简单的两个输入框实际承担着全局状态同步与跨设备文字互通两个职责。本篇围绕这两个职责展开先讲TextInput/TextArea与状态同步再讲拖拽三件套draggable/allowDrop/onDrop与 UDMF 数据读取最后看键盘安全区的配合。知识点讲解TextInput 与 TextAreaArkUI 的两个文本输入组件本项目这样分工TextInput单行输入框用于标题。placeholder显示占位提示Add a title to be more likely to recommend (optional)text是受控文本。TextArea多行输入框用于正文可换行、可滚动。两者都提供.onChange(callback)在内容变化时回调最新文本。这里受控的含义是输入框显示什么由text参数决定而text又绑定在状态上——用户每敲一个字onChange把新值写回状态状态变化又驱动输入框刷新。这个状态 → 视图 → 状态的闭环是 ArkUI 声明式开发的核心心智模型开发者不直接操作组件只操作数据。AppStorage 双向同步EditorComponent用StorageLink绑定全局状态StorageLink(mainTitle)mainTitle: string ;StorageLink(textContent)textContent: string ;StorageLink是双向的组件内赋值会写回 AppStorageAppStorage 被外部修改比如接续恢复数据时组件会自动刷新。而onChange里那句AppStorage.set(mainTitle, mainTitle)看似冗余实则是显式加固——即使某些时序下StorageLink的自动回写未生效全局存储也必然拿到最新值。UDMF 与纯文本拖拽UDMFUnified Data Management Framework统一数据管理框架是鸿蒙跨应用、跨设备共享数据的标准拖拽时数据被封装成UnifiedData内部是若干UnifiedRecord记录每条记录有类型标识。纯文本对应uniformTypeDescriptor.UniformDataType.PLAIN_TEXT落地后可用unifiedDataChannel.PlainText.textContent取到字符串。拖拽接收的三个属性.draggable(true)组件可作为拖拽源可被拖出.allowDrop([类型列表])声明组件可接收哪些类型的数据.onDrop(callback)数据落入时触发参数是DragEvent。getDataFromUdmf带重试的取数封装DragEvent.getData()在拖拽刚落下时数据可能尚未就绪跨进程/跨设备传输有延迟首次调用可能拿到空。EditorComponent以及AddMedia封装了一个重试一次的工具方法getDataFromUdmf(event: DragEvent,callback: (data: DragEvent) void) {if(this.getDataFromUdmfRetry(event,callback)) { return;// 第一次就成功}// 1.5 秒后重试一次setTimeout(() { this.getDataFromUdmfRetry(event,callback); },1500); }getDataFromUdmfRetry内部 try/catch 尝试event.getData()能取到且记录非空才回调并返回 true。这套先试、失败再等 1.5s 补一次的模式是拖拽场景下的常见稳健写法。两个细节值得学一是getData()可能抛异常不只是返回空所以必须 try/catch二是先试一次、1.5 秒后再补一次的重试窗口是经验值——太短数据没传完太长用户感知卡顿。这套封装在 AddMedia 与 EditorComponent 两个组件里各写了一份属于小工具就地复制的务实取舍。拖拽事件流与 allowDrop 守门一次完整的拖放会依次触发一串事件onDragStart拖起→onDragEnter进入目标→onDragMove在目标内移动→onDrop放下→onDragEnd结束。本项目只实现了onDrop——对接收文字这个需求其他阶段可以全部省略。接收方只需三个配置.draggable(true)允许组件被拖出、.allowDrop([类型])声明可接收的类型、.onDrop(callback)处理落地数据。其中allowDrop是守门员类型不匹配的数据根本进不了onDrop系统在拖拽过程中就会用光标样式提示不可投放。项目里标题/正文只声明PLAIN_TEXT所以拖图片进来会被直接拒绝——用声明式过滤代替代码里的类型判断是更安全的做法。结合本项目源码分析组件骨架与键盘联动文件路径entry/src/main/ets/view/contentEditor/EditorComponent.ets。Component exportstructEditorComponent { StorageLink(BreakpointConstants.BREAKPOINT_NAME)currentBreakpoint: WidthBreakpoint WidthBreakpoint.WIDTH_LG; StorageLink(mainTitle)mainTitle:string ;// 标题全局同步StorageLink(textContent)textContent:string ;// 正文全局同步// 是否显示软键盘来自父组件 Link上一篇已讲Link isKeyboard: boolean; Link isShowLocalInfo: boolean; build(){Flex({direction: FlexDirection.Column }){TextInput({text:this.mainTitle,placeholder: $r(app.string.text_input_placeholder)}) {...}TextArea({text:this.textContent,placeholder: $r(app.string.richEditor_placeholder)}) {...} } .width(CommonConstants.FULL_PERCENT) .height($r(app.integer.flex_input_height))// 500vp.margin({ bottom:$r(app.integer.flex_input_margin)}) .layoutWeight(CommonConstants.DEFAULT_LAYOUT_WEIGHT)// 占据剩余空间.expandSafeArea([SafeAreaType.KEYBOARD])// 键盘避让.padding({ left: ..., right:...})// 断点响应式 padding} }.layoutWeight(1)是关键布局手段在父 Flex 中媒体区和工具栏高度固定文本区用权重 1 吃掉所有剩余高度保证不同屏幕尺寸下页面都填满。标题输入框编辑 拖拽接收TextInput({text:this.mainTitle,placeholder: $r(app.string.text_input_placeholder) }) .onChange((mainTitle:string) {this.mainTitle mainTitle;AppStorage.set(mainTitle, mainTitle);// 同步到全局接续时可打包带走}) .onFocus(() {this.isKeyboardtrue;// 获焦 → 键盘展开}) .onBlur(() {this.isKeyboardfalse;// 失焦 → 键盘收起}) .width(CommonConstants.FULL_PERCENT) .height($r(app.integer.text_input_height))// 48vp.fontSize($r(app.integer.text_size_body1))// 16fp.backgroundColor($r(sys.color.background_primary)) .constraintSize({minHeight: $r(app.integer.text_input_height) }) .margin({top: $r(app.integer.text_input_margin) })// —— 以下为拖拽接收配置 ——.draggable(true)// 标题可被拖出.allowDrop([uniformTypeDescriptor.UniformDataType.PLAIN_TEXT])// 只接收纯文本.onDrop((dragEvent?: DragEvent) {// 拖来的文字落进标题this.getDataFromUdmf((dragEventasDragEvent),(event: DragEvent) {try{letrecords:ArrayunifiedDataChannel.UnifiedRecord event.getData().getRecords();letplainText: unifiedDataChannel.PlainText records[0]asunifiedDataChannel.PlainText;this.mainTitle plainText.textContent;// 覆盖标题}catch(err) { hilog.error(DOMAIN,TAG,FORMAT,GetData failed. Cause code:${err.code}, message:${err.message}); } }) })要点allowDrop只声明了PLAIN_TEXT所以其他类型图片等拖到这里会被系统直接拒绝onDrop里取records[0]并强转PlainText读textContent覆盖标题。注意this.mainTitle ...与AppStorage的同步——StorageLink双向绑定会自动写回无需再手动set。正文输入框同样的配方TextArea({text:this.textContent,placeholder: $r(app.string.richEditor_placeholder)}) .width(CommonConstants.FULL_PERCENT) .height(CommonConstants.FULL_PERCENT) .id(CommonConstants.TITLE_ID)// titleId焦点调度目标见第 15 篇.fontSize($r(app.integer.text_size_body1)) .backgroundColor($r(sys.color.background_primary)) .constraintSize({minHeight: $r(app.integer.text_input_height)}) .margin({ top:$r(app.integer.text_input_margin)}) .onFocus(() { this.isKeyboard true; this.isShowLocalInfo false;// 开始输入时收起位置列表}) .onBlur(() { this.isKeyboard false; }) .onChange((textContent:string) { this.textContent textContent;AppStorage.set(textContent, textContent);// 同步全局}) .draggable(true) .allowDrop([uniformTypeDescriptor.UniformDataType.PLAIN_TEXT]).onDrop((dragEvent?: DragEvent) { this.getDataFromUdmf((dragEventasDragEvent),(event: DragEvent) {try{letrecords: ArrayunifiedDataChannel.UnifiedRecord event.getData().getRecords();letplainText: unifiedDataChannel.PlainText records[0]asunifiedDataChannel.PlainText; this.textContent plainText.textContent;// 覆盖正文} catch (err) {...} }) })正文框比标题多三个细节id(TITLE_ID)供changeFocus调度上一篇已讲onFocus顺带收起位置列表height(100%)撑满剩余空间。一个值得注意的设计取舍两个输入框的onDrop都是整体覆盖this.mainTitle plainText.textContent而不是光标处插入。对示例工程来说覆盖式写法最简单直观也足以演示跨设备拖文字进来的能力闭环。真实产品若要插入到光标处需要借助TextAreaController/TextInputController的caretPosition与文本拼接实现复杂度会明显上升——本项目没有这么做我们也不展开。AppStorage 双写为什么 onChange 里还要 set 一次细心的话会发现一个冗余StorageLink双向绑定已经会把this.mainTitle ...写回 AppStorage为什么onChange里还要显式AppStorage.set(mainTitle, mainTitle)这要从StorageLink的时序说起onChange回调发生时StorageLink的自动回写基于状态变量赋值这一动作触发直接AppStorage.set则是绕过组件状态、直达全局存储。两者叠加的意义在于解耦时序即使将来把标题改成Prop单向或普通成员变量AppStorage.set仍能保证全局数据同步不中断明确契约读者一眼就能看出这个输入框的值会进 AppStorage比隐式的装饰器行为更好理解。对于接续场景AppStorage 里的mainTitle/textContent是打包迁移的数据源双写等于给这条生命线上了双保险。这是示例工程里少见的看似重复、实则有意的写法值得揣摩。布局细节为什么是 Flex layoutWeight正文区高度用CommonConstants.DEFAULT_LAYOUT_WEIGHT值为 1配合.layoutWeight(1)在父级 Flex 中媒体区96vp 固定高与工具栏固定高先占位剩余高度全部给文本区。这保证了小屏上文本区被压缩、大屏上文本区被拉伸输入区域永远弹性填满。配合.height($r(app.integer.flex_input_height))500vp作为基准高度与constraintSize({ minHeight: 48 })下限即使极端窄屏也不会把输入框压没了——基准 弹性 下限三段式布局是编辑类页面最实用的防变形配方。与全局状态的联动全景把EditorComponent放进整页看文本数据的流动是用户输入/拖入文字 → onChange / onDrop →StorageLink自动回写 AppStorage(mainTitle/textContent) → 接续时打包进 ContentInfo → 迁移到目标设备 → 恢复时AppStorage 值被写入 → 输入框自动刷新文本区没有把数据私有化在组件内部而是直接写进全局存储——这正是为应用接续服务的发布到一半的内容标题、正文、媒体、位置全部在 AppStorage 里接续时一次性打包带走。小结EditorComponent用最朴素的两个输入框示范了三个高级主题全局状态同步StorageLinkAppStorage.set双保险让标题/正文随时可被接续打包拖拽文本接收draggableallowDrop(PLAIN_TEXT)onDropgetDataFromUdmf重试封装一套组合拳把别处拖来的文字落进输入框做到可靠键盘与布局onFocus/onBlur上报键盘状态、expandSafeArea([SafeAreaType.KEYBOARD])避让、layoutWeight(1)弹性填满。如果要用一句话记住本文那就是EditorComponent 是声明式思维的最小完整样本——不操作组件实例、不监听原生事件只用状态StorageLink和声明allowDrop/expandSafeArea/layoutWeight就表达完了全部交互逻辑。初学者写编辑类页面时可以对照本文检查三件事输入内容是否进了全局状态拖拽接收是否用声明式过滤键盘避让是否分区域处理三问都对体验基本不会差。至此发布页的上媒体中文本两层已就位下一篇分析最下方的BottomToolbar——位置添加与图标工具栏。本文引用源码entry/src/main/ets/view/contentEditor/EditorComponent.ets、entry/src/main/ets/constants/CommonConstants.ets