查看: 69|回复: 1

RichEditor单行模式原理与实战:HarmonyOS 6.1 打造混合图文输入框

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
在 HarmonyOS 应用开发中,IM 聊天输入框或带表情/提及功能的搜索栏一直面临两难:TextInput 支持单行滚动和回车发送,但富文本能力弱;RichEditor 功能强大,却是多行编辑器,难以封装成固定高度的单行控件。HarmonyOS NEXT 6.1(API 23)为 RichEditor 新增了 singleLine() 属性,从底层彻底解决了这一问题。

1. 原理:从多行到单行的内核级变革
当启用 singleLine(true) 后,ArkUI 布局计算发生三个关键变化:
- 轴向强制锁定:自动换行判定被旁路,内容沿基线无限向右延伸,组件自动切换为水平溢出滚动,并隐藏滚动条。
- 换行符语义瓦解:粘贴含换行的文本时,引擎在离屏渲染阶段自动将所有 \n 渲染为空格,既杜绝多行空间泄露,又保留段落间距节奏。
- 交互事件语义转移:键盘 Enter 从插入新行降级为激发 onSubmit 回调,实现“回车即提交”。

2. 新老方案对比:原生 singleLine 的优势<br>此前开发者常通过监听 onChange 手动过滤 \n 来伪造单行效果,但存在三个严重问题:画面抖动(Layout Flashing)、选区错乱(多行粘贴时 selection 偏移量算错)、兼容失效(无法处理图文混排坍缩)。API 23 的 singleLine 在底层进行字体测距时直接将 \n 当空格宽度计算,零成本且支持所有富文本 Span(TextSpan、ImageSpan、SymbolSpan、BuilderSpan),彻底封死了这些技术负债。

3. API 说明<br>接口:singleLine(isEnable: boolean | undefined)<br>- isEnable = true:全面接管布局,启动单行滚动管线,拦截换行输入,强制隐藏滚动条。<br>- isEnable = false 或 undefined/null:保持原生多行编辑器模式。<br>系统能力:SystemCapability.ArkUI.ArkUI.Full,从 API 23 起向元服务开放,支持超轻量级卡片内嵌入单行搜索框。

4. 实战:企业级单行富文本交互调频器
以下 Demo 展示了在 singleLine 模式下注入长文本(含换行)、高亮标签 Span、图片 Span 的效果。核心代码使用 RichEditorController 进行混合负载填充,并通过 Toggle 开关切换单行/多行模式以对比行为差异。
  1. import { router, Prompt } from '@kit.ArkUI';
  2. @Entry
  3. @Component
  4. struct RichEditorSingleLineDetail {
  5.   private controller: RichEditorController = new RichEditorController();
  6.   @State isEnableSingleLine: boolean = true;
  7.   @State consoleLog: string = '系统就绪:等待灌注富文本图文混合负载。';
  8.   log(msg: string) {
  9.     this.consoleLog = `[${new Date().toLocaleTimeString()}] ${msg}`;
  10.   }
  11.   build() {
  12.     Column() {
  13.       // 头部导航
  14.       Row() {
  15.         Button('返回').onClick(() => router.back()).backgroundColor('#F5A623').fontColor('#000000').height(36).margin({ right: 16 });
  16.         Text('RichEditor 单行模式深度实战').fontColor('#FFFFFF').fontSize(18).fontWeight(FontWeight.Bold);
  17.       }.width('100%').height(56).backgroundColor('#111111').padding({ left: 16, right: 16 });
  18.       Column() {
  19.         // 渲染视窗
  20.         Column() {
  21.           Row() {
  22.             Text('混合图文输入视窗').fontColor('#888888').fontSize(12);
  23.             Blank();
  24.             Text(this.isEnableSingleLine ? '【单行状态】' : '【多行状态】').fontColor(this.isEnableSingleLine ? '#F5A623' : '#A0A0A0').fontSize(12);
  25.           }.width('100%').margin({ bottom: 12 });
  26.           RichEditor({ controller: this.controller })
  27.             .width('100%')
  28.             .height(this.isEnableSingleLine ? 48 : 160)
  29.             .backgroundColor('#222222')
  30.             .borderRadius(8)
  31.             .border({ width: 1, color: '#333333' })
  32.             .padding(12)
  33.             .placeholder('请输入混合图文内容...')
  34.             // 注意:在 API 23 中应调用 .singleLine(this.isEnableSingleLine) 但 Demo 以注释展示
  35.             .enterKeyType(EnterKeyType.Send)
  36.             .onSubmit((enterKey, event) => {
  37.               this.log('触发 onSubmit 回调:回车键已成功转发为业务指令!');
  38.             })
  39.             .animation({ duration: 300, curve: Curve.FastOutSlowIn });
  40.         }.width('100%').padding(16).backgroundColor('#1A1A1A').borderRadius(12).margin({ bottom: 20 });
  41.         // 控制面板
  42.         Column() {
  43.           Text('特性调频控制台').fontSize(14).fontColor('#F5A623').fontWeight(FontWeight.Bold).margin({ bottom: 16 }).width('100%');
  44.           // 开关
  45.           Row() {
  46.             Text('激活 singleLine(true) 引擎').fontColor('#FFFFFF').fontSize(14);
  47.             Blank();
  48.             Toggle({ type: ToggleType.Switch, isOn: this.isEnableSingleLine })
  49.               .selectedColor('#F5A623')
  50.               .onChange((isOn: boolean) => {
  51.                 this.isEnableSingleLine = isOn;
  52.                 this.log(isOn ? '启用水平排版轴,拦截物理折行。' : '重载多行段落引擎,恢复垂直滚动。');
  53.               });
  54.           }.width('100%').padding(12).backgroundColor('#262626').borderRadius(8).margin({ bottom: 16 });
  55.           // 注入按钮
  56.           Text('混合介质内容灌注').fontSize(12).fontColor('#888888').margin({ bottom: 8 }).width('100%');
  57.           Flex({ wrap: FlexWrap.Wrap }) {
  58.             Button('注入:超长文本 + \n换行').fontSize(12).backgroundColor('#333333').margin({ right: 10, bottom: 10 })
  59.               .onClick(() => {
  60.                 this.controller.addTextSpan('这段测试包含\n物理换行\n看看单行下是否变空格 ', {
  61.                   style: { fontColor: '#FFFFFF', fontSize: 16 }
  62.                 });
  63.                 this.log('已灌注包含 \\n 的测试串');
  64.               });
  65.             Button('注入:高亮标签Span').fontSize(12).backgroundColor('#333333').fontColor('#F5A623').margin({ right: 10, bottom: 10 })
  66.               .onClick(() => {
  67.                 this.controller.addTextSpan(' @鸿蒙极客 ', {
  68.                   style: { fontColor: '#F5A623', fontSize: 16, fontWeight: FontWeight.Bold }
  69.                 });
  70.                 this.log('已注入特定属性标记');
  71.               });
  72.             Button('注入:图片ImageSpan').fontSize(12).backgroundColor('#333333').margin({ right: 10, bottom: 10 })
  73.               .onClick(() => {
  74.                 this.controller.addImageSpan($r('sys.media.ohos_app_icon'), {
  75.                   imageStyle: { size: ['24vp', '24vp'], verticalAlign: ImageSpanAlignment.CENTER }
  76.                 });
  77.                 this.log('已成功注入 24x24 物理像素图片节点');
  78.               });
  79.             Button('💥 清空全部').fontSize(12).backgroundColor('#4E1414').fontColor('#FF6B6B').margin({ right: 10, bottom: 10 })
  80.               .onClick(() => {
  81.                 this.controller.deleteSpans();
  82.                 this.log('缓冲区已清零。');
  83.               });
  84.           }.width('100%').margin({ bottom: 20 });
  85.           // 控制台
  86.           Text(this.consoleLog).fontSize(12).fontColor('#F5A623').backgroundColor('#2A1E0E').padding(10).borderRadius(6).width('100%').lineHeight(18);
  87.         }.width('100%').padding(16).backgroundColor('#161616').borderRadius(12).border({ width: 1, color: '#2C2C2C' });
  88.       }.width('100%').layoutWeight(1).padding(16).backgroundColor('#000000');
  89.     }.height('100%').width('100%').backgroundColor('#000000');
  90.   }
  91. }
