【HarmonyOS】富文本編輯器RichEditor詳解
一、前言
在信息化高速發展的今天,普通的文本容器,已經不能夠承載用戶豐富的表達欲。富文本展示已經是移動開發中,必備要解決的問題,在鴻蒙中,通過在系統層提供RichEditor控件,來解決富文本展示的問題。
HarmonyOS推出的RichEditor控件,提供了從基礎文本輸入到復雜圖文混排的完整解決方案。
從API version 10開始支持的RichEditor控件,不僅具備文本輸入、樣式設置等基礎能力,還創新性地支持自定義鍵盤、圖文混排、事件回調等高級特性。
隨著版本迭代,RichEditor不斷進化,從API version 11開始支持元服務調用,到API version 20引入AI菜單和撤銷樣式保留等功能,已發展為一個成熟穩定的富文本解決方案。
本文將從實際使用流程和完整實戰Demo出發,詳細解析RichEditor控件的核心功能與應用場景,幫助開發者快速掌握這一強大工具的使用方法。
二、使用流程
1、組件創建方式
RichEditor控件提供了兩種創建方式:
(1)使用屬性字符串構建
這種方式一般用于比較簡單的富文本場景,例如上圖顏色不同的一段話。
基于屬性字符串(StyledString/MutableStyledString)構建,持有屬性字符串對象來管理數據,通過修改屬性字符串對象的內容、樣式,再傳遞給組件,實現對富文本組件內容的更新。
相比于使用controller接口進行內容樣式更新,使用起來更加靈活便捷。
@Entry
@Component
struct 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構建
這種方式一般用于復雜內容場景,通過RichEditorController提供的接口實現內容、樣式的管理。
@Entry
@Component
struct 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方法添加系統內置符號(籃球圖標)// 設置符號大小為30pxthis.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屬性可以設置自定義選擇菜單,替代組件默認的文本選擇菜單,實現更豐富的菜單功能,如翻譯、加粗等。
// 自定義菜單構建器
@Builder
CustomMenu() {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實現了一個富文本編輯器界面,支持字體樣式設置、段落縮進控制、內容選中與編輯等功能,并通過自定義標記生成器實現列表縮進的可視化展示。
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
@Component
struct 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, // 字號50fontStyle: 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, // 字號50fontStyle: 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, // 字號30fontStyle: 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);}}
}