> 技术文档 > 【HarmonyOS】富文本编辑器RichEditor详解_richtextvista

【HarmonyOS】富文本编辑器RichEditor详解_richtextvista


【HarmonyOS】富文本编辑器RichEditor详解

一、前言

在信息化高速发展的今天,普通的文本容器,已经不能够承载用户丰富的表达欲。富文本展示已经是移动开发中,必备要解决的问题,在鸿蒙中,通过在系统层提供RichEditor控件,来解决富文本展示的问题。

HarmonyOS推出的RichEditor控件,提供了从基础文本输入到复杂图文混排的完整解决方案。

从API version 10开始支持的RichEditor控件,不仅具备文本输入、样式设置等基础能力,还创新性地支持自定义键盘、图文混排、事件回调等高级特性。

随着版本迭代,RichEditor不断进化,从API version 11开始支持元服务调用,到API version 20引入AI菜单和撤销样式保留等功能,已发展为一个成熟稳定的富文本解决方案。

本文将从实际使用流程和完整实战Demo出发,详细解析RichEditor控件的核心功能与应用场景,帮助开发者快速掌握这一强大工具的使用方法。

二、使用流程

1、组件创建方式

RichEditor控件提供了两种创建方式:

(1)使用属性字符串构建

【HarmonyOS】富文本编辑器RichEditor详解_richtextvista

这种方式一般用于比较简单的富文本场景,例如上图颜色不同的一段话。
基于属性字符串(StyledString/MutableStyledString)构建,持有属性字符串对象来管理数据,通过修改属性字符串对象的内容、样式,再传递给组件,实现对富文本组件内容的更新。
相比于使用controller接口进行内容样式更新,使用起来更加灵活便捷。

