开天辟地 HarmonyOS(鸿蒙) - 组件(文本类): RichEditor(富文本编辑器)

源码 https://github.com/webabcd/HarmonyDemo
作者 webabcd

开天辟地 HarmonyOS(鸿蒙) - 组件(文本类): RichEditor(富文本编辑器)

示例如下:

pages\component\text\RichEditorDemo.ets

/*
 * RichEditor - 富文本编辑器(支持图文混排和文本交互式编辑)
 *
 * 注:部分用法和 Text, TextInput 差不多,详见 TextDemo.ets, TextInputDemo.ets, StyledStringDemo.ets 中的说明
 */

import { TitleBar } from '../../TitleBar'

@Entry
@Component
struct RichEditorDemo {

  build() {
    Column({ space: 10 }) {
      TitleBar()

      Tabs() {
        TabContent() { MySample1() }.tabBar('基础').align(Alignment.Top)
        TabContent() { MySample2() }.tabBar('添加自定义组件').align(Alignment.Top)
        TabContent() { MySample3() }.tabBar('自定义上下文菜单').align(Alignment.Top)
        TabContent() { MySample4() }.tabBar('属性字符串').align(Alignment.Top)
      }
      .scrollable(true)
      .barMode(BarMode.Scrollable)
    }
  }
}

@Component
struct MySample1 {

  // RichEditorController 是用于和 RichEditor 交互的,声明式编程通常都会用这种方式
  controller: RichEditorController = new RichEditorController();
  options: RichEditorOptions = { controller: this.controller };

  @State message: string = ""

