Skip to content
🌊海洋蓝
🌸樱花粉
🍃森林绿
🔮幻夜紫
🌙暗夜黑

HarmonyOS 文本展开折叠功能 - 从入门到精通

📚 前言

在我们日常使用的社交媒体应用中,你是否注意到这样的场景:当一条评论或动态的文字内容过长时,会显示"...展开"的按钮,点击后可以查看完整内容,再次点击"收起"又能折叠回原来的状态。这就是我们今天要学习的文本展开折叠功能

🎯 为什么需要文本展开折叠?

应用场景

  • 社交媒体动态:微博、朋友圈的长文本内容
  • 评论系统:博客、新闻下的用户评论
  • 商品描述:电商应用中的产品详情
  • 文章摘要:新闻应用的文章预览

用户体验价值

  1. 节省屏幕空间:避免长文本占据过多界面
  2. 提升浏览效率:用户可以快速浏览多条内容
  3. 保持界面整洁:统一的视觉呈现效果

💡 技术挑战分析

在 HarmonyOS 开发中,实现文本展开折叠功能面临以下技术难点:

纯文本场景的挑战

  • 如何准确计算文本高度?
  • 怎样确定截断位置?
  • "..."和"展开"按钮如何定位?

富文本场景的挑战(更复杂)

  • 包含表情图片的文本如何处理?
  • 不同字号、颜色的文字混排
  • 超链接等交互元素的处理
  • 图片位置和大小不固定导致的计算复杂性

功能展示图


📖 第一部分:纯文本展开折叠

🎯 需求分析

我们先从最简单的纯文本场景开始学习。这种场景的特点是:

包含内容:只有纯文字,无图片、表情等元素
功能要求:超过 3 行时显示"...展开",点击展开后显示"收起"
交互逻辑:点击展开/收起按钮切换显示状态

纯文本效果图

🔧 核心原理解析

基本思路

想象你要在一本书的第 3 页末尾插入一个"展开"按钮,你需要知道:

  1. 📏 第 3 页能容纳多少文字?
  2. 📍 最后一个文字的位置在哪里?
  3. 🔘 按钮应该放在什么位置?

原理示意图

计算逻辑

  1. 测量文本总高度 → 判断是否需要折叠
  2. 计算指定行数的高度 → 确定截断位置
  3. 计算截断点坐标 → 确定"..."的位置
  4. 添加交互按钮 → 实现展开/收起功能

🛠️ 详细实现步骤

步骤 1:计算原始文本高度

typescript
// 使用 HarmonyOS 提供的测量 API
import measure from "@ohos.measure";

// 测量完整文本的高度
const fullTextHeight = measure.measureTextSize({
  textContent: originalText, // 原始文本内容
  fontSize: "16fp", // 字体大小
  fontFamily: "HarmonyOS Sans", // 字体家族
  constraintWidth: containerWidth, // 容器宽度限制
});

💡 新手提示measureTextSize() 是 HarmonyOS 提供的文本测量工具,就像用尺子测量文字的长度和高度一样。

步骤 2:计算收起状态的高度

typescript
// 计算指定行数(如3行)的高度
const collapsedHeight = measure.measureTextSize({
  textContent: "测试文本\n测试文本\n测试文本", // 3行测试文本
  fontSize: "16fp",
  fontFamily: "HarmonyOS Sans",
  constraintWidth: containerWidth,
});

// 判断是否需要显示展开按钮
const needExpand = fullTextHeight.height > collapsedHeight.height;

💡 新手提示:通过比较完整文本高度和限制行数高度,我们就知道是否需要添加"展开"功能。

步骤 3:计算截断文本内容

typescript
// 逐步减少文字,直到高度符合要求
function getCollapsedText(originalText: string, maxHeight: number): string {
  let text = originalText;

  while (text.length > 0) {
    // 预留"...展开"按钮的空间
    const testText = text + "...展开";
    const textHeight = measure.measureTextSize({
      textContent: testText,
      fontSize: "16fp",
      fontFamily: "HarmonyOS Sans",
      constraintWidth: containerWidth,
    });

    // 如果高度合适,返回截断后的文本
    if (textHeight.height <= maxHeight) {
      return text;
    }

    // 继续减少文字
    text = text.substring(0, text.length - 1);
  }

  return text;
}

💡 新手提示:这个过程就像试衣服一样,一点点调整直到合适为止。

