一、开场白

今天聊聊 HarmonyOS 里一个贼实用的功能——富文本显示。

这玩意儿啥场景用?你随便打开几个 APP 看看:社交聊天、详情页内容、列表信息流、编辑器、弹窗提示框…到处都是富文本的身影。

富文本格式是一种便于在不同设备和系统间查看的文本与图形文档格式。本文将重点介绍显示富文本数据所需的相关组件的特性,并探讨如下几种常见场景及其实现方法:

  1. 实现高亮显示的超链接文本
  2. 实现文本中的图片表情
  3. 实现自定义的图文元素
  4. 实现图标与文本的组合元素

二、富文本能力介绍

特点

应用中的富文本可能具有以下特点:

  • 文本样式:包括字体前景色、背景色、字体类型、字号、粗体、下划线、删除线等装饰线、基线偏移、字符间距、行高和段落样式等
  • 定制效果:如边框、阴影、渐变和背景图片等
  • 图文混排:支持 emoji 表情、图标和网络图片等
  • 高亮超链接:包括@提醒、#标签、电话、Email 和 Https 链接等
  • 手势交互:支持单击和长按等操作
  • 应用范围:富文本可能在文本显示的任何区域使用,如详情页内容、列表信息流、编辑器及弹窗提示框等

能力支持对比

技术方案 Text/RichEditor+ StyledString Text+Span 子组件 Text+ enableDataDetector() RichEditor+ addTextSpan() Web RichText(Deprecated)
支持元素种类 文本、图片、自定义 Span、ImageSpan、SymbolSpan、ContainerSpan TextDataDetectorType 中的类型 文本、图片、自定义 丰富,HTML 标签元素 支持标签范围内的元素
扩展元素类型 支持 不支持 不支持 支持 不支持 不支持
自定义元素样式 支持 有限支持 支持颜色、装饰线 支持 通过 css 支持 通过 css 有限支持
元素事件类型 点击、长按 Span、ImageSpan 支持点击 点击 点击、长按 点击 不支持
自定义元素事件 支持 支持 不支持 支持 支持 不支持
自定义扩展信息 支持 自行维护 自行维护 自行维护 自行维护 自行维护
加载 HTML 文本并渲染 仅支持

、、

不支持 不支持 不支持 支持 有限支持
长列表中显示 支持 支持 支持 支持 不推荐 不推荐
宽高自适应 支持 支持 支持 支持 不支持 不支持

使用建议

建议你们全面考虑需求场景的特点及其潜在的扩展需求,然后根据自身能力进行技术选型。

方案 优势 劣势
Text/RichEditor+ StyledString 可以更新各元素的样式;可自定义富文本的呈现效果;可扩展元素种类并自定义其样式;元素可携带自定义扩展信息 使用较为复杂
Text+Span 等 Text 的子组件 通过子组件布局实现,结构较为清晰 支持的元素种类有限(如文本、图片、图标等)
Text+ enableDataDetector() 使用相对简单 仅支持文本内容;依赖底层的识别能力;事件和菜单不可自定义
RichEditor+ addTextSpan() 可以自定义富文本的呈现效果;可以扩展元素种类并自定义样式 不支持更新自定义 Span 的样式;扩展信息需由开发者自行维护
Web 支持加载和显示本地网页、在线网页及 HTML 格式的文本 不适用于长列表等特定场景
RichText(Deprecated) 可加载并显示 HTML 格式的文本 较为消耗内存资源;不适用于长列表等场景;不适用于需要对 HTML 字符串显示效果进行大量自定义的应用场景

选择路线图

从上图可以看出:

  • 简单场景:通常使用 Text+Span 组件,因为其使用简便且能满足需求,可以优先考虑
  • 复杂场景RichEditor+addTextSpan() 较为复杂,适用于更复杂的场景
  • 高兼容性需求Text/RichEditor+StyledString 属性字符串的使用虽然更为复杂,但其兼容性更高,功能更丰富,可以根据具体场景自定义组件,适用范围更广

三、实现高亮显示的超链接文本

场景描述