  build() {
    Column({space: 10}) {

      Text(this.message).fontSize(16)

      /*
       * RichEditor - 富文本编辑器(可以添加编辑多个不同种类的 span)
       *   placeholder() - 无输入时的占位文本
       *   onReady() - 富文本编辑器初始化完成后的回调
       *   onSelect() - 选中内容后的回调
       *   aboutToIMEInput(), onIMEInputComplete() - 输入字符前和输入字符后的一对回调
       *   aboutToDelete(), onDeleteComplete() - 删除字符前和删除字符后的一对回调
       * RichEditorController - 富文本编辑器的 controller
       *   addTextSpan() - 添加文本 span
       *   addSymbolSpan() - 添加符号图标 span
       *   addImageSpan() - 添加图片 span
       *   addBuilderSpan() - 插入自定义组件 span
       *   updateSpanStyle() - 更新指定范围的文本 span 的样式
       *   updateParagraphStyle() - 更新指定范围的段落的样式
       *   setTypingStyle() - 设置新输入的文本的样式
       *   getTypingStyle() - 获取新输入的文本的样式
       *   getSpans() - 获取指定范围内的所有 span
       *   deleteSpans() - 删除指定范围内的数据
       *   getParagraphs() - 获取指定范围内的所有段落
       *   setSelection() - 设置选中内容的范围
       *   getSelection() - 获取选中内容的范围和选中的内容
       */
      Button("全部斜体").onClick(() => {
        // 全部文本都变为斜体
        this.controller.updateSpanStyle({
          start: -1,
          end: -1,
          textStyle:
          {
            fontStyle: FontStyle.Italic
          }
        })
      })
      Button("字符索引 1 - 4 加粗蓝色").onClick(() => {
        // 将索引位置 1 - 4 的所有文本的样式变为蓝色且加粗
        this.controller.updateSpanStyle({
          start: 1,
          end: 5,
          textStyle:
          {
            fontWeight: FontWeight.Bolder,
            fontColor: Color.Blue,
          }
        })
      })
      Button("获取字符索引 1 - 9 的所有父 span 集合").onClick(() => {
        // 获取索引位置 1 - 9 的内容的所属的所有父 span 集合
        let content = "";
        this.controller.getSpans({
          start: 1,
          end: 10
        }).forEach(item => {
          // 当前 span 是图片 span
          if (typeof(item as RichEditorImageSpanResult)['imageStyle'] != 'undefined'){
            content += (item as RichEditorImageSpanResult).valueResourceStr;
            content += "\n"
          }
          // 当前 span 是符号图标 span
          else if(typeof(item as RichEditorTextSpanResult)['symbolSpanStyle'] != 'undefined') {
            content += (item as RichEditorTextSpanResult).symbolSpanStyle?.fontSize;
            content += "\n"
          }
          // 当前 span 是文本 span
          else {
            content += (item as RichEditorTextSpanResult).value;
            content += "\n"
          }
        })
        this.message = content
      })
      Button("字符索引 1 - 2 删除").onClick(() => {
        // 删除索引位置 1 - 2 的内容
        this.controller.deleteSpans({
          start: 1,
          end: 3
        })
      })
      Button("全部段落居右显示").onClick(() => {
        // 全部段落居右显示
        this.controller.updateParagraphStyle({
          start: -1,
          end: -1,
          style: {
            textAlign: TextAlign.End,
          }
        })
      })

      Column() {
        RichEditor(this.options)
          .placeholder("请输入", {
            fontColor: Color.Gray,
            font: {
              size: 16,
              weight: FontWeight.Normal,
              style: FontStyle.Normal,
              family: "HarmonyOS Sans",
            }
          })
          .onReady(() => {
            // 添加文本 span,并指定其样式以及手势事件
            this.controller.addTextSpan("012345",
              {
                style:
                {
                  fontColor: Color.Orange,
                  fontSize: 32
                },
                gesture:
                {
                  onClick: () => {
                    this.message = "app.media.app_icon onClick"
                  },
                  onLongPress: () => {
                    this.message = "app.media.app_icon onClick onLongPress"
                  }
                }
              })
            // 添加符号图标 span,并指定其样式
            this.controller.addSymbolSpan($r("sys.symbol.wifi"),
              {
                style:
                {
                  fontSize: 32
                }
              })
            // 添加图片 span,并指定其样式
            this.controller.addImageSpan($r("app.media.app_icon"),
              {
                imageStyle:
                {
                  size: ["48vp", "48vp"]
                }
              })
            // 添加文本 span,并指定其样式
            this.controller.addTextSpan("56789",
              {
                style:
                {
                  fontColor: Color.Black,
                  fontSize: 32
                }
              })
            // 设置新输入的文本的样式
            this.controller.setTypingStyle(
              {
                fontColor: Color.Blue,
                fontSize: 48,
              })
          })
          // 选中内容后的回调
          .onSelect((value: RichEditorSelection) => {
            this.message += `\nonSelect: ${value.selection[0]}, ${value.selection[1]}`
          })
          // 输入字符前的回调
          .aboutToIMEInput((value: RichEditorInsertValue) => {
            this.message = `aboutToIMEInput
  insertValue:${value.insertValue},
  insertOffset:${value.insertOffset},
  previewText:${value.previewText}`
            // 返回 true 则代表允许此次输入,然后会调用 onIMEInputComplete()
            // 返回 false 则代表拒绝此次输入,之后也不会调用 onIMEInputComplete()
            return true;
          })
          // 输入字符后的回调
          .onIMEInputComplete((value: RichEditorTextSpanResult) => {
            // value - 输入的字符的所属的 span 的值
            // spanIndex - 输入的字符的所属的 span 在 RichEditor 中的所有 span 中的索引位置
            // offsetInSpan - 输入的字符在所属 span 中的索引位置
            this.message += `\nonIMEInputComplete
  value:${value.value}
  previewText:${value.previewText}
  spanIndex:${value.spanPosition.spanIndex}
  spanRange:${value.offsetInSpan[0]}, ${value.offsetInSpan[1]}`
          })
          // 删除字符前的回调
          .aboutToDelete((value: RichEditorDeleteValue) => {
            // richEditorDeleteSpans - 删除的所有内容的所属 span 的集合(如何判断当前 span 的类型已经在前面说明过了)
            this.message = `aboutToDelete,
offset:${value.offset}
direction:${value.direction}
length:${value.length}
richEditorDeleteSpans length:${value.richEditorDeleteSpans.length}`
            // 返回 true 则代表允许此次删除,然后会调用 onDeleteComplete()
            // 返回 false 则代表拒绝此次删除,之后也不会调用 onDeleteComplete()
            return true;
          })
          // 删除字符后的回调
          .onDeleteComplete(() => {
            this.message += `\nonDeleteComplete`
          })
          .borderWidth(1)
          .borderColor(Color.Blue)
          .width("95%")
          .height("200")
      }
    }
  }
}

@Component
struct MySample2 {

  @State message:string = ""
  controller: RichEditorController = new RichEditorController();

  // 自定义组件
  @Builder myBuilder() {
    Row({ space: 10 }) {
      Image($r("app.media.app_icon")).width(24).height(24)
      Text('hello').fontSize(16)
    }.backgroundColor(Color.Orange)
  }
  // 自定义组件 span
  private myBuilderSpan: CustomBuilder = this.myBuilder.bind(this)

  build() {
    Column({space: 10}) {

      Text(this.message).fontSize(16)

      RichEditor({
       controller: this.controller
      })
        .onReady(() => {
          this.controller.addTextSpan("012",
            {
              style:
              {
                fontColor: Color.Red,
                fontSize: 24,
              }
            })
          this.controller.addTextSpan("34567",
            {
              style:
              {
                fontColor: Color.Red,
                fontSize: 24,
              }
            })
        })
        .borderWidth(1)
        .borderColor(Color.Blue)
        .width("95%")
        .height("200")

      Button("插入自定义组件 span")
        .onClick(() => {
          /*
           * addBuilderSpan() - 插入自定义组件 span
           *   value - 需要插入的自定的组件 span
           *   options - 插入选项
           *     offset - 插入的位置
           *   返回值为:插入后,自定义组件 span 在所有 span 中的索引位置
           */
          let index = this.controller.addBuilderSpan(
            this.myBuilderSpan, {
              offset: 5, // 在索引位置 5 之前插入自定义组件 span
            })
          // 以本例来说
          // 第一次在索引位置 5 之前插入自定义组件 span 后,原来的 2 个 span 会变为 4 个 span
          // 第 1 个 span 是 012
          // 第 2 个 span 是 34
          // 第 3 个 span 是 新插入的自定义组件 span
          // 第 4 个 span 是 567
          this.message = `spanIndex:${index}, spansCount:${this.controller.getSpans().length}`
        })
    }
  }
}