步骤 4:实现交互逻辑

typescript
@Entry
@Component
struct TextExpandDemo {
  @State isExpanded: boolean = false;      // 展开状态
  @State displayText: string = '';         // 显示的文本
  private originalText: string = '你的长文本内容...';
  private collapsedText: string = '';      // 收起状态的文本

  aboutToAppear() {
    // 计算收起状态的文本
    this.collapsedText = this.getCollapsedText(this.originalText);
    this.displayText = this.collapsedText;
  }

  build() {
    Column() {
      // 显示文本
      Text(this.displayText)
        .fontSize(16)
        .fontFamily('HarmonyOS Sans')
        .width('100%')

      // 展开/收起按钮
      if (!this.isExpanded && this.needExpandButton()) {
        Text('...展开')
          .fontSize(16)
          .fontColor(Color.Blue)
          .onClick(() => {
            this.isExpanded = true;
            this.displayText = this.originalText;
          })
      } else if (this.isExpanded) {
        Text('收起')
          .fontSize(16)
          .fontColor(Color.Blue)
          .onClick(() => {
            this.isExpanded = false;
            this.displayText = this.collapsedText;
          })
      }
    }
  }
}

💡 新手提示@State 装饰器让变量具有响应性,当值改变时界面会自动更新。


📚 第二部分:富文本展开折叠(进阶)

🎯 场景分析

富文本场景比纯文本复杂得多,包含以下元素:

🎨 多样化内容

  • 表情图片(emoji)
  • 不同颜色的文字
  • 不同字号的文字
  • 超链接等交互元素

🔧 技术难点

  • 图片大小不固定
  • 混合排版计算复杂
  • 精确定位截断点困难

富文本效果图

🔧 高级原理解析

富文本的处理就像排版一本杂志:

  • 📝 文字有不同的字体和颜色
  • 🖼️ 图片需要占位和定位
  • 📐 需要精确计算每个元素的位置
  • ✂️ 在合适的位置进行"剪切"

富文本原理图

🛠️ 高级实现步骤

步骤 1:引入图形文本处理模块

typescript
import { graphics } from "@kit.ArkGraphics2D";

// 引入必要的文本处理工具
// graphics.text 提供了强大的文本排版能力

💡 新手提示graphics.text 就像一个专业的排版工具,能够处理复杂的文本布局。

步骤 2:创建段落构建器

typescript
// 创建段落构建器和样式
const style = new graphics.text.TextStyle();
style.color = Color.Black;
style.fontSize = vp2px(16); // 注意单位转换

const paragraphStyle = new graphics.text.ParagraphStyle();
paragraphStyle.textStyle = style;

const builder = new graphics.text.ParagraphBuilder(paragraphStyle);

💡 新手提示

  • vp2px() 用于单位转换(虚拟像素转物理像素)
  • 需要考虑系统字体缩放设置

步骤 3:添加内容元素

typescript
// 遍历富文本内容数组
for (let item of richTextArray) {
  if (item.type === "text") {
    // 添加文本
    if (item.color) {
      style.color = item.color;
      builder.pushStyle(style);
    }

    builder.addText(item.content);

    if (item.color) {
      builder.popStyle(); // 恢复之前的样式
    }
  } else if (item.type === "image") {
    // 添加图片占位符
    const placeholderStyle = new graphics.text.PlaceholderSpan();
    placeholderStyle.width = vp2px(item.width);
    placeholderStyle.height = vp2px(item.height);
    placeholderStyle.alignment = graphics.text.PlaceholderAlignment.BASELINE;

    builder.addPlaceholder(placeholderStyle);
  }
}

💡 新手提示

  • pushStyle()popStyle() 就像 CSS 的样式继承
  • 图片用占位符表示,实际渲染时再替换

步骤 4:预排版计算

typescript
// 构建段落对象
const paragraph = builder.build();

// 进行预排版(重要!)
paragraph.layoutSync(containerWidth); // 宽度要与实际显示一致

// 获取排版信息
const lineCount = paragraph.getLineCount();
const needCollapse = lineCount > maxLines;

💡 新手提示layoutSync() 就像提前彩排,让我们知道内容会如何显示。

步骤 5:计算精确截断位置