在社交和聊天等应用平台中,常见的文本元素包括@昵称、#话题和 https 链接等高亮显示的内容。
在这里插入图片描述

实现原理

只需对文中的@昵称和#话题等文字设置高亮样式,并添加点击跳转事件,点击后跳转至相应的话题详情页面或用户详情页面。

选择的方案:可以通过属性字符串 StyledString 中的 TextStyle 属性设置样式,并通过 GestureStyle 属性实现点击事件。
在这里插入图片描述

开发步骤

第一步:初始化时通过 TextStyle 属性定义文字样式

textAttribute: TextStyle = new TextStyle({
  fontColor: $r('app.color.styled_text_link_font_color'),
  fontSize: LengthMetrics.fp(14)
});

第二步:使用 GestureStyle 属性定义点击超链接时的跳转事件

generateClickStyle(span: MyCustomSpan): GestureStyle {
  return new GestureStyle({
    onClick: () => {
      this.linkClickCallback(span);
    }
  })
}

第三步:循环处理文本数据,并生成属性字符串

handleStyledString() {
  if (this.systemLanguage === 'zh-Hans') {
    this.spans = TitleLinkMock;
  } else {
    this.spans = TitleLinkMock_EN;
  }
  this.spans.forEach((span) => {
    if (span.url) {
      this.handleLink(span);
    } else {
      this.styledStrings.push(new MutableStyledString(span.content, []));
    }
  });

  this.controller = HandleData.handleStyledString(this.styledStrings);
}

第四步:设置文本样式和点击事件

handleLink(span: MyCustomSpan) {
  this.styledStrings.push(new MutableStyledString(span.content, [{
    start: 0,
    length: span.content.length,
    styledKey: StyledStringKey.GESTURE,
    styledValue: this.generateClickStyle(span)
  }, {
    start: 0,
    length: span.content.length,
    styledKey: StyledStringKey.FONT,
    styledValue: this.textAttribute
  }]));
}

第五步:将生成的属性信息拼接成属性字符串,并绑定到 Text 组件以进行渲染显示

static handleStyledString(styledStrings: MutableStyledString[]): TextController {
  let controller: TextController = new TextController();
  let paragraphStyledString: MutableStyledString = new MutableStyledString('', []);

  // 将每个文本片段生成的属性字符串追加到属性字符串段落中
  styledStrings.forEach((mutableStyledString: MutableStyledString) => {
    paragraphStyledString.appendStyledString(mutableStyledString);
  })

  controller.setStyledString(paragraphStyledString);
  return controller;
}

四、实现文本中的图片表情

场景描述

文本中的自定义 emoji 表情通常使用类似 [哈哈] 这样的字符进行传输,但在显示时会被替换为本地或网络图片。
在这里插入图片描述

实现原理

文本中显示为表情图片,需要调整其样式设置,而无需编辑文本信息。

选择的方案:可以先获取输入字符对应的图片,然后通过属性字符串 StyledStringImageAttachment 属性加载图片,并使用 UserDataSpan 属性存储自定义扩展信息。
在这里插入图片描述

开发步骤

第一步:初始化声明文字和图片的对应关系

export const EMOJI_DATA: Map<string, Resource> = new Map([
  ["[哈哈]", $r('app.media.smile')]
]);

第二步:循环处理文本数据,并生成属性字符串

handleStyledString() {
  this.spans = EmojiMock;
  this.spans.forEach((span) => {
    this.handleEmoji(span);
  });

  this.controller = HandleData.handleStyledString(this.styledStrings);
}

第三步:将输入的文本转换为图片,使用 ImageAttachment,设置图片资源和大小等

handleEmoji(span: MyCustomSpan) {
  this.styledStrings.push(new MutableStyledString(new ImageAttachment({
    resourceValue: EMOJI_DATA.get(span.content),
    size: {
      width: 16,
      height: 16
    }
  })));
}

第四步:将生成的属性信息拼接成属性字符串,并绑定到 Text 组件以进行渲染显示

