
前言
随着HarmonyOS 6的正式发布,声明式UI开发框架------ArkUI,为开发者提供了更加高效、便捷的应用开发体验。本文将深入探讨如何在DevEco Studio开发环境中,基于ArkUI框架实现一个文本展开折叠组件,带您走进HarmonyOS应用开发的实际应用场景。
文本展开折叠组件实现
实现效果

技术方案
我们将使用ArkUI的measureTextSize
接口结合二分查找算法来实现精确的文本截断和展开功能。

实现原理
- 文本测量 :使用
measureTextSize
方法测量完整文本和限制行数的高度。 - 判断折叠需求:对比两种高度,确定是否需要折叠处理。
- 二分查找算法: 通过动态调整文本截断位置,快速找到最佳的折叠点。
- 状态管理 :通过
@State
装饰器管理展开/折叠状态。

完整代码实现
基础常量定义
typescript
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 = "收起"; // 收起按钮文本
主要组件实现
检查文本是否需要折叠功能
组件加载时,首先需要判断给定的文本是否真的需要折叠功能。核心原理是比较文本在无限制高度和限制行数两种情况下的实际渲染高度。
步骤如下:
- 测量文本在无行数限制下的完整高度。
- 测量文本在指定最大行数限制下的高度。
- 对比两个高度值,判断是否需要折叠功能。
- 根据判断结果初始化组件状态。
arkts
private checkIfNeedCollapse(): void {
// 测量完整文本的高度
let expandSize: SizeOptions = this.getUIContext()
.getMeasureUtils()
.measureTextSize({
textContent: this.title,
constraintWidth: TEXT_WIDTH,
fontSize: 16
});
// 测量限制行数时的文本高度
let collapseSize: SizeOptions = this.getUIContext()
.getMeasureUtils()
.measureTextSize({
textContent: this.title,
constraintWidth: TEXT_WIDTH,
fontSize: 16,
maxLines: COLLAPSE_LINES
});
// 如果两个高度不同,说明文本超出了限制行数,需要折叠功能
if ((expandSize.height as number) !== (collapseSize.height as number)) {
this.needCollapse = true;
this.calculateCollapseText();
} else {
this.needCollapse = false;
this.displayText = this.title;
}
}
计算折叠状态下的显示文本
这里我们需要考虑一个问题,为什么选择二分查找而不是线性搜索?
- 线性搜索:每次逐个字符进行比较,时间复杂度为O(n),性能随着文本长度的增加而线性增长。
- 二分查找:每次通过重点将搜索范围减半,时间复杂度O(log n),随着文本长度增加,性能提升显著。
二分查找实现策略:
- 边界设定:设置搜索区间的左右边界,初始时左边界为0,右边界为文本的总长度。
- 中点计算:计算当前搜索区间的中点作为测试位置,进行高度测量。
- 高度测试:构造"截断文本+省略号+展开按钮"进行高度测量。
- 区间调整:根据测量结果调整搜索区间。
- 收敛条件:当搜索区间足够小(例如左边界与右边界相差不到一定阙值)时停止搜索。
arkts
private calculateCollapseText(): void {
let leftCursor: number = 0;
let rightCursor: number = this.title.length;
let cursor: number = Math.floor(this.title.length / 2);
let tempTitle: string = "";
// 首先测量限制行数的基准高度
const collapseSize: SizeOptions = this.getUIContext()
.getMeasureUtils()
.measureTextSize({
textContent: this.title,
constraintWidth: TEXT_WIDTH,
fontSize: 16,
maxLines: COLLAPSE_LINES
});
// 二分查找最佳截断位置
while (Math.abs(rightCursor - leftCursor) > 1) {
// 构造测试文本:截断文本 + 省略号 + 展开按钮
tempTitle = this.title.substring(0, cursor) + ELLIPSIS + EXPAND_STR;
// 测量当前测试文本的高度
const currentTextSize: SizeOptions = this.getUIContext()
.getMeasureUtils()
.measureTextSize({
textContent: tempTitle,
fontSize: 16,
constraintWidth: TEXT_WIDTH
});
// 如果当前文本超出了限制高度,需要继续缩短
if ((currentTextSize.height as number) > (collapseSize.height as number)) {
rightCursor = cursor;
cursor = leftCursor + Math.floor((cursor - leftCursor) / 2);
} else {
// 否则可以尝试更长的文本
leftCursor = cursor;
cursor += Math.floor((rightCursor - cursor) / 2);
}
}
// 设置最终的折叠显示文本
this.displayText = this.title.substring(0, leftCursor) + ELLIPSIS;
}
切换展开、折叠状态
使用ArkUI的@State装饰器实现响应式状态管理:
<font style="color:rgb(35, 36, 37);">isExpanded</font>
:当前展开状态<font style="color:rgb(35, 36, 37);">displayText</font>
:当前显示的文本内容<font style="color:rgb(35, 36, 37);">needCollapse</font>
:是否需要折叠功能
展开和折叠的切换需要考虑:
- 状态标记的更新。
- 显示文本的重新计算。
- UI的自动更新。
arkts
private toggleExpand(): void {
this.isExpanded = !this.isExpanded;
if (this.isExpanded) {
// 展开状态:显示完整文本
this.displayText = this.title;
} else {
// 折叠状态:重新计算折叠文本
this.calculateCollapseText();
}
}

