基于Flex布局的常见换行场景
===============

#### 问题现象

Flex布局中子元素的换行是布局开发中常见的问题，针对不同的需求，可采用不同的换行策略。本文将结合实际场景，由浅入深地介绍几种常见的换行场景及其解决方案。

#### \[h2\]场景一：基础网格布局

如何实现子元素宽度不固定且能自动换行的基础网格布局？

| 
单行预期效果

 | 

多行预期效果

 |
| :-- | :-- |
| 

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/f0/v3/KseskbljTsir0764wSMpxQ/zh-cn_image_0000002628753476.png?HW-CC-KV=V1&HW-CC-Date=20260709T134112Z&HW-CC-Expire=86400&HW-CC-Sign=A72CE84B1EBF72664FE736B3217A3AF8D8603C679ADDEDAF40BDC20A6CAB26DE "点击放大")

 | 

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/d5/v3/dhUlb2ftRYSDkAqhMjTH1w/zh-cn_image_0000002658952789.png?HW-CC-KV=V1&HW-CC-Date=20260709T134112Z&HW-CC-Expire=86400&HW-CC-Sign=06636453913BD3CC25FEBB78D045742B4BA8C7995F0A6F520D68AFCE7D57318F "点击放大")

 |

#### \[h2\]场景二：智能自适应布局

在实际开发中，我们常面对更复杂的布局需求，往往需要根据内容或容器状态动态决定是否换行。下面列举常见的两种情况：

*   单行自适应问题：如何实现子元素少时Flex组件宽度自适应（不占满整行）？
    
    如图所示，当子元素较多时，Flex组件同时设置FlexWrap.Wrap和width('auto')后，换行场景可以正常实现，但是子元素少时，Flex宽度会默认占用一整行，不会根据子元素内容自适应宽度。该如何解决？
    
    ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/57/v3/h1qO3lxRTYir1lFFuOzGtA/zh-cn_image_0000002658832827.png?HW-CC-KV=V1&HW-CC-Date=20260709T134112Z&HW-CC-Expire=86400&HW-CC-Sign=C829186136DB0AE87D705BE5110C2D9DC79C917DADE49AAD9AD7DDCF1F3E10E4 "点击放大")
    
*   阈值控制换行：如何基于剩余空间计算实现精准的换行策略？
    
    如图所示，当输入框可用空间充足时，让输入框与标签同行并自适应占满剩余宽度；当可用空间不足时，自动将输入框换到下一行并占满整行宽度。
    

#### 背景知识

