HarmonyOS鸿蒙Next中如何实现一个功能丰富的富文本编辑器?

HarmonyOS鸿蒙Next中如何实现一个功能丰富的富文本编辑器? RichEditor管理内容有“基于属性字符串”和“基于Span”两种主要方式。在需要频繁交互(如发布动态)的场景下,后者通常更合适。但是,如何让这些可交互的“Span”(如一个@好友的名字)在光标移动、删除操作时能被整体对待,并且还能携带好友ID等额外数据?

8 回复

这种 @好友 不建议只当普通字符串处理,最好把它设计成“原子节点 + 数据模型”。UI 上可以用 RichEditor 的 BuilderSpan/自定义 Span 展示,业务侧维护一份结构化数组,例如 { type: ‘mention’, uid, name, start, end }。

关键点有三个:

  1. 插入时记录 mention 的 uid/name 和当前范围,不只存显示文本。

  2. 删除/退格时监听删除前后的光标和范围,如果命中 mention 区间,就一次性删除整个节点,而不是让光标进入 @名字 中间。

  3. 提交内容时不要只读纯文本,建议序列化为“文本片段 + mention 节点”的结构;服务端保存 uid,展示时再还原成 BuilderSpan 或普通文本。

如果只是静态展示,用 TextSpan 够用;但发布动态、编辑、删除、携带好友 ID 这种交互,BuilderSpan + 独立数据模型会更稳。

更多关于HarmonyOS鸿蒙Next中如何实现一个功能丰富的富文本编辑器?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


有好用的数学公式文本三方库吗?

ArkUI组件RichEditor,这些都可以实现。而且官方给了非常多的《示例》

像这种@好友的,可用RichEditorController.addTextSpan()RichEditorController.addBuilderSpan()

如果addTextSpan,先定义个TextSpan。

interface TextSpan {
  value: string;//值
  spanRange: [number, number]; //起止位置
  type: string;//类型如 contact
  data: ESObject;//记录好友ID等
}

private textSpans: TextSpan[] = [];

删除整体就考虑用addBuilderSpan,在aboutToDelete监听下。示例参考:《自定义布局Span》

有轮子,别自己做了。
参考组件:OpenHarmony三方库中心仓

这个场景其实已经不是“普通富文本”了,而是:

结构化富文本编辑器

本质上:

@用户
#话题
链接
卡片
表情
图片

这些都应该被当成:

“原子节点”

而不是普通字符串。

——

所以如果你用:

StyledString

或者:

纯 TextSpan

后期一定会崩。

因为:

  • 删除不好处理
  • 光标会跑进 @名字 中间
  • 无法整体选中
  • 无法挂 metadata
  • 无法携带 uid/topicId

——

目前 HarmonyOS RichEditor 里,

真正适合做:

@好友
话题
卡片
复杂富文本

的方案其实是:

BuilderSpan + 自定义数据模型

不是普通 TextSpan。

——

推荐架构:

一、UI层:BuilderSpan

例如:

this.controller.addBuilderSpan(
  this.buildMentionSpan(user)
)

BuilderSpan 本质上是:

“可交互组件”

不是普通文本。

所以:

  • 可以整体删除
  • 可以整体选中
  • 可以点击
  • 可以自定义背景
  • 可以携带数据
  • 可以完全自定义行为

官方也明确推荐复杂交互用 Span/BuilderSpan。

——

二、数据层:不要依赖 RichEditor 内容

真正重要。

很多人会犯一个错误:

把 RichEditor 当数据源

这是错的。

RichEditor:

应该只是 View

真正的数据:

你必须自己维护。

例如:

type RichNode =
  | TextNode
  | MentionNode
  | TopicNode
  | ImageNode

例如:

interface MentionNode {
  type: 'mention'
  uid: string
  nickname: string
}

——

然后:

RichEditor 只是渲染这些节点

不是存储数据。

——

三、删除“整体删除”

这个是核心。

例如:

