前面 14 篇把项目拆开讲了。最后这一篇换个角度:如果你自己从零写一个 HarmonyOS7 富文本收起展开功能,应该按什么顺序来?

项目地址:https://gitcode.com/HarmonyOS_Samples/TextExpand

我不会让你一上来写 ParagraphBuilder。那样太容易乱。更稳的路线是从页面、数据、显示、测量、交互一步步推进。

第一步:先做一个能打开的页面

先别管富文本,保证页面能跳转、能显示列表。

首页按钮可以这样写:

Button('进入富文本示例')
  .width('100%')
  .onClick(() => {
    this.getUIContext().getRouter().pushUrl({
      url: 'pages/RichTextExpand'
    });
  })

An ink-style schematic diagram illustrating the 7-

目标很简单:点按钮能进入页面。这个没跑通,后面所有功能都没地方展示。

第二步:把信息流卡片搭出来

先写卡片结构:头像、昵称、正文区域、图片、操作栏。

可以参考项目里的 RichItemPart

ListItem() {
  Flex({ direction: FlexDirection.Column }) {
    Row() {
      Image(this.profileImg)
      Column() {
        Text($r('app.string.text_expand_text_name'))
        Text($r('app.string.text_expand_text_date'))
      }
    }

    RichTextExpandView({
      dataModel: this.dataModel,
      textSectionAttribute: new RichTextSectionAttribute(this.rawTitle),
      lastSpanAttribute: new LastSpanAttribute(0, 2, this.handleContent(), 16)
    })
  }
}

先让页面长得像样,再逐步补逻辑。

第三步:设计富文本数据模型

不要直接传一整段 HTML 或字符串。先拆成片段:

export class RichTextContentModel {
  index: number = 0;
  length: number = 0;
  type: string = '';
  images: string[] = [];
  content: string = '';
  link: string = '';
  fontColor: string = '#000';
  fontSize: number = 16;
  imgWidth: number = 16;
  imgHeight: number = 16;
  shortContent: string = '';
}

这一步很重要。后面截断、渲染、点击链接,全靠这个模型撑住。

第四步:先完整渲染,不急着折叠

先把 textArray 完整画出来:

Text() {
  ForEach(this.textModifier.textContentArray, (item: RichTextContentModel) => {
    if (item.type === 'text') {
      Span(item.content)
        .fontSize(item.fontSize)
        .fontColor(item.fontColor)
    } else if (item.type === 'images') {
      ForEach(item.images, (url: string) => {
        ImageSpan($r('app.media.' + url))
          .width(item.imgWidth)
          .height(item.imgHeight)
      })
    } else if (item.type === 'link') {
      Span(item.content)
        .fontColor(item.fontColor)
    }
  })
}

只要完整渲染没问题,再做折叠。别同时调渲染和截断,排查会很痛苦。

第五步:用 ParagraphBuilder 计算行数

能显示后,再判断是否超过最大行数:

let paragraph = TextUtils.getParagraph(
  this.dataModel.textArray,
  this.dataModel.fontSize,
  this.textSectionAttribute.constraintWidth
);

this.textModifier.needProcess =
  paragraph.getLineCount() > this.textSectionAttribute.maxLines;

短内容不显示“展开”,长内容才进入截断逻辑。

第六步:给 ... 展开 留位置

折叠不是简单截到第三行末尾。要先测按钮宽度:

const minLinesTextSize = uiContext?.getMeasureUtils().measureTextSize({
  textContent: suffix + lastSpan,
  fontSize: dataModel.fontSize,
});
const widthMore = uiContext?.px2vp(Number(minLinesTextSize?.width));

然后把第三行的 x 坐标往左挪,避免按钮换行。

第七步:用坐标反查截断字符

关键 API 是:

let positionWithAffinity = paragraph.getGlyphPositionAtCoordinate(x, y);
let index = positionWithAffinity.affinity === text.Affinity.UPSTREAM
  ? positionWithAffinity.position
  : positionWithAffinity.position + 1;

A hand-drawn ink flowchart showing the conditional

拿到全局 index 后,再映射回 textArray 里的片段,生成 shortContent

第八步:加点击切换状态

状态切换可以参考项目:

process(): void {
  if (this.expanded) {
    this.expandText();
    this.expanded = false;
    this.textModifier.exceedOneLine = true;
  } else {
    this.expanded = true;
    this.textModifier.exceedOneLine = false;
    this.collapseText();
  }
}

别忘了短文本不响应点击:

if (!this.textModifier.needProcess) {
  return;
}

第九步:补齐链接和图片边界

示例项目已经能跑通常规场景。如果你做业务项目,还要继续增强:

  • 链接点击跳转真实页面。
  • 图片片段被截断时单独处理。
  • 不同字号片段参与排版。
  • 横竖屏或不同设备宽度重新计算。
  • 列表复用时保存每条内容展开状态。

这些不是第一版必须做,但上线前最好考虑。

最终效果再对一下

富文本折叠:

富文本折叠

富文本展开:

富文本展开

如果你的效果和它接近,说明主流程基本没问题。

写在最后

这套 HarmonyOS7 富文本收起展开案例,真正值得学的不是某一个 API,而是完整拆解问题的方式:先建模,再排版,再截断,再交互。

别一上来就追求完美。先让完整内容显示,再让折叠跑通,最后处理边界。按这个顺序写,真的不容易乱。

Logo

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

更多推荐