typescript
function calculateCutoffPosition(paragraph: graphics.text.Paragraph): number {
  // 计算"...展开"按钮占用的宽度
  const moreButtonWidth = measure.measureTextSize({
    textContent: "...展开",
    fontSize: "16fp",
  }).width;

  // 计算最后一行可用宽度
  const lastLineY = 0;
  for (let i = 0; i < maxLines - 1; i++) {
    lastLineY += paragraph.getLineHeight(i);
  }
  lastLineY += paragraph.getLineHeight(maxLines - 1) / 2; // 最后一行中间位置

  // 计算X坐标(需要为按钮预留空间)
  const lastLineX = containerWidth - moreButtonWidth;

  // 根据坐标获取字符索引
  const position = paragraph.getGlyphPositionAtCoordinate(lastLineX, lastLineY);

  return position.position; // 返回截断位置的字符索引
}

💡 新手提示:这就像在地图上通过坐标找到具体位置一样。

步骤 6:实现完整的交互组件

typescript
@Entry
@Component
struct RichTextExpandDemo {
  @State isExpanded: boolean = false;
  @State displayContent: Array<RichTextItem> = [];
  private originalContent: Array<RichTextItem> = [];  // 原始富文本数据
  private collapsedContent: Array<RichTextItem> = [];  // 收起状态数据

  aboutToAppear() {
    this.processRichText();
  }

  processRichText() {
    // 使用上述步骤计算截断位置
    const cutoffIndex = this.calculateCutoffPosition();

    // 生成收起状态的内容数组
    this.collapsedContent = this.originalContent.slice(0, cutoffIndex);
    this.displayContent = this.collapsedContent;
  }

  build() {
    Column() {
      // 渲染富文本内容
      ForEach(this.displayContent, (item: RichTextItem) => {
        if (item.type === 'text') {
          Text(item.content)
            .fontSize(item.fontSize || 16)
            .fontColor(item.color || Color.Black)
        } else if (item.type === 'image') {
          Image(item.src)
            .width(item.width)
            .height(item.height)
        }
      })

      // 展开/收起按钮
      if (!this.isExpanded && this.needExpandButton()) {
        Row() {
          Text('...')
          Text('展开')
            .fontColor(Color.Blue)
            .onClick(() => this.expandText())
        }
      } else if (this.isExpanded) {
        Text('收起')
          .fontColor(Color.Blue)
          .onClick(() => this.collapseText())
      }
    }
  }

  expandText() {
    this.isExpanded = true;
    this.displayContent = this.originalContent;
  }

  collapseText() {
    this.isExpanded = false;
    this.displayContent = this.collapsedContent;
  }
}

🎯 关键技术要点总结

纯文本方案

适用场景:简单的文字内容展开折叠
核心 APImeasure.measureTextSize()
计算方式:文本高度比较 + 字符串截断
难度等级:⭐⭐⭐

富文本方案

适用场景:包含图片、多样式文字的复杂内容
核心 APIgraphics.text 系列接口
计算方式:预排版 + 坐标转换 + 精确定位
难度等级:⭐⭐⭐⭐⭐

⚠️ 开发注意事项

性能优化

  1. 避免频繁计算:缓存计算结果
  2. 合理使用预排版:只在必要时进行计算
  3. 内存管理:及时释放不用的对象

兼容性考虑

  1. 字体缩放:适配系统字体大小设置
  2. 屏幕适配:考虑不同屏幕密度
  3. 主题适配:支持深色模式等主题切换

用户体验

  1. 动画效果:添加平滑的展开收起动画
  2. 加载状态:长文本处理时显示加载提示
  3. 无障碍支持:添加合适的无障碍描述

🚀 进阶扩展

学会了基础实现后,你还可以尝试:

  1. 添加渐变遮罩效果:让收起状态更自然
  2. 支持多种展开方式:点击、滑动、长按等
  3. 智能截断优化:避免在词语中间截断
  4. 自定义按钮样式:更丰富的视觉效果

💭 总结

文本展开折叠功能看似简单,实际包含了很多细节处理。通过本文的学习,你应该能够:

  1. 理解文本展开折叠的应用场景和价值
  2. 掌握纯文本场景的实现方法
  3. 了解富文本场景的处理思路
  4. 具备解决相关问题的能力

记住,好的用户体验往往来自于对细节的精心雕琢。希望这篇文章能帮助你在 HarmonyOS 开发路上更进一步!


💡 学习建议:建议先从纯文本方案开始实践,熟练后再挑战富文本方案。每个步骤都可以单独验证,这样更容易定位和解决问题。

Released under the MIT License.