工具函数扩展
为了增强组件的可复用性,我们可以将核心逻辑抽取成工具类:
typescript
/**
* 文本处理工具类
* 提供文本测量和截断相关的工具方法
*/
export class TextUtils {
/**
* 检查文本是否需要折叠
* @param context UI上下文
* @param text 原始文本
* @param width 容器宽度
* @param fontSize 字体大小
* @param maxLines 最大行数
* @returns 是否需要折叠
*/
static checkNeedCollapse(
context: UIContext,
text: string,
width: number,
fontSize: number,
maxLines: number
): boolean {
const expandSize = context.getMeasureUtils().measureTextSize({
textContent: text,
constraintWidth: width,
fontSize: fontSize
});
const collapseSize = context.getMeasureUtils().measureTextSize({
textContent: text,
constraintWidth: width,
fontSize: fontSize,
maxLines: maxLines
});
return (expandSize.height as number) !== (collapseSize.height as number);
}
/**
* 计算最佳文本截断位置
* @param context UI上下文
* @param text 原始文本
* @param width 容器宽度
* @param fontSize 字体大小
* @param maxLines 最大行数
* @param suffix 后缀文本(如"...展开")
* @returns 截断后的文本
*/
static calculateTruncateText(
context: UIContext,
text: string,
width: number,
fontSize: number,
maxLines: number,
suffix: string
): string {
const collapseSize = context.getMeasureUtils().measureTextSize({
textContent: text,
constraintWidth: width,
fontSize: fontSize,
maxLines: maxLines
});
let left = 0;
let right = text.length;
let result = text;
while (left < right) {
const mid = Math.floor((left + right + 1) / 2);
const testText = text.substring(0, mid) + suffix;
const testSize = context.getMeasureUtils().measureTextSize({
textContent: testText,
fontSize: fontSize,
constraintWidth: width
});
if ((testSize.height as number) <= (collapseSize.height as number)) {
left = mid;
result = text.substring(0, mid);
} else {
right = mid - 1;
}
}
return result;
}
}
完整代码
arkts
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 = "收起"; // 收起按钮文本
@Entry
@Component
struct TextExpandComponent {
@State private title: string = FULL_TEXT; // 完整文本内容
@State private isExpanded: boolean = false; // 是否已展开
@State private displayText: string = ""; // 当前显示的文本
@State private needCollapse: boolean = false; // 是否需要折叠功能
/**
* 组件初始化时执行
* 判断文本是否需要折叠,并计算折叠状态下的显示内容
*/
aboutToAppear(): void {
this.checkIfNeedCollapse();
}
/**
* 检查文本是否需要折叠功能
* 通过对比完整文本和限制行数文本的高度来判断
*/
private checkIfNeedCollapse(): void {
// 测量完整文本的高度
let expandSize: SizeOptions = this.getUIContext()
.getMeasureUtils()
.measureTextSize({
textContent: this.title,
constraintWidth: TEXT_WIDTH,
fontSize: 16
});
// 测量限制行数时的文本高度
let collapseSize: SizeOptions = this.getUIContext()
.getMeasureUtils()
.measureTextSize({
textContent: this.title,
constraintWidth: TEXT_WIDTH,
fontSize: 16,
maxLines: COLLAPSE_LINES
});
// 如果两个高度不同,说明文本超出了限制行数,需要折叠功能
if ((expandSize.height as number) !== (collapseSize.height as number)) {
this.needCollapse = true;
this.calculateCollapseText();
} else {
this.needCollapse = false;
this.displayText = this.title;
}
}
/**
* 计算折叠状态下的显示文本
* 使用二分查找算法找到最佳的文本截断位置
*/
private calculateCollapseText(): void {
let leftCursor: number = 0;
let rightCursor: number = this.title.length;
let cursor: number = Math.floor(this.title.length / 2);
let tempTitle: string = "";
// 首先测量限制行数的基准高度
const collapseSize: SizeOptions = this.getUIContext()
.getMeasureUtils()
.measureTextSize({
textContent: this.title,
constraintWidth: TEXT_WIDTH,
fontSize: 16,
maxLines: COLLAPSE_LINES
});
// 二分查找最佳截断位置
while (Math.abs(rightCursor - leftCursor) > 1) {
// 构造测试文本:截断文本 + 省略号 + 展开按钮
tempTitle = this.title.substring(0, cursor) + ELLIPSIS + EXPAND_STR;
// 测量当前测试文本的高度
const currentTextSize: SizeOptions = this.getUIContext()
.getMeasureUtils()
.measureTextSize({
textContent: tempTitle,
fontSize: 16,
constraintWidth: TEXT_WIDTH
});
// 如果当前文本超出了限制高度,需要继续缩短
if ((currentTextSize.height as number) > (collapseSize.height as number)) {
rightCursor = cursor;
cursor = leftCursor + Math.floor((cursor - leftCursor) / 2);
} else {
// 否则可以尝试更长的文本
leftCursor = cursor;
cursor += Math.floor((rightCursor - cursor) / 2);
}
}
// 设置最终的折叠显示文本
this.displayText = this.title.substring(0, leftCursor) + ELLIPSIS;
}
/**
* 切换展开/折叠状态
*/
private toggleExpand(): void {
this.isExpanded = !this.isExpanded;
if (this.isExpanded) {
// 展开状态:显示完整文本
this.displayText = this.title;
} else {
// 折叠状态:重新计算折叠文本
this.calculateCollapseText();
}
}
build() {
Column({ space: 20 }) {
// 标题
Text("文本展开折叠组件演示")
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 10 })
// 文本容器
Column() {
// 主要文本内容显示区域
Text() {
// 使用Span组件显示文本内容
Span(this.displayText)
.fontSize(16)
.fontColor(Color.Black)
// 如果需要折叠功能,显示操作按钮
if (this.needCollapse) {
Span(this.isExpanded ? COLLAPSE_STR : EXPAND_STR)
.fontSize(16)
.fontColor('#007DFF') // 主题蓝色
.onClick(() => {
this.toggleExpand();
})
}
}
.width(TEXT_WIDTH)
.textAlign(TextAlign.Start)
.lineHeight(24)
}
.width('100%')
.padding(16)
.backgroundColor('#F5F5F5')
.borderRadius(8)
// 状态指示器
Row() {
Text(`当前状态: ${this.isExpanded ? '已展开' : '已折叠'}`)
.fontSize(14)
.fontColor('#666666')
if (this.needCollapse) {
Text(`需要折叠: 是`)
.fontSize(14)
.fontColor('#666666')
.margin({ left: 20 })
}
}
.width('100%')
.justifyContent(FlexAlign.Start)
}
.width('100%')
.height('100%')
.padding(20)
.backgroundColor(Color.White)
}
}
/**
* 文本处理工具类
* 提供文本测量和截断相关的工具方法
*/
export class TextUtils {
/**
* 检查文本是否需要折叠
* @param context UI上下文
* @param text 原始文本
* @param width 容器宽度
* @param fontSize 字体大小
* @param maxLines 最大行数
* @returns 是否需要折叠
*/
static checkNeedCollapse(
context: UIContext,
text: string,
width: number,
fontSize: number,
maxLines: number
): boolean {
const expandSize = context.getMeasureUtils().measureTextSize({
textContent: text,
constraintWidth: width,
fontSize: fontSize
});
const collapseSize = context.getMeasureUtils().measureTextSize({
textContent: text,
constraintWidth: width,
fontSize: fontSize,
maxLines: maxLines
});
return (expandSize.height as number) !== (collapseSize.height as number);
}
/**
* 计算最佳文本截断位置
* @param context UI上下文
* @param text 原始文本
* @param width 容器宽度
* @param fontSize 字体大小
* @param maxLines 最大行数
* @param suffix 后缀文本(如"...展开")
* @returns 截断后的文本
*/
static calculateTruncateText(
context: UIContext,
text: string,
width: number,
fontSize: number,
maxLines: number,
suffix: string
): string {
const collapseSize = context.getMeasureUtils().measureTextSize({
textContent: text,
constraintWidth: width,
fontSize: fontSize,
maxLines: maxLines
});
let left = 0;
let right = text.length;
let result = text;
while (left < right) {
const mid = Math.floor((left + right + 1) / 2);
const testText = text.substring(0, mid) + suffix;
const testSize = context.getMeasureUtils().measureTextSize({
textContent: testText,
fontSize: fontSize,
constraintWidth: width
});
if ((testSize.height as number) <= (collapseSize.height as number)) {
left = mid;
result = text.substring(0, mid);
} else {
right = mid - 1;
}
}
return result;
}
}
技术要点解析
measureTextSize接口
measureTextSize
是ArkUI提供的文本测量接口,主要参数包括:
textContent
: 要测量的文本内容constraintWidth
: 约束宽度fontSize
: 字体大小maxLines
: 最大行数(可选)wordBreak
: 换行规则(可选)
二分查找算法的优势
相比于逐字符尝试的方法,二分查找具有以下优势:
- 时间复杂度: O(log n) vs O(n)
- 性能表现: 大幅减少文本测量次数
- 用户体验: 响应更快,无明显卡顿
状态管理机制
使用@State
装饰器的核心原理:
typescript
@State private isExpanded: boolean = false;
- 状态变化自动触发UI更新
- 声明式编程范式的体现
- 减少手动DOM操作
可添加扩展功能
动画效果
添加展开/折叠的动画过渡。
typescript
@State private animationHeight: number = 0;
private toggleWithAnimation(): void {
animateTo({
duration: 300,
curve: Curve.EaseInOut
}, () => {
this.isExpanded = !this.isExpanded;
this.updateDisplayText();
});
}
自定义样式配置
typescript
interface TextExpandConfig {
maxLines: number;
expandText: string;
collapseText: string;
textColor: ResourceColor;
actionColor: ResourceColor;
fontSize: number;
lineHeight: number;
}
总结
HarmonyOS 6和ArkUI框架为开发者提供了强大且灵活的开发能力。通过充分利用框架特性与优化算法,我们可以构建出具有卓越用户体验和高性能的移动应用组件。希望本文的深度解析能够帮助您深入理解HarmonyOS应用开发,并在实际项目中充分发挥技术优势,创造更大的价值。