static handleStyledString(styledStrings: MutableStyledString[]): TextController {
  let controller: TextController = new TextController();
  let paragraphStyledString: MutableStyledString = new MutableStyledString('', []);

  styledStrings.forEach((mutableStyledString: MutableStyledString) => {
    paragraphStyledString.appendStyledString(mutableStyledString);
  })

  controller.setStyledString(paragraphStyledString);
  return controller;
}

五、实现自定义的图文元素

场景描述

文中包含小图标与文本的组合,点击可跳转至详情页面。
在这里插入图片描述

实现原理

文本中包含一个系统小图标和一段高亮显示的文字,点击可跳转至详情页面。

选择的方案:需要自定义一个包含系统图标的超链接文本,可以通过属性字符串 StyledString 中的 ImageAttachment 属性来加载系统图片,并通过 TextStyle 属性设置来调整字体样式,点击事件则可以通过 GestureStyle 属性来实现。
在这里插入图片描述

开发步骤

第一步:初始化时通过 TextStyle 属性定义文字样式

textAttribute: TextStyle = new TextStyle({
  fontColor: $r('app.color.styled_text_link_font_color'),
  fontSize: LengthMetrics.fp(14)
});

第二步:使用 GestureStyle 属性定义点击超链接时的跳转事件

generateClickStyle(span: MyCustomSpan): GestureStyle {
  return new GestureStyle({
    onClick: () => {
      this.linkClickCallback(span);
    }
  })
}

第三步:实现点击跳转

private linkClickCallback: (span: MyCustomSpan) => void =
  (span: MyCustomSpan) => {
    // 根据文本超链接类型进行处理
    if (span) {
      let uiContext = this.getUIContext();
      let router = uiContext.getRouter();
      if (span.url !== null) {
        router.pushUrl({ url: span.url });
      }
    }
  };

第四步:循环处理文本数据,并生成属性字符串

handleStyledString() {
  if (this.systemLanguage === 'zh-Hans') {
    this.spans = VideoLinkMock;
  } else {
    this.spans = VideoLinkMock_EN;
  }
  this.spans.forEach((span) => {
    if (span.url) {
      this.handleVideoLink(span);
    } else {
      this.styledStrings.push(new MutableStyledString(span.content, []));
    }
  });

  this.controller = HandleData.handleStyledString(this.styledStrings);
}

第五步:设置小图标、文本样式和点击事件

handleVideoLink(span: MyCustomSpan) {
  // 如果视频链接图标的 pixelMap 存在,在对应链接前添加图片附件属性字符串
  this.styledStrings.push(new MutableStyledString(new ImageAttachment({
    resourceValue: $r('app.media.play_round_rectangle'),
    size: {
      width: $r('app.integer.styled_text_video_link_icon_size'),
      height: $r('app.integer.styled_text_video_link_icon_size')
    },
    verticalAlign: ImageSpanAlignment.CENTER,
    objectFit: ImageFit.Contain
  })));
  this.styledStrings.push(new MutableStyledString(span.content, [{
    start: 0,
    length: span.content.length,
    styledKey: StyledStringKey.GESTURE,
    styledValue: this.generateClickStyle(span)
  }, {
    start: 0,
    length: span.content.length,
    styledKey: StyledStringKey.FONT,
    styledValue: this.textAttribute
  }]));
}

第六步:将生成的属性信息拼接成属性字符串,并绑定到 Text 组件以进行渲染显示

static handleStyledString(styledStrings: MutableStyledString[]): TextController {
  let controller: TextController = new TextController();
  let paragraphStyledString: MutableStyledString = new MutableStyledString('', []);

  styledStrings.forEach((mutableStyledString: MutableStyledString) => {
    paragraphStyledString.appendStyledString(mutableStyledString);
  })

  controller.setStyledString(paragraphStyledString);
  return controller;
}

六、实现图标与文本的组合元素

场景描述

文中包含自定义的小图标与文本的组合。
在这里插入图片描述

实现原理

文本中包含一个小图标、文字和背景颜色的复杂样式。

选择的方案:需要通过属性字符串 StyledString 属性中的自定义 CustomSpan 来进行绘制。
在这里插入图片描述

