富文本编辑:基于TextInput的富文本编辑器开发(80)
在鸿蒙(HarmonyOS)应用开发中,首先需要明确一个核心的技术选型前提:TextInput组件仅支持单行文本输入,且不支持富文本样式。因此,开发富文本编辑器不能基于TextInput,而必须使用鸿蒙原生提供的RichEditor组件。
结合鸿蒙 ArkUI 的特性,以下是基于RichEditor构建富文本编辑器的核心架构与实战代码:
一、 基础架构:RichEditor 与控制器绑定
RichEditor组件的核心在于RichEditorController,它负责控制编辑器的样式、内容插入及状态获取。同时,配合TextInput处理单行标题输入,可构建出完整的笔记编辑界面。
核心代码示例:
@Entry @Component struct NoteEditor { @State noteTitle: string = ''; controller: RichEditorController = new RichEditorController(); build() { Column({ space: 16 }) { // 1. 标题输入(使用 TextInput) TextInput({ placeholder: '请输入标题', text: this.noteTitle }) .fontSize(24) .fontWeight(FontWeight.Bold) .onChange(value => this.noteTitle = value) // 2. 富文本编辑区(使用 RichEditor) RichEditor(this.controller) .placeholder('开始编写内容...') .width('100%') .height('70%') .border({ width: 1, color: '#E0E0E0' }) .borderRadius(8) .padding(12) } .padding(16) } }二、 核心交互:工具栏与样式控制
富文本编辑器的灵魂在于工具栏与编辑器的联动。通过监听onSelect事件获取当前选中文本的状态,并调用 Controller 的 API(如addTextSpan、insertText)来应用格式。
核心代码示例:
// 工具栏构建 @Builder buildToolbar() { Row({ space: 12 }) { Button('B').onClick(() => { // 注意:aboutToIMEInput 仅对接下来输入的文本生效 // 若要修改已选中文本,需先 getSelection() 再 setStyle() this.controller.addTextSpan('', { fontWeight: FontWeight.Bold }); }) Button('I').onClick(() => { this.controller.addTextSpan('', { fontStyle: FontStyle.Italic }); }) Button('插入图片').onClick(() => { // 在光标位置插入图片 this.controller.insertImage({ src: $r('app.media.icon'), width: 100, height: 100, position: RichEditorInsertPosition.CURSOR }); }) } }三、 进阶能力:内容序列化与持久化存储
编辑完成后,需要将带有样式的富文本内容保存下来。RichEditor支持导出为AttributedString对象,该对象包含了文本内容和所有样式信息,可序列化为二进制格式存储。
核心代码示例:
// 保存笔记 async function saveNote() { // 获取带样式的文本对象 const attributedString = await this.controller.getAttributedText(); // 序列化为二进制/字符串格式 const serializedData = attributedString.serialize(); // 存入数据库(建议使用 BLOB 字段保存二进制格式,避免 JSON 丢失样式) await saveToFile(serializedData); } // 加载已保存的笔记 async function loadNote(fileContent: string) { // 反序列化并还原编辑器内容 const attributedString = AttributedString.deserialize(fileContent); this.controller.setText(attributedString); }四、 桌面级体验:性能优化
在 PC 端或处理长文档时,富文本编辑器面临极大的性能挑战。鸿蒙RichEditor提供了底层优化机制,开发者在实战中需注意以下规范:
- 大文档渲染优化:
RichEditor内部采用了虚拟滚动技术和增量更新机制,仅渲染可视区域内容,并在编辑时只更新变化部分,避免全量重绘。 - 大文本分块加载:处理超长文本时,建议采用分块加载策略(如每页不超过 5000 字符),图片插入前务必进行压缩处理,防止内存溢出。
- 键盘遮挡问题:在移动端或特定窗口布局下,若遇到键盘遮挡内容,可使用
.customKeyboard()自定义键盘,或开启avoidKeyboard: true属性实现自动避让。 - 安全与权限限制:出于安全考虑,
RichEditor会自动过滤<script>和<iframe>等危险标签。若需支持复制粘贴功能,必须在module.json5中声明ohos.permission.PASTE权限。
五、 进阶交互:工具栏与编辑器状态的双向联动
在真实的编辑器中,当用户通过鼠标选中一段已有的文本时,工具栏的按钮(如加粗、斜体)必须高亮显示当前状态。这需要监听onSelect事件,并结合getSpansAPI 动态更新工具栏的状态变量。
核心代码示例:
RichEditor(this.controller) .onSelect((selection: RichEditorSelection) => { // 获取当前选中区域的 Span 信息 const spans = this.controller.getSpans({ start: selection.start, end: selection.end }); // 遍历判断是否包含粗体样式,从而驱动工具栏 UI 更新 this.isBoldActive = spans.some(span => span.style?.fontWeight === FontWeight.Bold); })六、 内容扩展:嵌入自定义组件(BuilderSpan)
除了常规的图文混排,现代富文本编辑器通常需要支持插入代码块、自定义图表或文件附件。通过addBuilderSpanAPI,可以将任意@Builder修饰的自定义 UI 组件无缝嵌入到文本流中。
核心代码示例:
@Builder CodeBlockBuilder() { Column() { Text('console.log("Hello HarmonyOS");') .fontFamily('monospace') .fontColor(Color.White) } .width('100%') .padding(12) .backgroundColor('#282C34') .borderRadius(8) } // 在工具栏触发插入 Button('插入代码块').onClick(() => { this.controller.addBuilderSpan(() => { this.CodeBlockBuilder() }); })七、 桌面级体验:自定义文本选择菜单(Edit Menu)
PC 端用户习惯在选中文本后通过右键或长按呼出菜单。除了系统默认的复制、粘贴,开发者可以通过editMenuOptions扩展自定义操作(如:AI 润色、翻译、搜索)。
核心代码示例:
RichEditor(this.controller) .editMenuOptions({ onCreateMenu: (textMenuItems: Array<TextMenuItem>) => { // 向系统菜单追加自定义选项 textMenuItems.push({ content: 'AI 润色', id: TextMenuItemId.of('ai_polish'), icon: $r("app.media.ai_icon") }); return textMenuItems; }, onMenuItemClick: (item: TextMenuItem, range: TextRange) => { if (item.id?.toString() === 'ai_polish') { const selectedSpans = this.controller.getSpans(range); // 触发 AI 润色逻辑... } return true; } })八、 智能化增强:实体识别与预上屏(API 12+)
鸿蒙系统为RichEditor提供了强大的底层智能能力。开启enableDataDetector后,编辑器能自动识别文本中的电话号码、URL 链接并添加可点击的下划线;结合enablePreviewText,还能实现输入法候选词的实时预览,大幅提升输入体验。
核心代码示例:
RichEditor(this.controller) // 自动识别链接和电话 .enableDataDetector(true) .dataDetectorConfig({ types: [DetectType.PHONE, DetectType.URL], onDetectResultUpdate: (result) => { console.info(`识别到实体类型: ${result.type}`); } }) // 开启输入法候选词预上屏 .enablePreviewText(true) .onIMEInputComplete((result) => { console.info(`最终确认输入: ${result.text}`); })- 区分行内样式与段落样式:在调用 Controller API 时需注意,加粗、颜色等属于行内样式,需通过
aboutToIMEInput影响后续输入,或通过setStyle修改选中文本;而左对齐、无序列表等属于段落样式,应使用setTextAlign或setStyle({ applyList: ... })进行控制。 - 大文本内存管理:虽然
RichEditor内部有虚拟滚动优化,但在处理超长文档时,仍建议在业务层实现“分块加载”。每次仅向编辑器注入 5000 字符左右的内容,滚动到底部时再动态追加,避免内存飙升。 - 图片异步压缩:在通过
insertImage或addImageSpan插入本地相册图片时,切勿直接将原图 URI 传入。应先在后台线程(TaskPool)对图片进行尺寸压缩与格式转换,再插入编辑器,否则极易导致编辑器渲染卡顿或 OOM。 - 撤销/重做(Undo/Redo)机制:
RichEditorController内置了历史记录栈。在构建工具栏时,可直接绑定controller.onDo()(撤销)和controller.reDo()(重做)方法,并结合currentIndex与historyRecordArray.length来动态控制按钮的禁用状态。
