HarmonyOS 文本展開摺疊組件實現指南
文本展開摺疊是現代應用中常見的交互模式,能夠在有限空間內展示長文本內容,提升用户體驗
關於本文
本文將詳細介紹在 HarmonyOS 5.0 中實現文本展開摺疊功能的完整解決方案,從純文本到富文本,幫助開發者輕鬆實現類似朋友圈、新聞列表等場景的文本交互效果。
官方文檔是個好東西!
官方文檔是個好東西!
官方文檔是個好東西!
重要的內容説三遍
參考文檔
- HarmonyOS 文本展開摺疊最佳實踐
- HarmonyOS Text組件文檔
- 華為開發者聯盟
環境要求
|
軟件/環境
|
版本要求
|
|
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 應用中實現這一功能的完整解決方案:
- 純文本展開摺疊的核心實現原理
- 組件化封裝的最佳實踐
- 圖文混排場景的處理方法
- 性能優化的關鍵技巧
- 實際應用場景的完整示例
💡 提示:實踐是最好的學習方式,建議你動手嘗試上述示例代碼,並根據自己的應用需求進行擴展和優化!
祝你開發順利!