@Component
struct MySample3 {

  @State message: string = ''
  controller: RichEditorController = new RichEditorController();

  // 自定义组件
  @Builder myBuilder() {
    Row({ space: 10 }) {
      Image($r("app.media.app_icon")).width(24).height(24)
      Text('hello').fontSize(16)
    }.backgroundColor(Color.Orange)
  }
  // 自定义组件 span
  private myBuilderSpan: CustomBuilder = this.myBuilder.bind(this)

  @Builder MyContextMenu() {
    Column() {
      Menu() {
        MenuItemGroup() {
          MenuItem({ startIcon: $r('app.media.app_icon'), content: "menu 1" })
            .onClick((event) => {
              // 关闭指定的菜单
              this.controller.closeSelectionMenu();
            })
          MenuItem({ startIcon: $r('app.media.app_icon'), content: "menu 2" })
          MenuItem({ startIcon: $r('app.media.app_icon'), content: "menu 3" })
        }
        .backgroundColor(Color.Orange)
      }
    }
  }

  build() {
    Column({ space: 10 }) {

      /*
       * bindSelectionMenu() - 使用自定义的上下文菜单
       *   spanType - 哪种类型的内容被选中后需要弹出自定义的上下文菜单(RichEditorSpanType 枚举)
       *     TEXT - 选中的内容只有文本时
       *     IMAGE - 选中的内容只有图片时
       *     BUILDER - 选中的内容只有自定义组件 span 时
       *     MIXED - 选中的内容有 2 种或以上的类型时
       *   content - 自定义的上下文菜单组件
       *   TextResponseType - 调出上下文菜单的方式(ResponseType 枚举)
       *     RightClick - 右键
       *     LongPress - 长按
       *   options - 选项
       *     onAppear - 自定义菜单弹出时的回调
       *     onDisappear - 自定义菜单关闭时的回调
       */
      RichEditor({
        controller: this.controller
      })
        .onReady(() => {
          this.controller.addTextSpan("01234",
            {
              style:
              {
                fontColor: Color.Orange,
                fontSize: 32
              }
            })
          this.controller.addImageSpan($r("app.media.app_icon"),
            {
              imageStyle:
              {
                size: ["48vp", "48vp"]
              }
            })
          this.controller.addBuilderSpan(this.myBuilderSpan)
        })
        .bindSelectionMenu(RichEditorSpanType.MIXED, this.MyContextMenu, ResponseType.LongPress, {
          onAppear: () => {
            this.message = `自定义菜单弹出了`;
          },
          onDisappear: () => {
            this.message = `自定义菜单关闭了`;
          },
        })
        .borderWidth(1)
        .borderColor(Color.Blue)
        .width("95%")
        .height("200")
    }
  }
}

@Component
struct MySample4 {

  @State message:string = ""
  controller1: RichEditorController = new RichEditorController();
  controller2: TextController = new TextController();

  build() {
    Column({ space: 10 }) {

      RichEditor({
        controller: this.controller1
      })
        .onReady(() => {
          this.controller1.addTextSpan("012345",
            {
              style:
              {
                fontColor: Color.Orange,
                fontSize: 32
              }
            })
          this.controller1.addImageSpan($r("app.media.app_icon"),
            {
              imageStyle:
              {
                size: ["48vp", "48vp"]
              }
            })
          this.controller1.addTextSpan("56789",
            {
              style:
              {
                fontColor: Color.Black,
                fontSize: 32
              }
            })
        })
        .borderWidth(1)
        .borderColor(Color.Blue)
        .width("95%")
        .height("100")

      Text(undefined, { controller: this.controller2 })
        .fontSize(16)

      Text(this.message)
        .fontSize(16)

      /*
       * 关于 RichEditor 的属性字符串,即 StyledString 的用法详见 StyledStringDemo.ets 中的说明
       * RichEditorController - 用于和 RichEditor 交互
       *   toStyledString() - 将指定范围的内容转为 StyledString
       *   fromStyledString() - 将指定的 StyledString 转为 span 集合
       */
      Button("toStyledString() 和 fromStyledString()").onClick(() => {
        // 将全部内容转为 StyledString 对象
        let styledString = this.controller1.toStyledString({
          start: -1,
          end: -1
        })
        this.controller2.setStyledString(styledString)

        let spans = this.controller1.fromStyledString(styledString)
        this.message = JSON.stringify(spans)
      })
    }
  }
}

源码 https://github.com/webabcd/HarmonyDemo
作者 webabcd

posted @ 2025-02-05 14:38  webabcd  阅读(368)  评论(0)    收藏  举报