在 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 开关切换单行/多行模式以对比行为差异。
- import { router, Prompt } from '@kit.ArkUI';
- @Entry
- @Component
- struct RichEditorSingleLineDetail {
- private controller: RichEditorController = new RichEditorController();
- @State isEnableSingleLine: boolean = true;
- @State consoleLog: string = '系统就绪:等待灌注富文本图文混合负载。';
- log(msg: string) {
- this.consoleLog = `[${new Date().toLocaleTimeString()}] ${msg}`;
- }
- build() {
- Column() {
- // 头部导航
- Row() {
- Button('返回').onClick(() => router.back()).backgroundColor('#F5A623').fontColor('#000000').height(36).margin({ right: 16 });
- Text('RichEditor 单行模式深度实战').fontColor('#FFFFFF').fontSize(18).fontWeight(FontWeight.Bold);
- }.width('100%').height(56).backgroundColor('#111111').padding({ left: 16, right: 16 });
- Column() {
- // 渲染视窗
- Column() {
- Row() {
- Text('混合图文输入视窗').fontColor('#888888').fontSize(12);
- Blank();
- Text(this.isEnableSingleLine ? '【单行状态】' : '【多行状态】').fontColor(this.isEnableSingleLine ? '#F5A623' : '#A0A0A0').fontSize(12);
- }.width('100%').margin({ bottom: 12 });
- RichEditor({ controller: this.controller })
- .width('100%')
- .height(this.isEnableSingleLine ? 48 : 160)
- .backgroundColor('#222222')
- .borderRadius(8)
- .border({ width: 1, color: '#333333' })
- .padding(12)
- .placeholder('请输入混合图文内容...')
- // 注意:在 API 23 中应调用 .singleLine(this.isEnableSingleLine) 但 Demo 以注释展示
- .enterKeyType(EnterKeyType.Send)
- .onSubmit((enterKey, event) => {
- this.log('触发 onSubmit 回调:回车键已成功转发为业务指令!');
- })
- .animation({ duration: 300, curve: Curve.FastOutSlowIn });
- }.width('100%').padding(16).backgroundColor('#1A1A1A').borderRadius(12).margin({ bottom: 20 });
- // 控制面板
- Column() {
- Text('特性调频控制台').fontSize(14).fontColor('#F5A623').fontWeight(FontWeight.Bold).margin({ bottom: 16 }).width('100%');
- // 开关
- Row() {
- Text('激活 singleLine(true) 引擎').fontColor('#FFFFFF').fontSize(14);
- Blank();
- Toggle({ type: ToggleType.Switch, isOn: this.isEnableSingleLine })
- .selectedColor('#F5A623')
- .onChange((isOn: boolean) => {
- this.isEnableSingleLine = isOn;
- this.log(isOn ? '启用水平排版轴,拦截物理折行。' : '重载多行段落引擎,恢复垂直滚动。');
- });
- }.width('100%').padding(12).backgroundColor('#262626').borderRadius(8).margin({ bottom: 16 });
- // 注入按钮
- Text('混合介质内容灌注').fontSize(12).fontColor('#888888').margin({ bottom: 8 }).width('100%');
- Flex({ wrap: FlexWrap.Wrap }) {
- Button('注入:超长文本 + \n换行').fontSize(12).backgroundColor('#333333').margin({ right: 10, bottom: 10 })
- .onClick(() => {
- this.controller.addTextSpan('这段测试包含\n物理换行\n看看单行下是否变空格 ', {
- style: { fontColor: '#FFFFFF', fontSize: 16 }
- });
- this.log('已灌注包含 \\n 的测试串');
- });
- Button('注入:高亮标签Span').fontSize(12).backgroundColor('#333333').fontColor('#F5A623').margin({ right: 10, bottom: 10 })
- .onClick(() => {
- this.controller.addTextSpan(' @鸿蒙极客 ', {
- style: { fontColor: '#F5A623', fontSize: 16, fontWeight: FontWeight.Bold }
- });
- this.log('已注入特定属性标记');
- });
- Button('注入:图片ImageSpan').fontSize(12).backgroundColor('#333333').margin({ right: 10, bottom: 10 })
- .onClick(() => {
- this.controller.addImageSpan($r('sys.media.ohos_app_icon'), {
- imageStyle: { size: ['24vp', '24vp'], verticalAlign: ImageSpanAlignment.CENTER }
- });
- this.log('已成功注入 24x24 物理像素图片节点');
- });
- Button('💥 清空全部').fontSize(12).backgroundColor('#4E1414').fontColor('#FF6B6B').margin({ right: 10, bottom: 10 })
- .onClick(() => {
- this.controller.deleteSpans();
- this.log('缓冲区已清零。');
- });
- }.width('100%').margin({ bottom: 20 });
- // 控制台
- Text(this.consoleLog).fontSize(12).fontColor('#F5A623').backgroundColor('#2A1E0E').padding(10).borderRadius(6).width('100%').lineHeight(18);
- }.width('100%').padding(16).backgroundColor('#161616').borderRadius(12).border({ width: 1, color: '#2C2C2C' });
- }.width('100%').layoutWeight(1).padding(16).backgroundColor('#000000');
- }.height('100%').width('100%').backgroundColor('#000000');
- }
- }
复制代码
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 输入栏的富媒体交互控件。 |