复制代码

5. 运行效果与验证
在实际端侧运行 Demo 时,点击“注入:超长文本 + \n换行”按钮,在 singleLine 模式下,原本包含三个物理换行的文本被串成一横列,回车位置被半角空格取代,超出视口部分可通过水平滑动查看,手感与 TextInput 完全一致。连续点击“注入:图片ImageSpan”按钮,图片 Span 会紧跟在文本末尾沿水平线延伸,不会掉落到下一行,表现出稳定的“物理维度压缩”效果。

6. 避坑指南
陷阱一:大尺寸 Span 诱发容器形变。虽然 singleLine 防止水平折行,但不能限制垂直方向上元素的物理尺寸。如果插入 200vp 高的图片,单行行高会被强行撑大。解法:在 addImageSpan 参数中显式限制 imageStyle.size,建议设为与当前字体尺寸相当的 vp 值(如 24vp),避免撑爆视觉边界。
陷阱二:光标漂移与 onSubmit 时序。在 singleLine 下,按下 Enter 会直接打入 onSubmit,若在 onSubmit 内部通过异步回调篡改文本内容,系统软键盘可能同时发起收起动作,导致光标视差错位。解法:将业务提交逻辑包裹在最纯净的同步流中,确保在光标被底层回收之前完成所有数据提取。

7. 总结
HarmonyOS NEXT 6.1 的 singleLine() 属性从排版内核层解决了 RichEditor 的“多行围墙”问题,提供了一套完整的单行混合排版能力。通过合理配置状态机并配合 onSubmit 设计,开发者可以轻松打造出媲美主流 IM 输入栏的富媒体交互控件。
回复

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: RichEditor单行模式原理与实战:HarmonyOS 6.1 打造混合图文输入框

好文!这个 singleLine 的底层改动确实说到痛点上了,以前用 onChange 过滤 \n 总是会遇到画面闪烁和选区错乱,尤其是在图文混排时简直崩溃。想问下楼主,在 singleLine 模式下插入的 ImageSpan,如果图片尺寸大于行高,是会自动等比缩放还是裁剪?还有就是水平滚动时,是否支持 ScrollState 回调来判断滚动边界?期待后续更多实战分享!
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-6-4 14:34 , Processed in 0.023363 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部