开发步骤

第一步:创建自定义的 CustomSpan 以绘制自定义样式

export class MyDrawCustomSpan extends CustomSpan {
  width: number = 0;
  word: string = "drawing";
  height: number = 10;
  systemLanguage: string = 'zh-Hans';
  color: string | undefined = undefined;
  gUIContext: UIContext | undefined = undefined;

  // 绘制
  onDraw(context: DrawContext, options: CustomSpanDrawInfo) {
    let canvas = context.canvas;

    // 设置画笔
    const brush = new drawing.Brush();
    // ...

    // 计算偏移量
    let _left = options.x - 50;
    if (this.systemLanguage !== 'zh-Hans') {
      _left = options.x - 40;
    }

    // 绘制圆角矩形
    let rect: common2D.Rect = {
      left: _left,
      top: options.lineTop + 11,
      right: options.x + this.width,
      bottom: options.lineBottom
    };

    let roundRect = new drawing.RoundRect(rect, 10, 10);
    canvas.drawRoundRect(roundRect);
    // ...

    const font = new drawing.Font();
    font.setSize(40);
    const textBlob = drawing.TextBlob.makeFromString(this.word, font, drawing.TextEncoding.TEXT_ENCODING_UTF8);
    canvas.attachBrush(brush);
    canvas.drawTextBlob(textBlob, options.x + 5, options.lineBottom - 10);
    canvas.detachBrush();
  }

  setWord(word: string) {
    this.word = word;
  }
}

第二步:循环处理文本数据,并生成属性字符串

handleStyledString() {
  if (this.systemLanguage === 'zh-Hans') {
    this.spans = ImageTextMock;
  } else {
    this.spans = ImageTextMock_EN;
  }
  this.spans.forEach((span) => {
    if (span.url) {
      this.handleImageText(span);
    } else {
      this.styledStrings.push(new MutableStyledString(span.content, []));
    }
  });

  this.controller = HandleData.handleStyledString(this.styledStrings);
}

第三步:设置自定义图文元素

handleImageText(span: MyCustomSpan) {
  let resourceStr = $r('app.media.doc_plaintext_green');
  // ...

  this.styledStrings.push(new MutableStyledString(new ImageAttachment({
    resourceValue: resourceStr,
    size: {
      width: 13,
      height: 13
    },
    layoutStyle: {
      margin: { top: 4 }
    },
    verticalAlign: ImageSpanAlignment.CENTER
  })));
  
  // 根据语言计算所需宽度
  let width = 15 + 40 * span.content.length;
  if (this.systemLanguage !== 'zh-Hans') {
    width = 25 + 21 * span.content.length;
  }
  this.styledStrings.push(new MutableStyledString(new MyDrawCustomSpan(
    span.content, width, 20, this.systemLanguage, span.url, gUIContext
  )));
}

第四步:将生成的属性信息拼接成属性字符串,并绑定到 Text 组件以进行渲染和显示

static handleStyledString(styledStrings: MutableStyledString[]): TextController {
  let controller: TextController = new TextController();
  let paragraphStyledString: MutableStyledString = new MutableStyledString('', []);

  styledStrings.forEach((mutableStyledString: MutableStyledString) => {
    paragraphStyledString.appendStyledString(mutableStyledString);
  })

  controller.setStyledString(paragraphStyledString);
  return controller;
}

七、总结一下

富文本显示就这几个核心:

  1. 技术选型:简单场景用 Text+Span,复杂场景用 StyledStringRichEditor
  2. 高亮超链接:使用 TextStyle 设置样式,GestureStyle 实现点击事件
  3. 图片表情:使用 ImageAttachment 加载图片,替换文本中的表情字符
  4. 自定义图文:使用 CustomSpan 进行自定义绘制
  5. 图标 + 文本:组合使用 ImageAttachmentTextStyle
  6. 属性字符串拼接:使用 MutableStyledString 拼接,通过 TextController 绑定到 Text 组件

记住啊,长列表场景别用 Web 和 RichText,性能会受影响!

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