*   [Flex](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-flex)是以弹性方式布局子组件的容器组件，能够高效地排列、对齐子元素并分配剩余空间。
*   [FlexOptions](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-flex#flexoptions对象说明)对象用于设置子组件的排列对齐方式，主轴的方向[FlexDirection](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-appendix-enums#flexdirection)、换行方式[FlexWrap](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-appendix-enums#flexwrap)。主轴长度可设置为[width](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-universal-attributes-size#width)('auto')使Flex自适应子组件布局。
*   [MeasureUtils](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-uicontext-measureutils)提供文本宽度、高度等相关计算。使用[UIContext](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-uicontext-uicontext)中的[getMeasureUtils](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-uicontext-uicontext#getmeasureutils12)方法能够获取MeasureUtils实例。此实例中[measureText](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-uicontext-measureutils#measuretext12)方法可以计算单行文本显示时的宽度。

#### 解决方案

| 
使用场景

 | 

场景特点

 | 

推荐方案

 | 

技术要点

 |
| :-- | :-- | :-- | :-- |
| 

基础网格布局

 | 

简单列表，子元素宽度不固定，需要自行换行

 | 

基础布局

 | 

FlexWrap: Wrap

 |
| 

单行自适应问题

 | 

需要单行时自适应，多行时换行

 | 

精确控制

 | 

动态设置FlexWrap

 |
| 

阈值控制换行

 | 

特定元素需要智能换行

 | 

智能决策

 | 

剩余空间计算 + 阈值判断

 |

*   基础网格布局。
    
    使用弹性布局(Flex)可以实现子元素不固定的网格布局，只需按实际需求设置以下参数即可：
    
    *   FlexDirection.Row（默认值）：主轴为水平方向，子元素从起始端沿着水平方向开始排布。
    *   FlexWrap.Wrap：允许换行，每一行子元素按照主轴方向排列。
    
    代码示例如下：
    
    @Entry
    @Component
    struct FlexPage {
     _ // 模拟图标数据（宽度不一致）_
      @State iconLabels: string\[\] = \[\];
      private xCount: number = 0; _// 计数器_
    
      build() {
        Column() {
         _ // 核心Flex容器（自动换行）_
          Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap }) {
            ForEach(this.iconLabels, (label: string) => {
              Text(label)
                .height(40)
                .padding(8)
                .fontSize(12)
                .textAlign(TextAlign.Center)
                .backgroundColor('#330A59F7')
                .borderRadius(10)
                .margin({
                  left: '12vp',
                  bottom: '10vp'
                });
            });
          }
          .width('100%')
          .padding(10);
    
          Button('添加文本')
            .margin(7)
            .onClick(() => {
              const newItems: string\[\] = \[\];
              this.xCount++;
              const xString = 'X'.repeat(this.xCount);
              const newText = \`文本${xString}\`;
              newItems.push(newText);
              this.iconLabels = \[...newItems, ...this.iconLabels\];
            });
        }
        .width('100%')
        .height('100%');
      }
    }
    
    运行效果图如下：
    
    ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/9a/v3/b7zQ3h-WSySsX-2klWTVIw/zh-cn_image_0000002628593586.png?HW-CC-KV=V1&HW-CC-Date=20260709T134112Z&HW-CC-Expire=86400&HW-CC-Sign=BA7FDBB06D2D859C3B1726570BE8288D71437AEA4F5C816FC262AC04406347DA "点击放大")
    
*   单行自适应问题。
    
    当Flex子组件较多，需要使用换行参数（将wrap设置为FlexWrap.Wrap）时，主轴长度auto的自适应布局会失效，默认撑满父容器。所以要解决问题可以控制Flex组件的换行规则：当内容不超过一行时设置为FlexWrap.NoWrap，超过一行设置为FlexWrap.Wrap。
    
    *   步骤1：根据当前布局计算出Flex组件中一行可用的最大宽度。通过measureText得到文本占用的宽度，从而计算文本框占用的宽度。
    *   步骤2：当全部文本框占用的宽度加起来未超过一行就将Flex设置为FlexWrap.NoWrap；如果前几个文本框累计宽度超过一行，则无需计算后续文本框，直接将Flex设置为FlexWrap.Wrap。
    
    代码示例如下：
    
    import { MeasureUtils, UIContext, window } from '@kit.ArkUI';
    
    @Entry
    @Component
    struct FlexPageSample {
      @State textList: string\[\] = \['文本XXX', '文本XXXXX', '文本XXXXXX', '文本XXXXXXX', '文本XX', '文本X'\];
      @State isWarp: boolean = false;
      flexWidth: number = 0;
      uiContext: UIContext = this.getUIContext();
      measureUtils: MeasureUtils = this.getUIContext().getMeasureUtils();
    
      aboutToAppear(): void {
        window.getLastWindow(this.getUIContext().getHostContext(), (err, data) => {
          data.setWindowLayoutFullScreen(true); _// 设置沉浸式布局_
          let properties = data.getWindowProperties(); _// 获取当前窗口的属性_
          let windowWidth = properties.windowRect.width; _// 获取当前窗口宽度_
          this.flexWidth = windowWidth - this.uiContext.vp2px(20 \* 2); _// 计算Flex组件的最大宽度，单位px_
          this.setFlexWrap(this.textList);
        });
      }
    
     _ // 设置是否换行_
      setFlexWrap(textList: string\[\]) {
        this.isWarp = false; _// 默认不换行_
        let lineWidth = this.flexWidth - this.uiContext.px2vp(6 \* 2); _// Flex组件一行的最大宽度，单位px_
        let countWidth = 0; _// 文本框占用的的宽度_
        textList.forEach((item) => {
          if (this.isWarp === false) {
          _  // 计算文本的宽度_
            let textWidth = this.measureUtils.measureText({
              textContent: item,
              fontSize: 12 _// 文本字体大小_
            });
          _  // 累计文本框占用宽度，文本宽度+2\*文本框左右内边距+2\*文本框左右外边距_
            countWidth = countWidth + textWidth + this.uiContext.vp2px(2 \* 12 + 2 \* 3);
           _ // 超过每行最大宽度，设置为换行_
            if (countWidth >= lineWidth) {
              this.isWarp = true;
            }
          }
        });
      }
    
      build() {
        Column({ space: 10 }) {
          Column({ space: 10 }) {
            Text(\`计算首行能否放下全部文本，是否需要换行\`);
            Flex({
              justifyContent: FlexAlign.Start,
              wrap: this.isWarp ? FlexWrap.Wrap : FlexWrap.NoWrap, _// 根据状态变量设置是否换行_
              direction: FlexDirection.Row
            }) {
              ForEach(this.textList, (item: string) => {
                Text(item)
                  .fontSize(12)
                  .maxLines(1)
                  .backgroundColor('#330A59F7')
                  .borderRadius(4)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                  .ellipsisMode(EllipsisMode.END)
                  .margin(3) _// 文本框外边距3_
                  .padding({ left: 12, right: 12 }) _// 文本框左右内边距12_
                  .height(32);
              });
            }
            .padding(6) _// Flex组件内边距6_
            .backgroundColor('#ffffff')
            .width('auto');
          }.width('100%')
          .alignItems(HorizontalAlign.Start);
    
          Button('更换数据1')
            .onClick(() => {
              this.textList = \['文本XXXX', '文本X'\];
              this.setFlexWrap(this.textList);
            });
          Button('更换数据2')
            .onClick(() => {
              this.textList = \['文本XXXX', '文本X', '文本XXXXXXXXX', '文本XXXXXX', '文本XXX', '文本XX', '文本X', '文本XXX'\];
              this.setFlexWrap(this.textList);
            });
        }
        .padding({
          top: 50,
          left: 20,
          right: 20,
          bottom: 20
        }) _// 左右内边距20_
        .height('100%')
        .width('100%')
        .backgroundColor('#f1f3f5');
      }
    }
    
    运行效果图如下：
    
    ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/5f/v3/qUmTSVDkSESXleC12a1FIQ/zh-cn_image_0000002628753478.png?HW-CC-KV=V1&HW-CC-Date=20260709T134112Z&HW-CC-Expire=86400&HW-CC-Sign=59EC6CAD1C21B0911ED2991928919DF0AFFA2AAFE76E4B76C304102AD41A894F "点击放大")
    

*   阈值控制换行。
    
    在Flex布局中，若前面的子元素（如文本框）与最后一个子元素（输入框）的类型不一致，最后一个子元素在特定条件下需要换行。为了实现智能的响应式布局，可以通过精确计算前方所有元素的实际占用宽度，判断容器的剩余空间是否足以容纳该输入框，从而决定其是否换行。
    
    *   步骤1：计算标签占用宽度和剩余空间：
    *   步骤2：根据剩余空间决定输入框的布局方式，以150vp为阈值：
        *   剩余空间大于150vp：输入框在当前行显示，宽度等于剩余空间。
        *   剩余空间小于等于150vp：输入框换行到下一行，宽度占满整行。
    
    代码示例如下：
    
    import { LengthMetrics, MeasureUtils, UIContext, window } from '@kit.ArkUI';
    
    @Component
    export struct InputTag {
      @Link value: string\[\];
      @State inputVal: string = '';
      _// input组件的宽度_
      @Link remainingSpaceVp: number;
     _ // 父组件传递的计算函数_
      private onTagsChanged?: () => void;
      @Link lineWidth: number; _// 改为@Link修饰符_
      flexWidth: number = 0;
    
      aboutToAppear() {
       _ // 初始计算_
        if (this.onTagsChanged) {
          this.onTagsChanged();
        }
      }
    
      build() {
        Column() {
          Text('标签输入框').margin(7);
          Row() {
            Flex({
              wrap: FlexWrap.Wrap,
              space: { main: LengthMetrics.vp(6), cross: LengthMetrics.vp(6) }
            }) {
            _  // 渲染已有tag_
              ForEach(this.value, (tag: string) => {
                Text(tag)
                  .fontSize(12)
                  .height(24)
                  .backgroundColor('rgba(255, 255, 255, 1)')
                  .borderRadius(4)
                  .padding({ left: 8, right: 8 });
              });
             _ // 根据剩余空间决定输入框的布局方式_
              TextInput({ text: this.inputVal })
             _ // 剩余空间大于lineWidth占用剩余空间，小于lineWidth占用一整行_
                .width((this.remainingSpaceVp >= this.lineWidth) ? this.remainingSpaceVp - 30 : '100%')
                .height(24)
                .padding({ left: 5, right: 0 })
                .backgroundColor('rgba(255, 255, 255, 1)')
                .caretColor(0.5);
            }
            .width('100%');
          }
          .width('100%')
          .constraintSize({ minHeight: 32 })
          .borderRadius(24)
          .backgroundColor('rgba(0, 0, 0, 0.05)')
          .padding(16)
          .clip(true);
        };
      }
    }
    
    @Entry
    @Component
    struct InputTagPage {
      _// 容器宽度相关_
      private containerWidth: number = 0;
      flexWidth: number = 0;
      private uiContext: UIContext = this.getUIContext();
      private measureUtils: MeasureUtils = this.uiContext.getMeasureUtils();
      @State value: string\[\] = \['文本'\];
      @State remainingSpaceVp: number = 0;
      @State lineWidth: number = 150; _// 添加状态变量_
      @State customLineWidth: string = '150'; _// 用于输入框的状态_
    
      aboutToAppear(): void {
      _  // 获取窗口宽度_
        window.getLastWindow(this.getUIContext().getHostContext(), (err, data) => {
          data.setWindowLayoutFullScreen(true);
          let properties = data.getWindowProperties();
          let windowWidth = properties.windowRect.width;
         _ // 计算Flex容器的实际宽度_
          this.flexWidth = windowWidth - this.uiContext.vp2px(14 \* 2) - this.uiContext.vp2px(5 \* 2);
          this.containerWidth = this.flexWidth;
        _  // 初始计算_
          this.calculateLayout();
        });
      }
    
      aboutToUpdate() {
        this.calculateLayout();
      }
    
      _// 动态计算标签占用宽度和剩余空间_
      calculateLayout() {
        const tagSpacing = 6; _// tag之间的间距_
        const tagPadding = 8 \* 2; _// tag左右padding各8vp_
        let currentLineWidth = 0;
       _ // 计算当前行已使用的宽度_
        for (let i = 0; i < this.value.length; i++) {
          const tag = this.value\[i\];
         _ // 测量文本宽度_
          const textWidth = this.measureUtils.measureText({
            textContent: tag,
            fontSize: 12
          });
        _  // 计算tag总宽度：文本宽度+padding+可能的间距_
          const tagTotalWidth = textWidth + this.uiContext.vp2px(tagPadding);
          if (currentLineWidth === 0) {
           _ // 第一个元素，不加间距_
            currentLineWidth = tagTotalWidth;
          } else {
           _ // 检查当前行是否能放下这个tag_
            if (currentLineWidth + this.uiContext.vp2px(tagSpacing) + tagTotalWidth <= this.containerWidth) {
             _ // 能放下，添加到当前行_
              currentLineWidth += this.uiContext.vp2px(tagSpacing) + tagTotalWidth;
            } else {
            _  // 放不下，开始新的一行_
              currentLineWidth = tagTotalWidth;
              this.containerWidth = this.flexWidth;
            }
          }
        }
       _ // 计算剩余空间（转换为vp单位）_
        this.remainingSpaceVp = this.uiContext.px2vp(this.containerWidth - currentLineWidth);
      }
    
     _ // 更新lineWidth的函数_
      updateLineWidth() {
        const width = parseInt(this.customLineWidth);
        if (!isNaN(width) && width > 0) {
          this.lineWidth = width;
          this.calculateLayout();
        }
      }
    
      build() {
        Column() {
          InputTag({
            value: $value,
            remainingSpaceVp: this.remainingSpaceVp,
            lineWidth: $lineWidth, _// 传递lineWidth参数_
            flexWidth: this.flexWidth
          });
    
          Row({ space: 10 }) {
            Button('添加文本')
              .margin(7)
              .onClick(() => {
                const newItems: string\[\] = \[\];
                newItems.push(\`文本\`);
                this.value = \[...newItems, ...this.value\];
                this.calculateLayout();
              });
    
           _ // 添加避让长度输入框_
            TextInput({ text: this.customLineWidth, placeholder: '避让长度' })
              .width(100)
              .height(40)
              .borderRadius(24)
              .padding(8)
              .fontSize(14)
              .backgroundColor('rgba(0, 0, 0, 0.05)')
              .onChange((value: string) => {
                this.customLineWidth = value;
              })
              .onSubmit(() => {
                this.updateLineWidth();
              });
    
            Button('设置')
              .margin(7)
              .width(60)
              .onClick(() => {
                this.value = \['文本'\];
                this.updateLineWidth();
              });
          }
          .width('100%')
          .justifyContent(FlexAlign.Center)
          .margin({ top: 10 });
        }
        .padding({ left: 14, right: 14 })
        .width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center);
      }
    }
    
    运行效果图如下：
    
    ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/10/v3/sxu-JgVNRG-oAmMO26wynA/zh-cn_image_0000002658952791.png?HW-CC-KV=V1&HW-CC-Date=20260709T134112Z&HW-CC-Expire=86400&HW-CC-Sign=34AF7D4FBDEC83B6E685D4A676E64A901376A6CDBA690D0E2A6734AC7A122A10 "点击放大")