@Entry@Componentstruct Index { // 定义字体样式对象,设置字体颜色为粉色 fontStyle: TextStyle = new TextStyle({ fontColor: Color.Pink }); // 创建可变样式字符串,用于存储富文本内容及其样式 // 初始文本为\"使用属性字符串构建的RichEditor组件\" // 并为前5个字符(\"使用属性字\")应用上面定义的粉色字体样式 mutableStyledString: MutableStyledString = new MutableStyledString(\"使用属性字符串构建的RichEditor组件\", [{ start: 0, // 样式起始位置(从0开始) length: 5, // 样式作用的字符长度 styledKey: StyledStringKey.FONT, // 样式类型为字体样式 styledValue: this.fontStyle // 具体的样式值 }]); // 初始化属性字符串模式的RichEditor控制器 // 该控制器专门用于处理基于属性字符串的富文本操作 controller: RichEditorStyledStringController = new RichEditorStyledStringController(); // 配置RichEditor组件的选项,将控制器传入 options: RichEditorStyledStringOptions = { controller: this.controller }; build() { Column() { // 构建RichEditor组件,使用上面配置的选项 RichEditor(this.options) // 组件初始化完成回调 // 当RichEditor组件准备好后,将之前创建的可变样式字符串设置到编辑器中 .onReady(() => { this.controller.setStyledString(this.mutableStyledString); }) } .height(\'100%\') // Column高度占满整个父容器 .width(\'100%\') // Column宽度占满整个父容器 .justifyContent(FlexAlign.Center) // 垂直方向居中对齐子组件 }}
(2)使用RichEditorController构建

【HarmonyOS】富文本编辑器RichEditor详解_richtextvista

这种方式一般用于复杂内容场景,通过RichEditorController提供的接口实现内容、样式的管理。

@Entry@Componentstruct IndexPage2 { // 初始化富文本编辑器控制器,用于管理RichEditor组件 controller: RichEditorController = new RichEditorController(); // 配置RichEditor组件选项,传入控制器实例 options: RichEditorOptions = { controller: this.controller }; build() { Column() { Column() { // 创建RichEditor组件并应用配置选项 RichEditor(this.options) // 组件初始化完成回调,用于设置初始内容 .onReady(() => { // 1. 添加第一段文本内容 // 使用addTextSpan方法添加文本,并设置橙色字体、16px大小 this.controller.addTextSpan(\'使用RichEditorController\', {  style: { fontColor: Color.Orange, fontSize: 16  } }); // 2. 添加符号内容 // 使用addSymbolSpan方法添加系统内置符号(篮球图标) // 设置符号大小为30px this.controller.addSymbolSpan($r(\"sys.symbol.basketball_fill\"), {  style: { fontSize: 30  } }); // 3. 添加第二段文本内容 // 使用addTextSpan方法添加文本,并设置红色字体、20px大小 this.controller.addTextSpan(\'构建富文本!!!\', {  style: { fontColor: Color.Red, fontSize: 20  } }); }) }.width(\'100%\') // 内部Column宽度占满父容器 }.height(\'100%\') // 外部Column高度占满父容器 }}

2、组件的属性配置参数效果

RichEditor提供了丰富的属性来定制编辑体验,下面介绍几个常用属性的配置方法。

(1)自定义选择菜单

通过bindSelectionMenu属性可以设置自定义选择菜单,替代组件默认的文本选择菜单,实现更丰富的菜单功能,如翻译、加粗等。

// 自定义菜单构建器@BuilderCustomMenu() { Column() { Menu() { MenuItemGroup() { MenuItem({ startIcon: $r(\'app.media.icon_bold\'), content: \"加粗\" }) MenuItem({ startIcon: $r(\'app.media.icon_italic\'), content: \"斜体\" }) MenuItem({ startIcon: $r(\'app.media.icon_underline\'), content: \"下划线\" }) } } .radius(8) .backgroundColor(Color.White) .width(200) }}// 在RichEditor中绑定自定义菜单RichEditor(this.options) .onReady(() => { this.controller.addTextSpan(\'长按触发自定义菜单\', { style: { fontColor: Color.Black, fontSize: 16 } }) }) .bindSelectionMenu(RichEditorSpanType.TEXT, this.CustomMenu, ResponseType.LongPress) .width(300) .height(200)
(2)光标和手柄颜色设置

通过caretColor属性可以设置输入框光标和手柄的颜色,提高视觉辨识度,使光标颜色与应用整体风格相协调。

RichEditor(this.options) .onReady(() => { this.controller.addTextSpan(\'设置了橙色光标和手柄的富文本\', { style: { fontColor: Color.Black, fontSize: 16 } }) }) .caretColor(Color.Orange) .width(300) .height(100)
(3)占位文本设置

通过placeholder属性可以设置无输入时的提示文本,引导用户正确操作。

RichEditor(this.options) .placeholder(\"请输入您的内容...\", { fontColor: Color.Gray, font: { size: 14, family: \"HarmonyOS Sans\" } }) .width(300) .height(80)

3、组件的事件监听与交互控制逻辑

RichEditor提供了丰富的事件监听接口,实现更灵活的编辑交互逻辑。

(1)初始化完成事件

初始化回调函数,一般在这里进行数据的加载,或者组件文本的拼接等。

RichEditor(this.options) .onReady(() => { console.info(\'RichEditor初始化完成\'); })
(2)选择变化事件

内容选择区域或光标位置变化时触发,可用于实时更新工具栏状态。

RichEditor(this.options) .onSelectionChange((range) => { console.info(`选中范围变化: start=${range.start}, end=${range.end}`); // 根据选中范围更新工具栏按钮状态 this.updateToolbarState(range); })
(3)粘贴事件

粘贴操作前触发,可用于自定义粘贴内容处理。

RichEditor(this.options) .onPaste((event) => { // 阻止默认粘贴行为 event.preventDefault(); // 自定义粘贴处理逻辑 this.handleCustomPaste(event); })

4、内容操作与管理

通过控制器可以实现对编辑内容的程序化操作。

添加文本内容
// 添加普通文本this.controller.addTextSpan(\'新添加的文本内容\', { style: { fontSize: 16, fontColor: Color.Blue }});// 在指定位置添加文本this.controller.addTextSpan(\'在指定位置添加的文本\', { style: { fontSize: 16, fontStyle: FontStyle.Italic }, offset: 10 // 在偏移量10的位置添加});
5、添加图片内容
this.controller.addImageSpan($r(\'app.media.image\'), { imageStyle: { size: [300, 200], // 图片大小 objectFit: ImageFit.Contain, // 图片缩放类型 verticalAlign: ImageSpanAlignment.MIDDLE // 垂直对齐方式 }});
6、更新文本样式
// 更新指定范围的文本样式this.controller.updateSpanStyle({ start: 0, end: 5, textStyle: { fontWeight: 700, // 加粗 decoration: { type: TextDecorationType.Underline, // 下划线 color: Color.Red } }});

三、DEMO源码

DEMO实现了一个富文本编辑器界面,支持字体样式设置、段落缩进控制、内容选中与编辑等功能,并通过自定义标记生成器实现列表缩进的可视化展示。
【HarmonyOS】富文本编辑器RichEditor详解_richtextvista

const canvasWidth = 1000;const canvasHeight = 100;const Indentation = 40;// 段落缩进标记生成器类class LeadingMarginCreator { private settings: RenderingContextSettings = new RenderingContextSettings(true); // 渲染上下文设置 private offscreenCanvas: OffscreenCanvas = new OffscreenCanvas(canvasWidth, canvasHeight); // 离屏画布 private offContext: OffscreenCanvasRenderingContext2D = this.offscreenCanvas.getContext(\"2d\", this.settings); // 离屏画布渲染上下文 public static instance: LeadingMarginCreator = new LeadingMarginCreator(); // 单例实例 // 获得字体字号级别(0-4级) public getFontSizeLevel(fontSize: number) { const fontScaled: number = Number(fontSize) / 16; // 字体缩放比例(相对于16px基准) enum FontSizeScaleThreshold { SMALL = 0.9, // 小字体阈值 NORMAL = 1.1, // 正常字体阈值 LEVEL_1_LARGE = 1.2, // 1级大字体阈值 LEVEL_2_LARGE = 1.4, // 2级大字体阈值 LEVEL_3_LARGE = 1.5 // 3级大字体阈值 } let fontSizeLevel: number = 1; // 初始字号级别为1 // 根据缩放比例确定字号级别 if (fontScaled < FontSizeScaleThreshold.SMALL) { fontSizeLevel = 0; } else if (fontScaled < FontSizeScaleThreshold.NORMAL) { fontSizeLevel = 1; } else if (fontScaled < FontSizeScaleThreshold.LEVEL_1_LARGE) { fontSizeLevel = 2; } else if (fontScaled < FontSizeScaleThreshold.LEVEL_2_LARGE) { fontSizeLevel = 3; } else if (fontScaled < FontSizeScaleThreshold.LEVEL_3_LARGE) { fontSizeLevel = 4; } else { fontSizeLevel = 1; } return fontSizeLevel; } // 获得缩进级别比例(根据缩进宽度计算比例) public getmarginLevel(width: number) { let marginlevel: number = 1; // 初始缩进比例为1 // 根据不同缩进宽度设置对应的比例 if (width === 40) { marginlevel = 2.0; } else if (width === 80) { marginlevel = 1.0; } else if (width === 120) { marginlevel = 2/3; } else if (width === 160) { marginlevel = 0.5; } else if (width === 200) { marginlevel = 0.4; } return marginlevel; } // 生成文本标记(将文本转换为像素图) public genStrMark(fontSize: number, str: string): PixelMap { this.offContext = this.offscreenCanvas.getContext(\"2d\", this.settings); // 重新获取渲染上下文 this.clearCanvas(); // 清空画布 this.offContext.font = fontSize + \'vp sans-serif\'; // 设置字体样式 this.offContext.fillText(str + \'.\', 0, fontSize * 0.9); // 绘制文本(末尾加点以确保宽度) // 获取像素图(根据文本长度计算宽度) return this.offContext.getPixelMap(0, 0, fontSize * (str.length + 1) / 1.75, fontSize); } // 生成方形标记(绘制正方形并转换为像素图) public genSquareMark(fontSize: number): PixelMap { this.offContext = this.offscreenCanvas.getContext(\"2d\", this.settings); // 重新获取渲染上下文 this.clearCanvas(); // 清空画布 const coordinate = fontSize * (1 - 1 / 1.5) / 2; // 计算起始坐标 const sideLength = fontSize / 1.5; // 计算正方形边长 this.offContext.fillRect(coordinate, coordinate, sideLength, sideLength); // 绘制正方形 // 获取正方形像素图 return this.offContext.getPixelMap(0, 0, fontSize, fontSize); } // 生成圆圈符号标记(根据缩进级别、字体大小等参数绘制圆形标记) public genCircleMark(fontSize: number, width: number, level?: number): PixelMap { const indentLevel = level ?? 1; // 缩进级别(默认1) const offsetLevel = [22, 28, 32, 34, 38]; // 不同字号级别的垂直偏移量 const fontSizeLevel = this.getFontSizeLevel(fontSize); // 获取字号级别 const marginlevel = this.getmarginLevel(width); // 获取缩进比例 const newCanvas = new OffscreenCanvas(canvasWidth, canvasHeight); // 创建新的离屏画布 const newOffContext: OffscreenCanvasRenderingContext2D = newCanvas.getContext(\"2d\", this.settings); // 新画布的渲染上下文 const centerCoordinate = 50; // 圆心水平坐标基准 const radius = 10; // 圆半径基准 this.clearCanvas(); // 清空画布 // 绘制椭圆(根据参数计算位置和大小) newOffContext.ellipse( 100 * (indentLevel + 1) - centerCoordinate * marginlevel, // 圆心x坐标 offsetLevel[fontSizeLevel], // 圆心y坐标(根据字号级别) radius * marginlevel, // 水平半径(根据缩进比例) radius, // 垂直半径 0, 0, 2 * Math.PI // 椭圆参数(起始角度、结束角度) ); newOffContext.fillStyle = \'66FF0000\'; // 填充颜色(半透明红色) newOffContext.fill(); // 填充图形 // 获取圆形标记的像素图(根据缩进级别计算宽度) return newOffContext.getPixelMap(0, 0, 100 + 100 * indentLevel, 100); } private clearCanvas() { this.offContext.clearRect(0, 0, canvasWidth, canvasHeight); // 清空画布 }}@Entry@Componentstruct IndexPage3 { // 富文本控制器(用于操作编辑器内容和样式) controller: RichEditorController = new RichEditorController(); options: RichEditorOptions = { controller: this.controller }; // 富文本编辑器选项 // 缩进标记生成器实例(使用单例模式) private leadingMarkCreatorInstance = LeadingMarginCreator.instance; private fontNameRawFile: string = \'MiSans-Bold\'; // 自定义字体名称 // 状态变量(用于界面交互和数据展示) @State fs: number = 30; // 字体大小 @State cl: number = Color.Black; // 字体颜色 @State start: number = -1; // 选中起始位置 @State end: number = -1; // 选中结束位置 @State message: string = \"[-1, -1]\"; // 选中范围提示信息 @State content: string = \"\"; // 选中内容 private leftMargin: Dimension = 0; // 左缩进量 private richEditorTextStyle: RichEditorTextStyle = {}; // 富文本样式 // 新增:光标颜色和选中背景色状态 @State cursorColor: Color|string = Color.Black; // 光标颜色 @State selectionColor: Color|string = Color.Gray; // 选中背景色 aboutToAppear() { // 注册自定义字体(应用启动时加载字体文件) this.getUIContext().getFont().registerFont({ familyName: \'MiSans-Bold\', familySrc: \'/font/MiSans-Bold.ttf\' }); } build() { Scroll() { Column() { // 颜色控制区域(切换界面主题颜色) Row() { Button(\"红色主题\").onClick(() => { this.cursorColor = Color.Red; // 设置红色光标 this.selectionColor = \"#FFCCCC\"; // 设置红色选中背景 }).width(\"30%\"); Button(\"绿色主题\").onClick(() => { this.cursorColor = Color.Green; // 设置绿色光标 this.selectionColor = \"#CCFFCC\"; // 设置绿色选中背景 }).width(\"30%\"); Button(\"蓝色主题\").onClick(() => { this.cursorColor = Color.Blue; // 设置蓝色光标 this.selectionColor = \"#CCCCFF\"; // 设置蓝色选中背景 }).width(\"30%\"); } .width(\"100%\") .justifyContent(FlexAlign.SpaceBetween) .margin({ bottom: 10 }); // 选中范围和内容显示区域(展示当前选中的位置和内容) Column() { Text(\"selection range:\").width(\"100%\").fontSize(16); // 选中范围标题 Text() { Span(this.message) // 显示选中范围信息 }.width(\"100%\").fontSize(16); Text(\"selection content:\").width(\"100%\").fontSize(16); // 选中内容标题 Text() { Span(this.content) // 显示选中内容 }.width(\"100%\").fontSize(16); } .borderWidth(1) .borderColor(Color.Red) .width(\"100%\") .padding(10) .margin({ bottom: 10 }); // 样式操作按钮区域(对选中内容进行样式修改) Row() { Button(\"加粗\").onClick(() => { // 更新选中区域文本样式(设置为加粗) this.controller.updateSpanStyle({  start: this.start,  end: this.end,  textStyle: { fontWeight: FontWeight.Bolder } }); }).width(\"25%\"); Button(\"获取选中内容\").onClick(() => { this.content = \"\"; // 清空内容显示 // 获取选中范围内的所有文本片段 this.controller.getSpans({ start: this.start, end: this.end }).forEach(item => {  if (typeof(item as RichEditorImageSpanResult)[\'imageStyle\'] !== \'undefined\') { // 处理图片片段 this.content += (item as RichEditorImageSpanResult).valueResourceStr + \"\\n\";  } else { if (typeof(item as RichEditorTextSpanResult)[\'symbolSpanStyle\'] !== \'undefined\') {  // 处理符号片段(显示字号)  this.content += (item as RichEditorTextSpanResult).symbolSpanStyle?.fontSize + \"\\n\"; } else {  // 处理普通文本片段(显示文本内容)  this.content += (item as RichEditorTextSpanResult).value + \"\\n\"; }  } }); }).width(\"25%\"); Button(\"删除选中内容\").onClick(() => { // 删除选中区域内容 this.controller.deleteSpans({ start: this.start, end: this.end }); this.start = -1; // 重置选中起始位置 this.end = -1; // 重置选中结束位置 this.message = \"[\" + this.start + \", \" + this.end + \"]\"; // 更新选中范围提示 }).width(\"25%\"); Button(\"设置样式1\").onClick(() => { // 设置输入时的默认样式 this.controller.setTypingStyle({  fontWeight: \'medium\', // 中等粗细  fontFamily: this.fontNameRawFile, // 自定义字体  fontColor: Color.Blue, // 蓝色  fontSize: 50, // 字号50  fontStyle: FontStyle.Italic, // 斜体  decoration: { type: TextDecorationType.Underline, color: Color.Green } // 绿色下划线 }); }).width(\"25%\"); } .borderWidth(1) .borderColor(Color.Red) .width(\"100%\") .height(\"10%\") .margin({ bottom: 10 }); // 富文本编辑器区域(核心编辑界面) Column() { RichEditor(this.options) .onReady(() => {  // 编辑器准备就绪时初始化内容  this.controller.addTextSpan(\"0123456789\\n\", { style: {  fontWeight: \'medium\', // 中等粗细  fontFamily: this.fontNameRawFile, // 自定义字体  fontColor: Color.Red, // 红色  fontSize: 50, // 字号50  fontStyle: FontStyle.Italic, // 斜体  decoration: { type: TextDecorationType.Underline, color: Color.Green } // 绿色下划线 }  });  this.controller.addTextSpan(\"abcdefg\", { style: {  fontWeight: FontWeight.Lighter, // 更细  fontFamily: \'HarmonyOS Sans\', // HarmonyOS默认字体  fontColor: \'rgba(0,128,0,0.5)\', // 半透明绿色  fontSize: 30, // 字号30  fontStyle: FontStyle.Normal, // 正常样式  decoration: { type: TextDecorationType.Overline, color: \'rgba(169, 26, 246, 0.50)\' } // 半透明紫色上划线 }  }); }) .onSelect((value: RichEditorSelection) => {  // 选中事件回调(更新选中范围状态)  this.start = value.selection[0];  this.end = value.selection[1];  this.message = \"[\" + this.start + \", \" + this.end + \"]\"; }) .caretColor(this.cursorColor) // 设置光标颜色(来自状态变量) .selectedBackgroundColor(this.selectionColor) // 设置选中背景色(来自状态变量) .borderWidth(1) .borderColor(Color.Green) .width(\"100%\") .height(\"30%\") .margin({ bottom: 10 }); } .borderWidth(1) .borderColor(Color.Red) .width(\"100%\") .padding(10); // 缩进操作按钮区域(控制段落缩进) Column() { Row({ space: 5 }) { Button(\"向右列表缩进\").onClick(() => {  let margin = Number(this.leftMargin); // 当前左缩进量  if (margin < 200) { margin += Indentation; // 增加缩进量(40像素) this.leftMargin = margin;  }  // 更新段落样式(设置带标记的缩进)  this.controller.updateParagraphStyle({ start: -10, end: -10, style: {  leadingMargin: {  pixelMap: this.leadingMarkCreatorInstance.genCircleMark(100, margin, 1), // 圆形缩进标记  size: [margin, 40] // 缩进标记大小  } }  }); }).width(\"48%\"); Button(\"向左列表缩进\").onClick(() => {  let margin = Number(this.leftMargin); // 当前左缩进量  if (margin > 0) { margin -= Indentation; // 减少缩进量(40像素) this.leftMargin = margin;  }  // 更新段落样式(设置带标记的缩进)  this.controller.updateParagraphStyle({ start: -10, end: -10, style: {  leadingMargin: {  pixelMap: this.leadingMarkCreatorInstance.genCircleMark(100, margin, 1), // 圆形缩进标记  size: [margin, 40] // 缩进标记大小  } }  }); }).width(\"48%\"); } .margin({ bottom: 10 }); Row({ space: 5 }) { Button(\"向右空白缩进\").onClick(() => {  let margin = Number(this.leftMargin); // 当前左缩进量  if (margin < 200) { margin += Indentation; // 增加缩进量(40像素) this.leftMargin = margin;  }  // 更新段落样式(设置纯空白缩进)  this.controller.updateParagraphStyle({ start: -10, end: -10, style: { leadingMargin: margin } // 仅设置缩进宽度  }); }).width(\"48%\"); Button(\"向左空白缩进\").onClick(() => {  let margin = Number(this.leftMargin); // 当前左缩进量  if (margin > 0) { margin -= Indentation; // 减少缩进量(40像素) this.leftMargin = margin;  }  // 更新段落样式(设置纯空白缩进)  this.controller.updateParagraphStyle({ start: -10, end: -10, style: { leadingMargin: margin } // 仅设置缩进宽度  }); }).width(\"48%\"); } .margin({ bottom: 10 }); Button(\"获取当前样式\").onClick(() => { this.richEditorTextStyle = this.controller.getTypingStyle(); console.info(\"RichEditor getTypingStyle:\" + JSON.stringify(this.richEditorTextStyle)); }) .width(\"100%\") .margin({ bottom: 10 }); } .width(\"100%\") .padding(10); } .width(\"100%\") .padding(10); } }}