鸿蒙学习实战之路:HarmonyOS 文本展开折叠组件实现指南
HarmonyOS 文本展开折叠组件实现指南
文本展开折叠是现代应用中常见的交互模式,能够在有限空间内展示长文本内容,提升用户体验
关于本文
本文将详细介绍在 HarmonyOS 5.0 中实现文本展开折叠功能的完整解决方案,从纯文本到富文本,帮助开发者轻松实现类似朋友圈、新闻列表等场景的文本交互效果。
官方文档是个好东西!
官方文档是个好东西!
官方文档是个好东西!
重要的内容说三遍
参考文档
环境要求
| 软件/环境 | 版本要求 |
|---|---|
| HarmonyOS | 5.0+ |
| API Level | 12+ |
| DevEco Studio | 4.0+ |
文本展开折叠基础概念
什么是文本展开折叠?
文本展开折叠功能允许将长文本内容默认只显示部分(通常是几行),用户可以通过点击操作展开查看完整内容,再次点击则收起恢复到默认显示状态。
主要应用场景:
- 朋友圈或社交动态列表
- 新闻文章摘要展示
- 商品详情描述
- 评论区内容展示
实现原理
文本展开折叠功能的核心实现原理包括以下几个步骤:
- 文本测量:计算完整文本和限制行数的高度
- 需求判断:对比两种高度,确定是否需要折叠处理
- 文本截断:通过二分查找算法找到最佳的折叠点
- 状态管理:管理展开/折叠的状态切换
纯文本展开折叠实现
基础常量定义
// 完整的示例文本
const FULL_TEXT: string =
"君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪。人生得意须尽欢,莫使金樽空对月。天生我材必有用,千金散尽还复来。烹羊宰牛且为乐,会须一饮三百杯。岑夫子,丹丘生,将进酒,杯莫停。与君歌一曲,请君为我倾耳听。钟鼓馔玉不足贵,但愿长醉不愿醒。古来圣贤皆寂寞,惟有饮者留其名。陈王昔时宴平乐,斗酒十千恣欢谑。主人何为言少钱,径须沽取对君酌。五花马,千金裘,呼儿将出换美酒,与尔同销万古愁。";
const TEXT_WIDTH: number = 300; // 文本容器宽度
const COLLAPSE_LINES: number = 2; // 折叠时显示的行数
const ELLIPSIS: string = "..."; // 省略号
const EXPAND_STR: string = "展开"; // 展开按钮文本
const COLLAPSE_STR: string = "收起"; // 收起按钮文本
核心组件实现
import { Text, TextAlign, Flex, FlexDirection, Alignment } from '@ohos.arkui.component';
import { measureTextSize } from '@ohos.measure';
@Entry
@Component
struct TextExpandCollapseExample {
@State isExpanded: boolean = false; // 展开状态
@State displayText: string = ""; // 显示的文本内容
@State needExpand: boolean = false; // 是否需要展开功能
onPageShow() {
this.checkNeedExpand();
}
// 检查是否需要展开功能
checkNeedExpand() {
// 测量完整文本高度
const fullSize = measureTextSize({
textContent: FULL_TEXT,
fontSize: 16,
fontFamily: 'HarmonyOS Sans',
constraintWidth: TEXT_WIDTH
});
// 测量限制行数的文本高度
const collapsedSize = measureTextSize({
textContent: this.getLinesText(COLLAPSE_LINES),
fontSize: 16,
fontFamily: 'HarmonyOS Sans',
constraintWidth: TEXT_WIDTH
});
// 判断是否需要展开功能
this.needExpand = fullSize.height > collapsedSize.height;
// 设置初始显示文本
if (this.needExpand) {
this.displayText = this.getCollapsedText();
} else {
this.displayText = FULL_TEXT;
}
}
// 获取指定行数的测试文本
getLinesText(lines: number): string {
let result = "";
for (let i = 0; i < lines; i++) {
result += "测试文本\n";
}
return result.trim();
}
// 获取折叠后的文本
getCollapsedText(): string {
// 使用二分查找算法找到最佳截断位置
let left = 0;
let right = FULL_TEXT.length;
let result = FULL_TEXT;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
const testText = FULL_TEXT.substring(0, mid) + ELLIPSIS + EXPAND_STR;
const testSize = measureTextSize({
textContent: testText,
fontSize: 16,
fontFamily: 'HarmonyOS Sans',
constraintWidth: TEXT_WIDTH
});
const maxHeight = this.getMaxHeight();
if (testSize.height <= maxHeight) {
result = FULL_TEXT.substring(0, mid) + ELLIPSIS;
left = mid + 1;
} else {
right = mid - 1;
}
}
return result;
}
// 获取最大允许高度
getMaxHeight(): number {
const lineHeight = 24; // 估计行高
return lineHeight * COLLAPSE_LINES;
}
// 切换展开/折叠状态
toggleExpand() {
this.isExpanded = !this.isExpanded;
this.displayText = this.isExpanded ? FULL_TEXT : this.getCollapsedText();
}
build() {
Column() {
// 文本内容
Text(this.displayText)
.fontSize(16)
.width(TEXT_WIDTH)
.textAlign(TextAlign.Start)
.margin({ bottom: 10 })
// 展开/收起按钮
if (this.needExpand) {
Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.End })
.width(TEXT_WIDTH)
.padding({ right: 5 })
{
Text(this.isExpanded ? COLLAPSE_STR : EXPAND_STR)
.fontColor('#007DFF')
.fontSize(14)
.onClick(() => {
this.toggleExpand();
})
}
}
}
.padding(20)
.width('100%')
}
}
组件化封装
为了提高代码复用性,我们可以将文本展开折叠功能封装成一个独立的组件:
TextExpandView 组件
import { Text, TextAlign, Flex, FlexAlign, Prop } from '@ohos.arkui.component';
import { measureTextSize } from '@ohos.measure';
@Component
struct TextExpandView {
@Prop text: string; // 文本内容
@Prop maxLines: number = 2; // 最大显示行数
@Prop fontSize: number = 16; // 字体大小
@Prop lineHeight: number = 24; // 行高
@Prop constraintWidth: number = 300; // 文本容器宽度
@Prop expandText: string = "展开"; // 展开按钮文本
@Prop collapseText: string = "收起"; // 收起按钮文本
@Prop ellipsis: string = "..."; // 省略号
@State isExpanded: boolean = false; // 展开状态
@State displayText: string = ""; // 显示的文本内容
@State needExpand: boolean = false; // 是否需要展开功能
aboutToAppear() {
this.checkNeedExpand();
}
// 检查是否需要展开功能
checkNeedExpand() {
// 测量完整文本高度
const fullSize = measureTextSize({
textContent: this.text,
fontSize: this.fontSize,
constraintWidth: this.constraintWidth
});
// 计算最大允许高度
const maxHeight = this.lineHeight * this.maxLines;
// 判断是否需要展开功能
this.needExpand = fullSize.height > maxHeight;
// 设置初始显示文本
if (this.needExpand) {
this.displayText = this.getCollapsedText();
} else {
this.displayText = this.text;
}
}
// 获取折叠后的文本
getCollapsedText(): string {
let left = 0;
let right = this.text.length;
let result = this.text;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
const testText = this.text.substring(0, mid) + this.ellipsis + this.expandText;
const testSize = measureTextSize({
textContent: testText,
fontSize: this.fontSize,
constraintWidth: this.constraintWidth
});
const maxHeight = this.lineHeight * this.maxLines;
if (testSize.height <= maxHeight) {
result = this.text.substring(0, mid) + this.ellipsis;
left = mid + 1;
} else {
right = mid - 1;
}
}
return result;
}
// 切换展开/折叠状态
toggleExpand() {
this.isExpanded = !this.isExpanded;
this.displayText = this.isExpanded ? this.text : this.getCollapsedText();
}
build() {
Column() {
Text(this.displayText)
.fontSize(this.fontSize)
.width(this.constraintWidth)
.textAlign(TextAlign.Start)
.margin({ bottom: 5 })
if (this.needExpand) {
Flex({ justifyContent: FlexAlign.End })
.width(this.constraintWidth)
{
Text(this.isExpanded ? this.collapseText : this.expandText)
.fontColor('#007DFF')
.fontSize(this.fontSize - 2)
.onClick(() => {
this.toggleExpand();
})
}
}
}
}
}
使用示例
import { Column } from '@ohos.arkui.component';
import TextExpandView from './TextExpandView'; // 导入自定义组件
@Entry
@Component
struct TextExpandExample {
private longText: string = "这是一段很长很长的文本内容,在实际应用中,我们经常会遇到需要展示长文本的场景。默认情况下,我们希望只显示几行文本,当用户需要查看更多内容时,可以点击展开按钮查看完整内容。这种交互方式可以有效节省屏幕空间,提升用户体验。";
build() {
Column() {
Text('默认展开折叠示例')
.fontSize(18)
.margin({ bottom: 20 })
// 使用封装的文本展开折叠组件
TextExpandView({
text: this.longText,
maxLines: 3,
fontSize: 16,
constraintWidth: 350
})
.margin({ bottom: 30 })
Text('自定义配置示例')
.fontSize(18)
.margin({ bottom: 20 })
// 自定义配置的文本展开折叠组件
TextExpandView({
text: this.longText,
maxLines: 2,
fontSize: 18,
lineHeight: 28,
constraintWidth: 320,
expandText: '查看更多',
collapseText: '收起'
})
}
.padding(30)
.width('100%')
.height('100%')
}
}
高级实现:图文混排展开折叠
在实际应用中,我们经常会遇到图文混排的情况,这时候需要更复杂的处理逻辑:
图文混排组件实现
import { Row, Column, Text, Image, Flex, FlexAlign } from '@ohos.arkui.component';
import { measureTextSize } from '@ohos.measure';
@Component
struct RichTextExpandView {
@Prop text: string; // 文本内容
@Prop imageSrc?: string; // 图片资源路径
@Prop maxLines: number; // 最大显示行数
@Prop constraintWidth: number; // 容器宽度
@State isExpanded: boolean = false;
@State displayText: string = "";
@State needExpand: boolean = false;
aboutToAppear() {
this.checkNeedExpand();
}
checkNeedExpand() {
// 测量文本高度
const textSize = measureTextSize({
textContent: this.text,
fontSize: 16,
constraintWidth: this.constraintWidth
});
// 计算最大允许高度(包含图片空间)
const imageHeight = this.imageSrc ? 100 : 0; // 假设图片高度为100
const maxTextHeight = 24 * this.maxLines;
const maxTotalHeight = imageHeight + maxTextHeight;
// 判断是否需要展开
this.needExpand = textSize.height > maxTextHeight;
if (this.needExpand && !this.isExpanded) {
this.displayText = this.getCollapsedText();
} else {
this.displayText = this.text;
}
}
getCollapsedText(): string {
// 类似前面的二分查找算法,略...
// 这里需要考虑图片占用的空间进行调整
return this.text.substring(0, 50) + "...";
}
toggleExpand() {
this.isExpanded = !this.isExpanded;
this.displayText = this.isExpanded ? this.text : this.getCollapsedText();
}
build() {
Column() {
Row() {
// 左侧图片
if (this.imageSrc) {
Image(this.imageSrc)
.width(80)
.height(80)
.borderRadius(8)
.margin({ right: 12 })
}
// 右侧文本
Column()
.width(this.imageSrc ? this.constraintWidth - 100 : this.constraintWidth)
{
Text(this.displayText)
.fontSize(16)
.margin({ bottom: 5 })
// 展开/收起按钮
if (this.needExpand) {
Flex({ justifyContent: FlexAlign.End })
{
Text(this.isExpanded ? "收起" : "展开")
.fontColor('#007DFF')
.fontSize(14)
.onClick(() => {
this.toggleExpand();
})
}
}
}
}
}
}
}
实际应用场景
下面是一个模拟朋友圈列表的完整示例:
import { List, ListItem, Column, Text, Image, Flex, FlexAlign } from '@ohos.arkui.component';
// 朋友圈数据
interface PostData {
id: string;
avatar: string;
nickname: string;
content: string;
image?: string;
likes: number;
comments: number;
}
@Entry
@Component
struct MomentsExample {
@State posts: PostData[] = [
{
id: '1',
avatar: '/common/avatar1.png',
nickname: '小明',
content: '今天天气真好,出去走走感觉整个人都精神了!分享一下沿途的风景,希望大家也有好心情~#自然风光 #周末愉快',
image: '/common/scenery1.png',
likes: 15,
comments: 3
},
{
id: '2',
avatar: '/common/avatar2.png',
nickname: '小红',
content: '今天尝试了新的烘焙食谱,做了一个巧克力蛋糕!味道超级棒,分享一下制作过程和成品图。材料:巧克力100克,鸡蛋3个,面粉80克,糖50克,黄油40克。步骤:1. 巧克力和黄油隔水融化;2. 鸡蛋打散加糖打发;3. 加入融化的巧克力黄油糊;4. 筛入面粉拌匀;5. 倒入模具,烤箱180度烤30分钟。',
likes: 28,
comments: 7
}
]
build() {
Column() {
Text('朋友圈')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 })
List() {
ForEach(this.posts, (post) => {
ListItem() {
Column() {
// 用户信息
Row() {
Image(post.avatar)
.width(40)
.height(40)
.borderRadius(20)
.margin({ right: 12 })
Column()
.alignItems(HorizontalAlign.Start)
{
Text(post.nickname)
.fontSize(16)
.fontWeight(FontWeight.Medium)
}
}
.margin({ bottom: 10 })
// 帖子内容(使用文本展开折叠组件)
RichTextExpandView({
text: post.content,
imageSrc: post.image,
maxLines: 3,
constraintWidth: 350
})
.margin({ bottom: 10 })
// 互动栏
Row() {
Text(`❤️ ${post.likes}`)
.fontSize(14)
.fontColor('#999')
.margin({ right: 20 })
Text(`💬 ${post.comments}`)
.fontSize(14)
.fontColor('#999')
}
.margin({ top: 10 })
}
.padding(20)
.borderBottom({ width: 1, color: '#f0f0f0' })
}
}, (post) => post.id)
}
}
.padding(20)
.width('100%')
.height('100%')
}
}
性能优化
在实现文本展开折叠功能时,需要注意以下性能优化点:
1. 避免重复测量
// 优化前
@State textMeasureCount: number = 0;
checkNeedExpand() {
// 每次都重新测量
const fullSize = measureTextSize({...});
this.textMeasureCount++;
}
// 优化后
@State cachedTextSize: number = 0;
checkNeedExpand() {
// 只有在文本内容变化时才重新测量
if (this.cachedTextSize === 0) {
const fullSize = measureTextSize({...});
this.cachedTextSize = fullSize.height;
}
}
2. 使用防抖处理状态切换
private debounceTimer: number | null = null;
@State isExpanded: boolean = false;
toggleExpand() {
// 清除之前的定时器
if (this.debounceTimer !== null) {
clearTimeout(this.debounceTimer);
}
// 设置新的定时器
this.debounceTimer = setTimeout(() => {
this.isExpanded = !this.isExpanded;
this.debounceTimer = null;
}, 200); // 200ms防抖延迟
}
注意事项与最佳实践
⚠️ 重要提示
- 版本兼容性:本文所有示例基于 HarmonyOS 5.0+和 API Level 12+,在低版本系统上可能需要调整
- 文本测量精度:不同字体和字号下,文本测量结果可能略有差异,需要进行适当的校准
- 性能考量:在列表中使用文本展开折叠时,需要特别注意性能优化,避免卡顿
- 无障碍支持:为展开/收起按钮添加适当的 aria 属性,确保良好的无障碍体验
最佳实践建议
- 组件化封装:将文本展开折叠功能封装成独立组件,提高代码复用性
- 合理设置默认行数:根据具体场景设置合适的默认显示行数,通常为 2-3 行
- 视觉反馈:点击展开/收起时提供适当的视觉反馈,如动画效果
- 自适应宽度:组件应支持自适应容器宽度,提高通用性
- 缓存优化:对文本测量结果进行缓存,避免重复计算
常见问题解答
Q: 如何处理不同屏幕宽度下的文本展开折叠?
A: 可以监听容器尺寸变化,动态调整文本容器宽度和重新计算折叠文本。
Q: 如何为展开/收起操作添加动画效果?
A: 可以使用 HarmonyOS 的 animateTo API 实现平滑的过渡动画:
animateTo(
{
duration: 300,
curve: Curve.EaseInOut,
},
() => {
// 更新文本内容和状态
}
);
Q: 如何处理富文本内容的展开折叠?
A: 对于富文本内容,需要使用 Web 组件或者自定义富文本解析器,并实现相应的高度计算逻辑。
Q: 文本展开折叠在性能敏感的场景中如何优化?
A: 可以考虑以下优化策略:
- 使用虚拟化列表
- 延迟加载不在视口内的内容
- 预计算并缓存折叠文本
- 避免在滚动过程中进行文本测量
总结
文本展开折叠是提升用户体验的重要交互模式,通过本文的学习,你应该已经掌握了在 HarmonyOS 应用中实现这一功能的完整解决方案:
- 纯文本展开折叠的核心实现原理
- 组件化封装的最佳实践
- 图文混排场景的处理方法
- 性能优化的关键技巧
- 实际应用场景的完整示例
💡 提示:实践是最好的学习方式,建议你动手尝试上述示例代码,并根据自己的应用需求进行扩展和优化!
祝你开发顺利!

浙公网安备 33010602011771号