利用Flex布局实现子项的展开与折叠
==================

#### 问题现象

如何让Flex布局可以折叠与展开？场景如下：Flex可以设置折叠时显示的行数。当子组件布局会超过设置的行数时，按照设置的行数展示，并且可以通过展开/折叠按钮控制Flex展示部分或全部子组件；当子组件布局行数未超过设置的行数时，按照子组件当前布局行数展示，并且不显示展开/折叠按钮。

#### 背景知识

*   [Flex](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-flex)是以弹性方式布局子组件的容器组件，提供更加有效的方式对容器内的子元素进行排列、对齐和分配剩余空间。
*   [aboutToAppear](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-custom-component-lifecycle#abouttoappear)生命周期函数在创建自定义组件的新实例后，在执行其build()函数之前执行。
*   [constraintSize](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-universal-attributes-size#constraintsize)方法设置约束尺寸，组件布局时，进行尺寸范围限制。
*   [measureText](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-uicontext-measureutils#measuretext12)用于计算指定文本的宽度。
*   [display.getDefaultDisplaySync](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-display#displaygetdefaultdisplaysync9)获取当前默认的display对象。[display](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-display#display)屏幕实例。描述display对象的属性和方法。
*   [clip](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-universal-attributes-sharp-clipping#clip18)可设置对子组件超出当前组件范围外的区域进行裁剪。设置为true时超出部分将不会显示。

#### 解决方案

*   方案一：设置Flex组件的高度控制显示的部分，并通过clip裁剪多余子项实现展开与折叠。
    
    1.  通过子组件高度、Flex的属性、以及折叠状态下展示的行数计算，获取折叠状态下Flex组件的高度；
    2.  通过屏幕宽度计算Flex组件宽，从而得到每一行Flex组件布局时的可用宽度，使用measureText方法计算文本宽度，获取子组件宽度；
    3.  根据子组件占用的行数进行判断是否展示展开/折叠按钮。当子组件折叠时按照1中得到的高度设置Flex高度，展开时高度设置为'auto'，实现展开/折叠。
    
    完整代码如下：
    
    import { display, LengthMetrics, MeasureUtils } from '@kit.ArkUI';
    
    
    @Component
    struct TextItem {
      @Prop text: string;
    
    
      build() {
        Text(this.text)
          .textAlign(TextAlign.Center)
          .maxLines(1)
          .height(30) _// 文本框高度30_
          .padding({ left: 8, right: 8 })
          .constraintSize({ minWidth: 60, maxWidth: '100%' }) _// 文本框的宽度限制_
          .backgroundColor('#f1f3f5')
          .borderRadius(5);
      }
    }
    
    
    @Entry
    @Component
    struct Page1 {
      uiContext: UIContext = this.getUIContext();
      measureUtils: MeasureUtils = this.uiContext.getMeasureUtils();
      textList: string\[\] =
        \['11111', '2222222', '3333333', '444', '555555', '6666', '7777777', '88888', '999999999999', '000'\]; _// Flex子组件文本内容_
      textItemHeight: number = 30; _// 子组件的高度_
      foldLine: number = 2; _// 默认折叠行数，不到折叠行数时按当前Flex行数，超过会自动折叠到行数_
     _ // Flex折叠状态高度，文本框高度 \* 2行 + Flex每行之间的space + Flex上下内间距 + Flex边框_
      defaultFlexHeight: number = this.textItemHeight \* this.foldLine + (this.foldLine - 1) \* 16 + 10 \* 2 + 2;
      lineWidth: number = 0; _// Flex组件每行可布局宽度_
      @State showButton: boolean = false; _// 是否展示折叠按钮，行数超过默认折叠行数展示按钮_
      @State flexState: string = '展开'; _// Flex组件状态，展开/折叠_
    
      getShowButtonValue() {
        let line = 1; _// 子组件占用行数_
        let countWidth = 0; _// 当前行子组件已占用宽度_
        this.textList.forEach((item) => {
          if (line <= this.foldLine) { _// 行数还未达到需要折叠的行，继续计算布局_
            let textWidth = this.uiContext.px2vp(this.measureUtils.measureText({
              textContent: item,
              fontSize: '16fp'
            }));
          _  // 子组件的宽度，最小60vp，最大占一整行_
            let itemWidth =
              (textWidth + 16) < 60 ? 60 : (textWidth + 16 > this.lineWidth ? this.lineWidth : (textWidth + 16));
            if (countWidth === 0) {
              countWidth = itemWidth; _// 第一个子组件Flex没有主轴方向子组件间距_
            } else {
              if (countWidth + itemWidth + 10 <= this.lineWidth) {
                countWidth = countWidth + itemWidth + 10;
              } else {
                line++; _// 换行_
                countWidth = itemWidth; _// 子组件放到新的一行_
              }
            }
          }
        });
     _   // 不超过折叠行数时不显示按钮，否则展示按钮_
        if (line <= this.foldLine) {
          this.showButton = false;
        } else {
          this.showButton = true;
        }
      }
    
    
      aboutToAppear(): void {
        let displayClass = display.getDefaultDisplaySync();
        this.lineWidth = this.uiContext.px2vp(displayClass.width) \* 0.9 - 2 - 2 \* 10;
        this.getShowButtonValue();
      }
    
    
      build() {
        Column() {
          Flex({
            wrap: FlexWrap.Wrap,
            space: {
              main: LengthMetrics.vp(10), _// 主轴方向间距10_
              cross: LengthMetrics.vp(16)
            }
          }) {
            ForEach(this.textList, (item: string) => {
              TextItem({ text: item });
            });
          }
          .width('90%')
          .clip(true)
          .padding(10) _// Flex布局上下左右内间距为10_
          .border({ color: '#888888', radius: 10, width: 1 })
          .height((this.flexState === '折叠' ? this.defaultFlexHeight : 'auto'));
    
    
          Button(this.flexState === '展开' ? '折叠' : '展开')
            .onClick(() => {
              if (this.flexState === '展开') {
                this.flexState = '折叠';
              } else {
                this.flexState = '展开';
              }
            }).margin({ top: 10 })
            .visibility(this.showButton ? Visibility.Visible : Visibility.Hidden);
        }
        .width('100%')
        .height('100%');
      }
    }
    
    效果图如下：
    
    ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/fe/v3/IcC0rj5US6CiZsifnadJjQ/zh-cn_image_0000002628753484.png?HW-CC-KV=V1&HW-CC-Date=20260709T133938Z&HW-CC-Expire=86400&HW-CC-Sign=54AC66A6530500B220F7845914A80DE3F721027BDE620E876ACBE5BB9D40A7CA "点击放大")
    

*   方案二：获取Flex展示的最后一个子组件索引，通过部分渲染的方式实现展开与折叠。
    
    1.  通过屏幕宽度计算Flex组件宽，从而得到每一行Flex组件布局时的可用宽度，使用measureText方法计算文本宽度，获取子组件宽度；
    2.  对折叠前几行进行计算，当子组件换行时超过折叠行数记录当前子组件的索引-1，获取折叠状态下最后一个子组件索引；
    3.  在Flex组件中渲染子组件时添加判断条件。当Flex状态为展开状态时，渲染全部子组件。切换为折叠状态时，渲染记录索引前的组件。
    
    完整代码如下：
    
    import { display, LengthMetrics, MeasureUtils } from '@kit.ArkUI';
    
    
    @Component
    struct TextItem {
      @Prop text: string;
    
    
      build() {
        Text(this.text)
          .textAlign(TextAlign.Center)
          .maxLines(1)
          .height(30) _// 文本框高度30_
          .padding({ left: 8, right: 8 })
          .constraintSize({ minWidth: 60, maxWidth: '100%' }) _// 文本框的宽度限制_
          .backgroundColor('#f1f3f5')
          .borderRadius(5);
      }
    }
    
    
    @Entry
    @Component
    struct Page2 {
      uiContext: UIContext = this.getUIContext();
      measureUtils: MeasureUtils = this.uiContext.getMeasureUtils();
      textList: string\[\] =
        \['11111', '2222222', '3333333', '444', '555555', '6666', '7777777', '88888', '999999999999', '000'\]; _// Flex子组件文本内容_
      textItemHeight: number = 30; _// 子组件的高度_
      foldLine: number = 2; _// 默认折叠行数，不到折叠行数时按当前Flex行数，超过会自动折叠到行数_
      lineWidth: number = 0; _// Flex组件每行可布局宽度_
      @State foldIndex: number = 0; _// 折叠时最后一个子组件的索引_
      @State showButton: boolean = false; _// 是否展示折叠按钮，行数超过默认折叠行数展示_
      @State flexState: string = '展开'; _// Flex组件状态，展开/折叠_
    
      getFoldIndex() {
        this.showButton = false;
        let line = 1; _// 占用行数_
        let countWidth = 0; _// 当前行子组件已占用宽度_
        this.textList.forEach((item, index) => {
          if (line <= this.foldLine) { _// 行数还未达到需要折叠的行，继续计算布局_
            let textWidth = this.uiContext.px2vp(this.measureUtils.measureText({
              textContent: item,
              fontSize: '16fp'
            }));
          _  // 子组件的宽度，最小60vp，最大占一整行_
            let itemWidth =
              (textWidth + 16) < 60 ? 60 : (textWidth + 16 > this.lineWidth ? this.lineWidth : (textWidth + 16));
            if (countWidth === 0) {
              countWidth = itemWidth; _// 第一个子组件Flex没有主轴方向子组件间距_
            } else {
              if (countWidth + itemWidth + 10 <= this.lineWidth) {
                countWidth = countWidth + itemWidth + 10;
              } else {
                line++; _// 换行_
                countWidth = itemWidth; _// 子组件放到新的一行_
                if (line > this.foldLine) {
                  this.foldIndex = index - 1; _// 换行刚好超过折叠行数时，获取当前子组件索引-1_
                  this.showButton = true;
                }
              }
            }
          }
        });
      }
    
    
      aboutToAppear(): void {
        let displayClass = display.getDefaultDisplaySync();
      _  // 计算Flex组件每行可布局宽度，屏幕宽 \* 0.9 - Flex边框2 - 左右内边距各10_
        this.lineWidth = this.uiContext.px2vp(displayClass.width) \* 0.9 - 2 - 2 \* 10;
        this.getFoldIndex(); _// 获取Flex折叠时展示的最后一个子组件的索引值_
      }
    
    
      build() {
        Column() {
          Flex({
            wrap: FlexWrap.Wrap,
            space: {
              main: LengthMetrics.vp(10), _// 主轴方向间距10_
              cross: LengthMetrics.vp(16)
            }
          }) {
            ForEach(this.textList, (item: string, index: number) => {
              if (this.flexState === '展开' || index <= this.foldIndex) { _// 展开状态全展示，折叠状态展示部分_
                TextItem({ text: item });
              }
            });
          }
          .width('90%')
          .clip(true)
          .padding(10) _// Flex布局上下左右内间距为10_
          .border({ color: '#888888', radius: 10, width: 1 });
    
    
          Button(this.flexState === '展开' ? '折叠' : '展开')
            .onClick(() => {
              if (this.flexState === '展开') {
                this.flexState = '折叠';
              } else {
                this.flexState = '展开';
              }
            }).margin({ top: 10 })
            .visibility(this.showButton ? Visibility.Visible : Visibility.Hidden);
        }
        .width('100%')
        .height('100%');
      }
    }
    
    效果图如下：
    
    ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/c4/v3/3TV2EJ5VSIu4kZvvH4Qlqg/zh-cn_image_0000002658952797.png?HW-CC-KV=V1&HW-CC-Date=20260709T133938Z&HW-CC-Expire=86400&HW-CC-Sign=FEF4B09E33D5404DFEE209E89A0EA3CC606F003DAF3ECD98599978CB1779AF5E "点击放大")