HarmonyOS 文本展开折叠功能 - 从入门到精通
📚 前言
在我们日常使用的社交媒体应用中,你是否注意到这样的场景:当一条评论或动态的文字内容过长时,会显示"...展开"的按钮,点击后可以查看完整内容,再次点击"收起"又能折叠回原来的状态。这就是我们今天要学习的文本展开折叠功能。
🎯 为什么需要文本展开折叠?
应用场景
- 社交媒体动态:微博、朋友圈的长文本内容
- 评论系统:博客、新闻下的用户评论
- 商品描述:电商应用中的产品详情
- 文章摘要:新闻应用的文章预览
用户体验价值
- 节省屏幕空间:避免长文本占据过多界面
- 提升浏览效率:用户可以快速浏览多条内容
- 保持界面整洁:统一的视觉呈现效果
💡 技术挑战分析
在 HarmonyOS 开发中,实现文本展开折叠功能面临以下技术难点:
纯文本场景的挑战
- 如何准确计算文本高度?
- 怎样确定截断位置?
- "..."和"展开"按钮如何定位?
富文本场景的挑战(更复杂)
- 包含表情图片的文本如何处理?
- 不同字号、颜色的文字混排
- 超链接等交互元素的处理
- 图片位置和大小不固定导致的计算复杂性

📖 第一部分:纯文本展开折叠
🎯 需求分析
我们先从最简单的纯文本场景开始学习。这种场景的特点是:
✅ 包含内容:只有纯文字,无图片、表情等元素
✅ 功能要求:超过 3 行时显示"...展开",点击展开后显示"收起"
✅ 交互逻辑:点击展开/收起按钮切换显示状态

🔧 核心原理解析
基本思路
想象你要在一本书的第 3 页末尾插入一个"展开"按钮,你需要知道:
- 📏 第 3 页能容纳多少文字?
- 📍 最后一个文字的位置在哪里?
- 🔘 按钮应该放在什么位置?

计算逻辑
- 测量文本总高度 → 判断是否需要折叠
- 计算指定行数的高度 → 确定截断位置
- 计算截断点坐标 → 确定"..."的位置
- 添加交互按钮 → 实现展开/收起功能
🛠️ 详细实现步骤
步骤 1:计算原始文本高度
// 使用 HarmonyOS 提供的测量 API
import measure from "@ohos.measure";
// 测量完整文本的高度
const fullTextHeight = measure.measureTextSize({
textContent: originalText, // 原始文本内容
fontSize: "16fp", // 字体大小
fontFamily: "HarmonyOS Sans", // 字体家族
constraintWidth: containerWidth, // 容器宽度限制
});💡 新手提示:measureTextSize() 是 HarmonyOS 提供的文本测量工具,就像用尺子测量文字的长度和高度一样。
步骤 2:计算收起状态的高度
// 计算指定行数(如3行)的高度
const collapsedHeight = measure.measureTextSize({
textContent: "测试文本\n测试文本\n测试文本", // 3行测试文本
fontSize: "16fp",
fontFamily: "HarmonyOS Sans",
constraintWidth: containerWidth,
});
// 判断是否需要显示展开按钮
const needExpand = fullTextHeight.height > collapsedHeight.height;💡 新手提示:通过比较完整文本高度和限制行数高度,我们就知道是否需要添加"展开"功能。
步骤 3:计算截断文本内容
// 逐步减少文字,直到高度符合要求
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:实现交互逻辑
@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:引入图形文本处理模块
import { graphics } from "@kit.ArkGraphics2D";
// 引入必要的文本处理工具
// graphics.text 提供了强大的文本排版能力💡 新手提示:graphics.text 就像一个专业的排版工具,能够处理复杂的文本布局。
步骤 2:创建段落构建器
// 创建段落构建器和样式
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:添加内容元素
// 遍历富文本内容数组
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:预排版计算
// 构建段落对象
const paragraph = builder.build();
// 进行预排版(重要!)
paragraph.layoutSync(containerWidth); // 宽度要与实际显示一致
// 获取排版信息
const lineCount = paragraph.getLineCount();
const needCollapse = lineCount > maxLines;💡 新手提示:layoutSync() 就像提前彩排,让我们知道内容会如何显示。
步骤 5:计算精确截断位置
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:实现完整的交互组件
@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;
}
}🎯 关键技术要点总结
纯文本方案
✅ 适用场景:简单的文字内容展开折叠
✅ 核心 API:measure.measureTextSize()
✅ 计算方式:文本高度比较 + 字符串截断
✅ 难度等级:⭐⭐⭐
富文本方案
✅ 适用场景:包含图片、多样式文字的复杂内容
✅ 核心 API:graphics.text 系列接口
✅ 计算方式:预排版 + 坐标转换 + 精确定位
✅ 难度等级:⭐⭐⭐⭐⭐
⚠️ 开发注意事项
性能优化
- 避免频繁计算:缓存计算结果
- 合理使用预排版:只在必要时进行计算
- 内存管理:及时释放不用的对象
兼容性考虑
- 字体缩放:适配系统字体大小设置
- 屏幕适配:考虑不同屏幕密度
- 主题适配:支持深色模式等主题切换
用户体验
- 动画效果:添加平滑的展开收起动画
- 加载状态:长文本处理时显示加载提示
- 无障碍支持:添加合适的无障碍描述
🚀 进阶扩展
学会了基础实现后,你还可以尝试:
- 添加渐变遮罩效果:让收起状态更自然
- 支持多种展开方式:点击、滑动、长按等
- 智能截断优化:避免在词语中间截断
- 自定义按钮样式:更丰富的视觉效果
💭 总结
文本展开折叠功能看似简单,实际包含了很多细节处理。通过本文的学习,你应该能够:
- 理解文本展开折叠的应用场景和价值
- 掌握纯文本场景的实现方法
- 了解富文本场景的处理思路
- 具备解决相关问题的能力
记住,好的用户体验往往来自于对细节的精心雕琢。希望这篇文章能帮助你在 HarmonyOS 开发路上更进一步!
💡 学习建议:建议先从纯文本方案开始实践,熟练后再挑战富文本方案。每个步骤都可以单独验证,这样更容易定位和解决问题。