HarmonyOS 文本展開摺疊組件實現指南

文本展開摺疊是現代應用中常見的交互模式,能夠在有限空間內展示長文本內容,提升用户體驗

關於本文

本文將詳細介紹在 HarmonyOS 5.0 中實現文本展開摺疊功能的完整解決方案,從純文本到富文本,幫助開發者輕鬆實現類似朋友圈、新聞列表等場景的文本交互效果。

官方文檔是個好東西!

官方文檔是個好東西!

官方文檔是個好東西!

重要的內容説三遍

參考文檔

  • HarmonyOS 文本展開摺疊最佳實踐
  • HarmonyOS Text組件文檔
  • 華為開發者聯盟

環境要求

軟件/環境

版本要求

HarmonyOS

5.0+

API Level

12+

DevEco Studio

4.0+

文本展開摺疊基礎概念

什麼是文本展開摺疊?

文本展開摺疊功能允許將長文本內容默認只顯示部分(通常是幾行),用户可以通過點擊操作展開查看完整內容,再次點擊則收起恢復到默認顯示狀態。

主要應用場景:

  • 朋友圈或社交動態列表
  • 新聞文章摘要展示
  • 商品詳情描述
  • 評論區內容展示

實現原理

文本展開摺疊功能的核心實現原理包括以下幾個步驟:

  1. 文本測量:計算完整文本和限制行數的高度
  2. 需求判斷:對比兩種高度,確定是否需要摺疊處理
  3. 文本截斷:通過二分查找算法找到最佳的摺疊點
  4. 狀態管理:管理展開/摺疊的狀態切換

純文本展開摺疊實現

基礎常量定義

// 完整的示例文本
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 屬性,確保良好的無障礙體驗

最佳實踐建議

  1. 組件化封裝:將文本展開摺疊功能封裝成獨立組件,提高代碼複用性
  2. 合理設置默認行數:根據具體場景設置合適的默認顯示行數,通常為 2-3 行
  3. 視覺反饋:點擊展開/收起時提供適當的視覺反饋,如動畫效果
  4. 自適應寬度:組件應支持自適應容器寬度,提高通用性
  5. 緩存優化:對文本測量結果進行緩存,避免重複計算

常見問題解答

Q: 如何處理不同屏幕寬度下的文本展開摺疊? A: 可以監聽容器尺寸變化,動態調整文本容器寬度和重新計算摺疊文本。

Q: 如何為展開/收起操作添加動畫效果? A: 可以使用 HarmonyOS 的 animateTo API 實現平滑的過渡動畫:

animateTo(
  {
    duration: 300,
    curve: Curve.EaseInOut,
  },
  () => {
    // 更新文本內容和狀態
  }
);

Q: 如何處理富文本內容的展開摺疊? A: 對於富文本內容,需要使用 Web 組件或者自定義富文本解析器,並實現相應的高度計算邏輯。

Q: 文本展開摺疊在性能敏感的場景中如何優化? A: 可以考慮以下優化策略:

  • 使用虛擬化列表
  • 延遲加載不在視口內的內容
  • 預計算並緩存摺疊文本
  • 避免在滾動過程中進行文本測量

總結

文本展開摺疊是提升用户體驗的重要交互模式,通過本文的學習,你應該已經掌握了在 HarmonyOS 應用中實現這一功能的完整解決方案:

  • 純文本展開摺疊的核心實現原理
  • 組件化封裝的最佳實踐
  • 圖文混排場景的處理方法
  • 性能優化的關鍵技巧
  • 實際應用場景的完整示例

💡 提示:實踐是最好的學習方式,建議你動手嘗試上述示例代碼,並根據自己的應用需求進行擴展和優化!

祝你開發順利!