@张三

用户按删除:

你不应该删除:

而应该:

整个 MentionNode 删除

——

实现方式:

监听:

.onSelectionChange()

以及:

.onDeleteComplete()

然后:

自己维护 Span Range

例如:

interface SpanRange {
  start: number
  end: number
  data: MentionNode
}

当光标进入:

[start, end]

范围:

直接:

整块删除

——

这其实和:

iOS NSAttributedString
Android SpannableString

的实现思路一样。

——

四、不要让光标进入 Mention 内部

这是重点。

例如:

@张三

光标不应该:

@
张
三

之间移动。

而应该:

整个 Span 当成一个字符

——

目前 RichEditor 没有:

真正的 atomic span

机制。

所以:

工程上一般这么干:

在 Mention 后面:

额外插入一个空格

例如:

@张三·

(最后那个不可见空格)

——

然后:

光标只允许停在:

Mention 前
Mention 后

不允许进入中间。

——

五、额外数据怎么存?

不要存在 UI。

而是:

Map<spanId, metadata>

例如:

{
  spanId: "mention_1001",
  uid: "u123",
  nickname: "张三"
}

BuilderSpan 只负责显示:

@张三

真正业务数据:

自己存。

——

六、发布时怎么序列化?

推荐:

[
  {
    "type": "text",
    "text": "你好"
  },
  {
    "type": "mention",
    "uid": "123",
    "nickname": "张三"
  }
]

不要直接存 HTML。

也不要存:

@张三

纯字符串。

否则后面:

  • 改昵称
  • 点击跳转
  • 消息通知
  • @提醒

都会出问题。

——

七、为什么“基于 Span”更适合?

因为:

富交互编辑器
本质是“节点编辑器”

不是字符串编辑器。

所以:

属性字符串

更适合:

  • 富文本展示
  • 静态排版

而:

Span/BuilderSpan

更适合:

  • 社交编辑器
  • IM
  • 评论
  • 发帖
  • 动态发布

你好,参考最佳实践富文本编辑器, 使用RichEditor组件,实现自定义表情、@好友、添加话题等功能,并提供示例代码详细拆解细节逻辑,如@好友如何被视为一个整体,编辑器中内容如何获取并归一化处理等。

cke_287.gif

在HarmonyOS Next中,可使用ArkUI的RichEditor组件实现富文本编辑。通过设置textStyle、插入ImageSpanLinkSpanCustomSpan等扩展功能,支持字体样式、图片、链接、表格等。结合onEditingonChange事件监听内容变化,利用controller进行程序化操作。

可以使用 StyledString + PlaceholderSpan 实现“整体不可拆分”的 @好友 Span,并携带额外数据(如好友 ID)。光标移动时会自动将它视为一个整体,按 Delete 键会整体删除,点击时通过 RichEditoronClick 拿到数据即可。

核心步骤:

  1. 构建 StyledString,使用 controller.setStyledString()
  2. 创建 PlaceholderSpan 用于 @好友,并通过 PlaceholderSpanonClick 回调或 StyledStringsetData 存入好友 ID。
  3. 设置 RichEditoronClick 事件,用 getSpanAtOffset 获取当前点击的 Span,再读取数据。

示例关键代码(简化):

let styledStr = new StyledString();
styledStr.appendString("这是一条动态");
styledStr.appendPlaceholder(new PlaceholderSpan({
  width: 100, height: 30,
  content: { type: "mention", id: "123" }  // 携带数据
}));
this.richEditor.setStyledString(styledStr);

this.richEditor.onClick((event) => {
  let selection = this.richEditor.getSelection();
  let spanInfo = this.richEditor.getSpanAtOffset(selection.start);
  if (spanInfo && spanInfo.span instanceof PlaceholderSpan) {
    let data = spanInfo.span.content;  // 取出好友 ID
  }
});

该方式同时满足整体删除、光标跳转与携带数据的需要,适用于频繁交互场景。

回到顶部