openharmony 鸿蒙 arkts-layout-development-dynamiclayout

2026-08-25 浏览 (1)

动态布局 (DynamicLayout)

概述

从API version 24开始,支持动态布局容器组件DynamicLayout。DynamicLayout支持在运行时动态切换不同的布局算法,同时保持子组件的状态不变。通过DynamicLayout,开发者可以灵活实现同一种内容在不同场景下的多种布局展示。DynamicLayout组件支持的布局算法类包括RowLayoutAlgorithmColumnLayoutAlgorithmStackLayoutAlgorithmGridLayoutAlgorithm和自定义布局算法类CustomLayoutAlgorithm

约束与限制

  1. 布局算法类使用@ObservedV2装饰,不支持@State装饰器。
  2. 切换布局算法时,子组件的状态(如输入框内容、滚动位置等)保持不变。
  3. 在自定义布局算法的onMeasureonLayout方法中不允许修改状态变量,避免不可预期的行为。

创建DynamicLayout

通过传入LayoutAlgorithm类型入参,创建DynamicLayout组件并设置布局算法。LayoutAlgorithm类型变量支持赋值具体的布局算法类对象,包括内置布局算法自定义布局算法

import {
  DynamicLayout, DynamicLayoutAttribute,RowLayoutAlgorithm, ColumnLayoutAlgorithm, LayoutAlgorithm
} from '@kit.ArkUI';

@Entry
@ComponentV2
struct CreateDynamicLayout {
  @Local algorithm: LayoutAlgorithm = new RowLayoutAlgorithm();

  build() {
    Column({ space: 10 }) {
      DynamicLayout(this.algorithm) {
        Text('Item 1')
          .fontSize(16)
          .backgroundColor(0xF5DEB3)
          .padding(10)
        Text('Item 2')
          .fontSize(16)
          .backgroundColor(0xD2B48C)
          .padding(10)
        Text('Item 3')
          .fontSize(16)
          .backgroundColor(0xF5DEB3)
          .padding(10)
      }
      .width('100%')
      .height(150)
      .backgroundColor(0xEFEFEF)

      Button('切换为Column布局')
        .fontSize(16)
        .onClick(() => {
          this.algorithm = new ColumnLayoutAlgorithm();
        })
    }
    .width('100%')
    .padding(20)
  }
}

内置布局算法

线性布局算法RowLayoutAlgorithmColumnLayoutAlgorithm具有自适应拉伸、缩放的能力,可以用于界面元素自适应布局的场景。堆叠布局算法StackLayoutAlgorithm具有较强的页面层叠、位置定位能力,可以用于广告、卡片层叠等页面场景。网格布局算法GridLayoutAlgorithm具有较好的规律结构,适合展示同类项目集合,例如显示图片、视频、音乐、新闻、商品等。

RowLayoutAlgorithm

RowLayoutAlgorithm是水平方向线性布局算法,子组件沿水平方向依次排列。该算法支持设置子组件间距、子组件在主轴(水平方向)上的对齐方式、在交叉轴(垂直方向)上的对齐方式,以及是否反转子组件的排列方向。该布局算法与Row组件布局效果一致,详细效果说明请参考线性布局(Row/Column)。下述示例通过修改RowLayoutAlgorithm对象的space、justifyContent、alignItems和isReverse成员变量,调整子组件间距、主轴(水平方向)对齐方式、交叉轴(竖直方向)对齐方式和排列方向。

从API version 24开始,新增RowLayoutAlgorithmspacejustifyContentalignItemsisReverse属性。

import {
  DynamicLayout, DynamicLayoutAttribute, RowLayoutAlgorithm, LengthMetrics
} from '@kit.ArkUI';

@Entry
@ComponentV2
struct RowLayoutExample {
  @Local algorithm: RowLayoutAlgorithm = new RowLayoutAlgorithm({
    space: LengthMetrics.vp(10),
    alignItems: VerticalAlign.Top,
    justifyContent: FlexAlign.Start,
    isReverse: false
  });

  build() {
    Column({ space: 10 }) {
      DynamicLayout(this.algorithm) {
        Text('Item 1')
          .width(80)
          .height(40)
          .fontSize(14)
          .backgroundColor(0xF5DEB3)
        Text('Item 2')
          .width(80)
          .height(40)
          .fontSize(14)
          .backgroundColor(0xD2B48C)
        Text('Item 3')
          .width(80)
          .height(40)
          .fontSize(14)
          .backgroundColor(0xF5DEB3)
      }
      .width('100%')
      .height(80)
      .backgroundColor(0xEFEFEF)

      Row({ space: 10 }) {
        Button('修改间距')
          .fontSize(14)
          .onClick(() => {
            this.algorithm.space = LengthMetrics.vp(20);
          })
        Button('反转排列')
          .fontSize(14)
          .onClick(() => {
            this.algorithm.isReverse = !this.algorithm.isReverse;
          })
      }

      Row({ space: 10 }) {
        Button('竖直居中')
          .fontSize(14)
          .onClick(() => {
            this.algorithm.alignItems = VerticalAlign.Center;
          })
        Button('水平居中')
          .fontSize(14)
          .onClick(() => {
            this.algorithm.justifyContent = FlexAlign.Center;
          })
      }
    }
    .padding(20)
  }
}

dynamiclayout_column

ColumnLayoutAlgorithm

ColumnLayoutAlgorithm是垂直方向线性布局算法,子组件沿垂直方向依次排列。该算法支持设置子组件间距、子组件在主轴(垂直方向)上的对齐方式、在交叉轴(水平方向)上的对齐方式,以及是否反转子组件的排列方向。该布局算法与Column组件布局效果一致,详细效果说明请参考线性布局(Row/Column)。下述示例通过修改ColumnLayoutAlgorithm的space、justifyContent、alignItems和isReverse属性,调整子组件间距、主轴(竖直方向)对齐方式、交叉轴(水平方向)对齐方式和排列方向。

从API version 24开始,新增ColumnLayoutAlgorithmspacejustifyContentalignItemsisReverse属性。

import {
  DynamicLayout, DynamicLayoutAttribute, ColumnLayoutAlgorithm, LengthMetrics
} from '@kit.ArkUI';

@Entry
@ComponentV2
struct ColumnLayoutExample {
  @Local algorithm: ColumnLayoutAlgorithm = new ColumnLayoutAlgorithm({
    space: LengthMetrics.vp(10),
    alignItems: HorizontalAlign.Start,
    justifyContent: FlexAlign.Start,
    isReverse: false
  });

  build() {
    Column({ space: 10 }) {
      DynamicLayout(this.algorithm) {
        Text('Item 1')
          .width('80%')
          .height(40)
          .fontSize(14)
          .backgroundColor(0xF5DEB3)
        Text('Item 2')
          .width('80%')
          .height(40)
          .fontSize(14)
          .backgroundColor(0xD2B48C)
        Text('Item 3')
          .width('80%')
          .height(40)
          .fontSize(14)
          .backgroundColor(0xF5DEB3)
      }
      .width('100%')
      .height(200)
      .backgroundColor(0xEFEFEF)

      Row({ space: 10 }) {
        Button('修改间距')
          .fontSize(14)
          .onClick(() => {
            this.algorithm.space = LengthMetrics.vp(20);
          })
        Button('反转排列')
          .fontSize(14)
          .onClick(() => {
            this.algorithm.isReverse = !this.algorithm.isReverse;
          })
      }

      Row({ space: 10 }) {
        Button('竖直居中')
          .fontSize(14)
          .onClick(() => {
            this.algorithm.justifyContent = FlexAlign.Center;
          })
        Button('水平居中')
          .fontSize(14)
          .onClick(() => {
            this.algorithm.alignItems = HorizontalAlign.Center;
          })
      }
    }
    .padding(20)
  }
}

dynamiclayout_column

StackLayoutAlgorithm

StackLayoutAlgorithm是堆叠布局算法,子组件堆叠排列,后添加的子组件覆盖先添加的子组件。该算法支持通过alignContent设置子组件在容器中的九宫格对齐位置,子组件可以通过layoutGravity属性单独设置自己的对齐方式,优先级高于容器的alignContent。该布局算法与Stack组件布局效果一致,详细效果说明请参考堆叠布局。下述示例通过修改StackLayoutAlgorithmalignContent属性,调整子组件在容器中的九宫格对齐位置。

从API version 24开始,新增StackLayoutAlgorithmalignContent属性。

import {
  DynamicLayout, DynamicLayoutAttribute, StackLayoutAlgorithm
} from '@kit.ArkUI';

@Entry
@ComponentV2
struct StackLayoutExample {
  @Local algorithm: StackLayoutAlgorithm = new StackLayoutAlgorithm({
    alignContent: LocalizedAlignment.CENTER
  });

  build() {
    Column() {
      DynamicLayout(this.algorithm) {
        Text('第一层')
          .fontSize(14)
          .width(150)
          .height(150)
          .backgroundColor(0xD2B48C)
          .layoutGravity(LocalizedAlignment.TOP_START)

        Text('第二层')
          .fontSize(14)
          .width(150)
          .height(150)
          .backgroundColor(0xF5DEB3)
          .layoutGravity(LocalizedAlignment.CENTER)

        Text('第三层')
          .fontSize(14)
          .width(100)
          .height(100)
          .backgroundColor(0x8B4513)
      }
      .width(250)
      .height(250)
      .backgroundColor(0xEFEFEF)

      Row({ space: 10 }) {
        Button('顶部居中')
          .fontSize(14)
          .onClick(() => {
            this.algorithm.alignContent = LocalizedAlignment.TOP;
          })
        Button('居中对齐')
          .fontSize(14)
          .onClick(() => {
            this.algorithm.alignContent = LocalizedAlignment.CENTER;
          })
        Button('底部居中')
          .fontSize(14)
          .onClick(() => {
            this.algorithm.alignContent = LocalizedAlignment.BOTTOM;
          })
      }
      .margin({ top: 20 })
    }
    .width('100%')
  }
}

dynamiclayout_stack

GridLayoutAlgorithm

GridLayoutAlgorithm是垂直方向网格布局算法。该算法支持通过columnsTemplateItemFillPolicy设置列数,设置ItemFillPolicy为BREAKPOINT_DEFAULT时行为与Grid一致,行数由子节点数量和列数决定。该算法支持通过LengthMetrics设置行间距和列间距,通过align设置组件在网格中的对齐方式。下述示例修改GridLayoutAlgorithm的columnsTemplate属性调整网格列数。

从API version 24开始,新增GridLayoutAlgorithmcolumnsTemplate属性。

import {
  DynamicLayout, DynamicLayoutAttribute, GridLayoutAlgorithm, LengthMetrics
} from '@kit.ArkUI';

export class GridDataSource implements IDataSource {
  private list: string[] = [];
  private listeners: DataChangeListener[] = [];

  constructor(list: string[]) {
    this.list = list;
  }

  totalCount(): number {
    return this.list.length;
  }

  getData(index: number): string {
    return this.list[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener);
    }
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
      this.listeners.splice(pos, 1);
    }
  }

  // 通知控制器数据位置变化
  notifyDataMove(from: number, to: number): void {
    this.listeners.forEach(listener => {
      listener.onDataMove(from, to);
    })
  }

  // 交换元素位置
  public swapItem(from: number, to: number): void {
    let temp: string = this.list[from];
    this.list[from] = this.list[to];
    this.list[to] = temp;
    this.notifyDataMove(from, to);
  }
}

@Entry
@ComponentV2
struct GridLayoutExample {
  numbers: GridDataSource = new GridDataSource([]);
  @Local flag: boolean = false
  @Local gridLayoutAlgorithm: GridLayoutAlgorithm = new GridLayoutAlgorithm({
    columnsTemplate: '1fr 1fr 1fr',
    columnsGap: LengthMetrics.vp(10),
    rowsGap: LengthMetrics.vp(10)
  })

  aboutToAppear() {
    let list: string[] = [];
    for (let i = 0; i < 4; i++) {
      for (let j = 0; j < 3; j++) {
        list.push((i * 3 + j).toString());
      }
    }
    this.numbers = new GridDataSource(list);
  }

  build() {
    Column({ space: 10 }) {
      DynamicLayout(this.gridLayoutAlgorithm) {
        LazyForEach(this.numbers, (day: string) => {
          GridItem() {
            Text(day)
              .fontSize(16)
              .backgroundColor(0xF9CF93)
              .width('100%')
              .height(80)
              .textAlign(TextAlign.Center)
          }
        }, (index: number) => index.toString())
      }.width('100%')
      Button('change gridLayoutAlgorithm columns').onClick(() => {
        this.flag = !this.flag
        if (this.flag) {
          this.gridLayoutAlgorithm.columnsTemplate = '1fr 1fr'
        } else {
          this.gridLayoutAlgorithm.columnsTemplate = '1fr 1fr 1fr'
        }
      })
    }
  }
}

自定义布局算法

自定义布局算法通过继承CustomLayoutAlgorithm类,重写onMeasureonLayout方法实现。开发者可以根据具体业务需求,自定义子组件的大小测量和位置排列,实现内置布局算法无法满足的个性化布局效果,如瀑布流、标签云等不规则布局。

自定义布局算法实现指导

通过调用FrameNodegetChildrenCount()getChild()方法,开发者可以获取所有子组件FrameNode。在onMeasure方法中,调用measure()方法可以自定义测量子组件大小。在onLayout方法中,调用getMeasuredSize()可以获取子组件测量后的尺寸,调用layout()方法可以自定义排列子组件位置。下述示例展示如何重写onMeasureonLayout方法,调用FrameNode的相关方法实现水平方向线性布局的效果。

import {
  DynamicLayout, DynamicLayoutAttribute, CustomLayoutAlgorithm, FrameNode, LayoutConstraint, Position, LayoutAlgorithm
} from '@kit.ArkUI';

// 自定义布局算法类
class MyCustomLayout extends CustomLayoutAlgorithm {
  onMeasure(self: FrameNode, constraint: LayoutConstraint): void {
    // 1. 获取子组件数量
    const childCount = self.getChildrenCount();
    let totalWidth = 0;
    let maxHeight = 0;
    // 2. 遍历子组件,进行测量
    for (let i = 0; i < childCount; i++) {
      const child = self.getChild(i);
      if (child) {
        // 3. 创建子组件的布局约束
        const childConstraint: LayoutConstraint = {
          maxSize: { width: 150, height: 150},
          minSize: { width: 150, height: 150},
          percentReference: constraint.percentReference
        };
        // 4. 测量子组件
        child.measure(childConstraint);
        // 5. 获取子组件测量后的尺寸
        const childSize = child.getMeasuredSize();
        totalWidth += childSize.width;
        maxHeight = Math.max(maxHeight, childSize.height);
      }
    }
    const measuredSize: Size = {
      width: Math.min(totalWidth, constraint.maxSize.width),
      height: Math.min(maxHeight, constraint.maxSize.height)
    };
    // 6. 设置自身的测量尺寸
    self.setMeasuredSize(measuredSize);
  }

  onLayout(self: FrameNode, position: Position): void {
    // 1. 获取子组件数量
    const childCount = self.getChildrenCount();
    let offsetX = 0;
    // 2. 遍历子组件,设置布局位置
    for (let i = 0; i < childCount; i++) {
      const child = self.getChild(i);
      if (child) {
        // 3. 获取子组件测量后的尺寸
        const childSize = child.getMeasuredSize();
        const childPosition: Position = {
          x: offsetX,
          y: 0
        };
        // 4. 设置子组件的布局位置
        child.layout(childPosition);
        // 5. 更新偏移量
        offsetX += childSize.width;
      }
    }
    // 6. 设置自身的布局位置
    self.setLayoutPosition(position);
  }
}

@Entry
@ComponentV2
struct CustomLayoutBasic {
  @Local algorithm: LayoutAlgorithm = new MyCustomLayout()
  build() {
    Column({ space: 10 }) {
      DynamicLayout(this.algorithm) {
        Text('Item 1')
          .fontSize(14)
          .backgroundColor(0xF5DEB3)
        Text('Item 2')
          .fontSize(14)
          .backgroundColor(0xD2B48C)
        Text('Item 3')
          .fontSize(14)
          .backgroundColor(0xF5DEB3)
        Text('Item 4')
          .fontSize(14)
          .backgroundColor(0xD2B48C)
      }
      .width('100%')
      .height(200)
      .backgroundColor(0xEFEFEF)
    }
    .padding(20)
  }
}

瀑布流布局

下述示例实现了自定义瀑布流布局算法,将子组件按列排列,每列中的子组件依次堆叠,适用于商品展示的场景。

import {
  DynamicLayout, DynamicLayoutAttribute, CustomLayoutAlgorithm, LayoutAlgorithm, FrameNode, LayoutConstraint, Position
} from '@kit.ArkUI';

// 瀑布流布局算法
class WaterfallLayout extends CustomLayoutAlgorithm {
  private columnCount: number = 2;
  private columnGap: number = 10;
  private rowGap: number = 10;

  onMeasure(self: FrameNode, constraint: LayoutConstraint): void {
    const childCount = self.getChildrenCount();
    const columnWidth = (constraint.maxSize.width - (this.columnCount - 1) * this.columnGap) / this.columnCount;
    // 记录每列的当前高度
    const columnHeights: number[] = new Array(this.columnCount).fill(0);
    for (let i = 0; i < childCount; i++) {
      const child = self.getChild(i);
      if (child) {
        // 通过将minSize和maxSize设置为相同值来约束子组件宽度
        const childConstraint: LayoutConstraint = {
          maxSize: {
            width: columnWidth,
            height: constraint.maxSize.height
          },
          minSize: {
            width: columnWidth,
            height: 0
          },
          percentReference: constraint.percentReference
        };
        child.measure(childConstraint);
        // 找到当前高度最小的列
        const minColumn = columnHeights.indexOf(Math.min(...columnHeights));
        columnHeights[minColumn] += child.getMeasuredSize().height + this.rowGap;
      }
    }
    const maxHeight = Math.max(...columnHeights);
    self.setMeasuredSize({
      width: constraint.maxSize.width,
      height: maxHeight
    });
  }

  onLayout(self: FrameNode, position: Position): void {
    const childCount = self.getChildrenCount();
    const measuredSize = self.getMeasuredSize();
    const columnWidth = (measuredSize.width - (this.columnCount - 1) * this.columnGap) / this.columnCount;
    // 记录每列的当前Y坐标
    const columnYs: number[] = new Array(this.columnCount).fill(0);
    for (let i = 0; i < childCount; i++) {
      const child = self.getChild(i);
      if (child) {
        const childSize = child.getMeasuredSize();
        // 找到当前Y坐标最小的列
        const minColumn = columnYs.indexOf(Math.min(...columnYs));
        const x = minColumn * (columnWidth + this.columnGap);
        const y = columnYs[minColumn];
        child.layout({ x, y });
        columnYs[minColumn] += childSize.height + this.rowGap;
      }
    }
    self.setLayoutPosition(position);
  }
}

@Entry
@ComponentV2
struct WaterfallLayoutExample {
  @Local algorithm: LayoutAlgorithm = new WaterfallLayout();

  // 商品数据
  private products: Product[] = [
    { id: '1', name: '时尚运动鞋', price: '¥399', height: 180, image: '商品图' },
    { id: '2', name: '休闲双肩包', price: '¥259', height: 220, image: '商品图' },
    { id: '3', name: '无线蓝牙耳机', price: '¥599', height: 150, image: '商品图' },
    { id: '4', name: '智能手表', price: '¥1299', height: 200, image: '商品图' },
    { id: '5', name: '太阳眼镜', price: '¥199', height: 130, image: '商品图' },
    { id: '6', name: '便携充电宝', price: '¥129', height: 170, image: '商品图' },
    { id: '7', name: '机械键盘', price: '¥459', height: 160, image: '商品图' },
    { id: '8', name: '游戏鼠标', price: '¥189', height: 140, image: '商品图' },
    { id: '9', name: '高清显示器', price: '¥1599', height: 210, image: '商品图' },
    { id: '10', name: '智能音箱', price: '¥299', height: 190, image: '商品图' }
  ];

  // 商品卡片组件
  @Builder ProductCard(product: Product) {
    Column() {
      Text(product.image)
        .fontSize(18)
        .margin({ bottom: 8 })
      Text(product.name)
        .fontSize(14)
        .fontWeight(FontWeight.Medium)
        .fontColor(0x333333)
        .margin({ bottom: 4 })
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
      Text(product.price)
        .fontSize(16)
        .fontColor(0xFF6B35)
        .fontWeight(FontWeight.Bold)
    }
    .width('100%')
    .padding(12)
    .backgroundColor(0xFAFAFA)
    .borderRadius(8)
    .border({ width: 1, color: 0xE0E0E0 })
    .height(product.height)
    .justifyContent(FlexAlign.Center)
  }

  build() {
    Column() {
      Text('商品列表 - 瀑布流布局')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 20 })

      Scroll() {
        DynamicLayout(this.algorithm) {
          ForEach(this.products, (product: Product) => {
            this.ProductCard(product)
          })
        }
        .width('100%')
        .backgroundColor(0xEFEFEF)
        .borderRadius(12)
        .padding(10)
      }
      .scrollable(ScrollDirection.Vertical)
      .scrollBar(BarState.Auto)
      .edgeEffect(EdgeEffect.Spring)
      .width('100%')
      .layoutWeight(1)

      Text('商品卡片自动分配到高度最小的列')
        .fontSize(14)
        .fontColor(Color.Gray)
        .margin({ top: 12 })
    }
    .padding(20)
    .width('100%')
    .height('100%')
  }
}

// 商品数据模型
interface Product {
  id: string;
  name: string;
  price: string;
  height: number;
  image: string;
}

dynamiclayout_stack

网格布局

下述示例实现一个自定义网格布局算法,将子组件按网格排列,同一行的子组件高度保持一致。

import {
  DynamicLayout, DynamicLayoutAttribute, CustomLayoutAlgorithm, LayoutAlgorithm, FrameNode, LayoutConstraint, Position
} from '@kit.ArkUI';

// 2x2网格布局算法
export class GridLayout extends CustomLayoutAlgorithm {
  private gap: number = 12;
  private itemHeights: number[] = [];

  onMeasure(self: FrameNode, constraint: LayoutConstraint): void {
    const childCount = self.getChildrenCount();
    const columns = 2;
    const itemWidth = (constraint.maxSize.width - (columns - 1) * this.gap) / columns;
    this.itemHeights = [];

    // 第一遍测量:获取每个子组件的理想高度
    for (let i = 0; i < childCount; i++) {
      const child = self.getChild(i);
      if (child) {
        const childConstraint: LayoutConstraint = {
          maxSize: { width: itemWidth, height: Number.MAX_VALUE },
          minSize: { width: itemWidth, height: 0 },
          percentReference: constraint.percentReference
        };
        child.measure(childConstraint);
        this.itemHeights.push(child.getMeasuredSize().height);
      }
    }

    // 计算每行的最大高度
    const rows = Math.ceil(childCount / columns);
    const rowHeights: number[] = [];
    for (let r = 0; r < rows; r++) {
      let maxRowHeight = 0;
      for (let c = 0; c < columns; c++) {
        const index = r * columns + c;
        if (index < this.itemHeights.length && this.itemHeights[index] > maxRowHeight) {
          maxRowHeight = this.itemHeights[index];
        }
      }
      rowHeights.push(maxRowHeight);
    }

    // 计算总高度
    const totalHeight = rowHeights.reduce((sum, h) => sum + h, 0) + (rows - 1) * this.gap;

    // 第二遍测量:使用每行的统一高度重新测量子组件
    for (let i = 0; i < childCount; i++) {
      const child = self.getChild(i);
      if (child) {
        const row = Math.floor(i / columns);
        const rowHeight = rowHeights[row];
        const childConstraint: LayoutConstraint = {
          maxSize: { width: itemWidth, height: rowHeight },
          minSize: { width: itemWidth, height: rowHeight },
          percentReference: constraint.percentReference
        };
        child.measure(childConstraint);
      }
    }

    self.setMeasuredSize({
      width: constraint.maxSize.width,
      height: totalHeight
    });
  }

  onLayout(self: FrameNode, position: Position): void {
    const childCount = self.getChildrenCount();
    const measuredSize = self.getMeasuredSize();
    const columns = 2;
    const itemWidth = (measuredSize.width - (columns - 1) * this.gap) / columns;

    // 重新计算每行的最大高度
    const rows = Math.ceil(childCount / columns);
    const rowHeights: number[] = [];
    for (let r = 0; r < rows; r++) {
      let maxRowHeight = 0;
      for (let c = 0; c < columns; c++) {
        const index = r * columns + c;
        if (index < this.itemHeights.length && this.itemHeights[index] > maxRowHeight) {
          maxRowHeight = this.itemHeights[index];
        }
      }
      rowHeights.push(maxRowHeight);
    }

    for (let i = 0; i < childCount; i++) {
      const child = self.getChild(i);
      if (child) {
        const row = Math.floor(i / columns);
        const col = i % columns;
        const x = col * (itemWidth + this.gap);
        const y = row === 0 ? 0 : rowHeights.slice(0, row).reduce((sum, h) => sum + h, 0) + row * this.gap;
        child.layout({ x, y });
      }
    }

    self.setLayoutPosition(position);
  }
}

@Entry
@ComponentV2
struct GridLayoutExample {
  @Local algorithm: LayoutAlgorithm = new GridLayout();

  build() {
    Column() {
      Text('网格布局示例')
        .fontSize(18)
        .margin({ bottom: 20 })

      DynamicLayout(this.algorithm) {
        ForEach(['卡片1', '卡片2', '卡片3', '卡片4'], (title: string, index: number) => {
          Column() {
            Text(title)
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor(0x333333)
              .margin({ bottom: 8 })
            Text('内容区域')
              .fontSize(12)
              .fontColor(0x666666)
          }
          .width('100%')
          .padding(12)
          .backgroundColor(0xFAFAFA)
          .borderRadius(8)
          .border({ width: 1, color: 0xE0E0E0 })
        })
      }
      .width('100%')
      .backgroundColor(0xEFEFEF)
      .borderRadius(12)
      .padding(12)

      Text('同一行的子组件高度保持一致')
        .fontSize(14)
        .fontColor(Color.Gray)
        .margin({ top: 20 })
    }
    .padding(20)
    .width('100%')
  }
}

标签云布局

下述示例实现一个自定义标签云布局,标签自动换行排列,适合展示搜索历史、热门标签、技能标签等不规则布局的场景。

import {
  DynamicLayout, DynamicLayoutAttribute, CustomLayoutAlgorithm, LayoutAlgorithm, FrameNode, LayoutConstraint, Position
} from '@kit.ArkUI';

// 标签云布局算法
class TagCloudLayout extends CustomLayoutAlgorithm {
  private horizontalGap: number = 12;
  private verticalGap: number = 12;

  onMeasure(self: FrameNode, constraint: LayoutConstraint): void {
    const childCount = self.getChildrenCount();
    const maxWidth = constraint.maxSize.width;

    let currentLineWidth = 0;
    let totalHeight = 0;
    let maxLineWidth = 0;

    for (let i = 0; i < childCount; i++) {
      const child = self.getChild(i);
      if (child) {
        // 测量子组件,不限制宽高
        const childConstraint: LayoutConstraint = {
          maxSize: { width: maxWidth, height: Number.MAX_VALUE },
          minSize: { width: 0, height: 0 },
          percentReference: constraint.percentReference
        };
        child.measure(childConstraint);
        const childSize = child.getMeasuredSize();
        // 检查是否需要换行
        if (currentLineWidth + childSize.width > maxWidth && currentLineWidth > 0) {
          // 换行
          totalHeight += this.verticalGap;
          currentLineWidth = childSize.width;
          totalHeight += childSize.height;
          maxLineWidth = Math.max(maxLineWidth, currentLineWidth - this.horizontalGap);
        } else {
          // 继续当前行
          currentLineWidth += childSize.width + this.horizontalGap;
          if (i === 0) {
            totalHeight = childSize.height;
          }
          maxLineWidth = Math.max(maxLineWidth, currentLineWidth - this.horizontalGap);
        }
      }
    }
    self.setMeasuredSize({
      width: Math.min(maxLineWidth, maxWidth),
      height: totalHeight
    });
  }

  onLayout(self: FrameNode, position: Position): void {
    const childCount = self.getChildrenCount();
    const measuredSize = self.getMeasuredSize();
    const maxWidth = measuredSize.width;

    let currentX = 0;
    let currentY = 0;
    let currentLineHeight = 0;

    for (let i = 0; i < childCount; i++) {
      const child = self.getChild(i);
      if (child) {
        const childSize = child.getMeasuredSize();
        // 检查是否需要换行
        if (currentX + childSize.width > maxWidth && currentX > 0) {
          // 换行
          currentY += currentLineHeight + this.verticalGap;
          currentX = 0;
          currentLineHeight = 0;
        }
        // 布局子组件
        child.layout({ x: currentX, y: currentY })
        // 更新位置
        currentX += childSize.width + this.horizontalGap;
        currentLineHeight = Math.max(currentLineHeight, childSize.height);
      }
    }
    self.setLayoutPosition(position);
  }
}

@Entry
@ComponentV2
struct TagCloudExample {
  @Local algorithm: LayoutAlgorithm = new TagCloudLayout();

  // 热门标签数据
  private tags: string[] = [
    '标签1', '标签标签标签', '标签2', '标签标签', '标签标签标',
    '标签标签标', '标签标签标签标签', '标签标签', '标签标签',
    '标签标', '标签标签', '标签标签', '标签标签标签',
    '标签标', '标签标签', '标签标签', '标签标签标'
  ];

  // 标签组件
  @Builder TagItem(tag: string, index: number) {
    Text(tag)
      .fontSize(14)
      .fontColor([0xFF6B6B, 0x4ECDC4, 0x45B7D1, 0xFFA07A, 0x98D8C8, 0xF7DC6F][index % 6])
      .padding({ left: 12, right: 12, top: 8, bottom: 8 })
      .backgroundColor(0xF5F5F5)
      .borderRadius(16)
      .border({ width: 1, color: 0xE0E0E0 })
  }

  build() {
    Column() {
      Text('热门话题 - 标签云布局')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 20 })

      Scroll() {
        DynamicLayout(this.algorithm) {
          ForEach(this.tags, (tag: string, index: number) => {
            this.TagItem(tag, index)
          }, (tag: string, index: number) => `${index}_${tag}`)
        }
        .width('100%')
        .padding(16)
      }
      .scrollable(ScrollDirection.Vertical)
      .scrollBar(BarState.Auto)
      .edgeEffect(EdgeEffect.Spring)
      .width('100%')

      Text('标签自动换行,紧凑排列')
        .fontSize(14)
        .fontColor(Color.Gray)
        .margin({ top: 12 })
    }
    .padding(20)
    .width('100%')
    .height('100%')
  }
}

切换布局算法

DynamicLayout在切换布局算法时会保持子组件的状态不变,比如输入框内容、开关状态、进度条值等。下述示例展示TextInputToggleSliderCheckBox组件在布局切换过程中保持状态,同时使用animateTo为布局切换添加平滑的动画效果。

import {
  DynamicLayout, DynamicLayoutAttribute, ColumnLayoutAlgorithm, LayoutAlgorithm, curves, LengthMetrics,
  GridLayoutAlgorithm
} from '@kit.ArkUI';

@Entry
@ComponentV2
struct StatePreservationExample {
  @Local algorithm: LayoutAlgorithm = new ColumnLayoutAlgorithm({
    space: LengthMetrics.vp(12)
  });

  build() {
    Column() {
      Text('布局切换状态保持示例')
        .fontSize(18)
        .margin({ bottom: 20 })

      // 使用animateTo为布局切换添加动画效果
      DynamicLayout(this.algorithm) {
        // 子组件1:带输入框的卡片
        Column() {
          Text('输入框')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(0x333333)
            .margin({ bottom: 8 })
          TextInput({ placeholder: '请输入' })
            .width('100%')
            .height(36)
            .fontSize(12)
        }
        .width('100%')
        .padding(12)
        .backgroundColor(0x80F0F0F0)
        .borderRadius(8)

        // 子组件2:带开关的卡片
        Column() {
          Text('开关')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(0x333333)
            .margin({ bottom: 8 })
          Toggle({ type: ToggleType.Switch, isOn: true })
            .selectedColor(0xD4D4D4)
            .height(26)
            .width(52)
        }
        .width('100%')
        .padding(12)
        .backgroundColor(0x80F0F0F0)
        .borderRadius(8)

        // 子组件3:带进度条的卡片
        Column() {
          Text('进度条')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(0x333333)
            .margin({ bottom: 8 })
          Slider({ value: 60, min: 0, max: 100 })
            .width('100%')
            .trackColor(0xD4D4D4)
            .selectedColor(0xD4D4D4)
            .height(36)
        }
        .width('100%')
        .padding(12)
        .backgroundColor(0x80F0F0F0)
        .borderRadius(8)

        // 子组件4:带复选框的卡片
        Column() {
          Text('复选框')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(0x333333)
            .margin({ bottom: 8 })
          Row() {
            Checkbox({ name: 'check1' })
              .select(false)
              .selectedColor(0xD4D4D4)
            Text('记住密码')
              .fontSize(12)
              .fontColor(0x333333)
              .margin({ left: 8 })
          }
          .height(36)
        }
        .width('100%')
        .padding(12)
        .backgroundColor(0x80F0F0F0)
        .borderRadius(8)
      }
      .width('100%')
      .borderRadius(12)
      .padding(12)

      Row({ space: 10 }) {
        Button('列表布局')
          .onClick(() => {
            this.getUIContext()?.animateTo({ duration: 300, curve: curves.springMotion() }, () => {
              this.algorithm = new ColumnLayoutAlgorithm({
                space: LengthMetrics.vp(12)
              });
            });
          })
        Button('网格布局')
          .onClick(() => {
            this.getUIContext()?.animateTo({ duration: 300, curve: curves.springMotion() }, () => {
              this.algorithm = new GridLayoutAlgorithm({
                columnsTemplate: '1fr 1fr',
                columnsGap: LengthMetrics.vp(10),
                rowsGap: LengthMetrics.vp(10)
              });
            });
          })
      }
      .margin({ top: 20 })

      Text('切换布局后,子组件状态保持不变')
        .fontSize(14)
        .fontColor(Color.Gray)
        .margin({ top: 12 })
    }
    .padding(20)
    .width('100%')
  }
}

DynamicLayout支持以下几种方式触发重新布局:

  • 通过状态变量切换布局算法。

    开发者使用@Local装饰器修饰布局算法变量,可以实现运行时动态切换布局。

    import {
      DynamicLayout, DynamicLayoutAttribute, RowLayoutAlgorithm, ColumnLayoutAlgorithm,
      StackLayoutAlgorithm, GridLayoutAlgorithm, LayoutAlgorithm, LengthMetrics
    } from '@kit.ArkUI';
    
    @Entry
    @ComponentV2
    struct LayoutSwitchExample {
      @Local algorithm: LayoutAlgorithm = new RowLayoutAlgorithm({
        space: LengthMetrics.vp(10),
        alignItems: VerticalAlign.Center
      });
      @Local childWidth: string = '20%'
      @Local childHeight: string = '20%'
    
      build() {
        Column() {
          // 使用状态变量控制布局算法
          DynamicLayout(this.algorithm) {
            Text('Item 1')
              .width(this.childWidth)
              .height(this.childHeight)
              .fontSize(14)
              .textAlign(TextAlign.Center)
              .backgroundColor(0xF5DEB3)
              .borderRadius(8)
              .layoutGravity(LocalizedAlignment.TOP_START)
            Text('Item 2')
              .width(this.childWidth)
              .height(this.childHeight)
              .fontSize(14)
              .textAlign(TextAlign.Center)
              .backgroundColor(0xF5DEB3)
              .borderRadius(8)
              .layoutGravity(LocalizedAlignment.TOP_END)
            Text('Item 3')
              .width(this.childWidth)
              .height(this.childHeight)
              .fontSize(14)
              .textAlign(TextAlign.Center)
              .backgroundColor(0xF5DEB3)
              .borderRadius(8)
              .layoutGravity(LocalizedAlignment.BOTTOM_START)
            Text('Item 4')
              .width(this.childWidth)
              .height(this.childHeight)
              .fontSize(14)
              .textAlign(TextAlign.Center)
              .backgroundColor(0xF5DEB3)
              .borderRadius(8)
              .layoutGravity(LocalizedAlignment.BOTTOM_END)
          }
          .width(300)
          .height(280)
          .backgroundColor(0xEFEFEF)
          .borderRadius(12)
          .padding(10)
    
          Column({ space: 10 }) {
            Row({ space: 10 }) {
              Button('Row布局')
                .onClick(() => {
                  this.algorithm = new RowLayoutAlgorithm({
                    space: LengthMetrics.vp(10),
                    alignItems: VerticalAlign.Center
                  });
                  this.childWidth = '20%'
                  this.childHeight = '20%'
                })
              Button('Column布局')
                .onClick(() => {
                  this.algorithm = new ColumnLayoutAlgorithm({
                    space: LengthMetrics.vp(10),
                    alignItems: HorizontalAlign.Center
                  });
                  this.childWidth = '20%'
                  this.childHeight = '20%'
                })
            }
            Row({ space: 10 }) {
              Button('Stack布局')
                .onClick(() => {
                  this.algorithm = new StackLayoutAlgorithm({
                    alignContent: LocalizedAlignment.CENTER
                  });
                  this.childWidth = '20%'
                  this.childHeight = '20%'
                })
              Button('Grid布局')
                .onClick(() => {
                  this.algorithm = new GridLayoutAlgorithm({
                    columnsTemplate: '1fr 1fr',
                    rowsGap: LengthMetrics.vp(5),
                    columnsGap: LengthMetrics.vp(5)
                  });
                  this.childWidth = '100%'
                  this.childHeight = '50%'
                })
            }
          }
          .margin({ top: 20 })
        }
        .padding(20)
      }
    }
    

  • 通过条件运算符切换布局算法。

    开发者可以使用条件运算符,根据状态变量的值选择合适的布局算法。

    import { 
      DynamicLayout, DynamicLayoutAttribute, RowLayoutAlgorithm, ColumnLayoutAlgorithm, LengthMetrics 
    } from '@kit.ArkUI';
    
    @Entry
    @ComponentV2
    struct ConditionalLayoutExample {
      @Local isHorizontal: boolean = true;
    
      build() {
        Column() {
          // 使用三元运算符根据条件选择布局算法
          DynamicLayout(
            this.isHorizontal
              ? new RowLayoutAlgorithm({ space: LengthMetrics.vp(10) })
              : new ColumnLayoutAlgorithm({ space: LengthMetrics.vp(10) })
          ) {
            Text('Item 1')
              .width(80)
              .height(40)
              .fontSize(14)
              .backgroundColor(0xF5DEB3)
            Text('Item 2')
              .width(80)
              .height(40)
              .fontSize(14)
              .backgroundColor(0xD2B48C)
            Text('Item 3')
              .width(80)
              .height(40)
              .fontSize(14)
              .backgroundColor(0xF5DEB3)
          }
          .width('100%')
          .height(150)
          .backgroundColor(0xEFEFEF)
    
          Button('切换方向')
            .onClick(() => {
              this.isHorizontal = !this.isHorizontal;
            })
        }
        .padding(20)
      }
    }
    

  • 通过修改算法属性触发重新布局。

    布局算法类使用@ObservedV2装饰,布局算法成员属性使用@Trace装饰,修改属性值可以触发DynamicLayout组件重新布局。

    import {
      DynamicLayout, DynamicLayoutAttribute, RowLayoutAlgorithm, LengthMetrics
    } from '@kit.ArkUI';
    
    @Entry
    @ComponentV2
    struct PropertyChangeExample {
      @Local algorithm: RowLayoutAlgorithm = new RowLayoutAlgorithm({
        space: LengthMetrics.vp(10),
        justifyContent: FlexAlign.Start
      });
    
      build() {
        Column() {
          DynamicLayout(this.algorithm) {
            Text('Item 1')
              .width(60)
              .height(40)
              .fontSize(14)
              .backgroundColor(0xF5DEB3)
            Text('Item 2')
              .width(60)
              .height(40)
              .fontSize(14)
              .backgroundColor(0xD2B48C)
            Text('Item 3')
              .width(60)
              .height(40)
              .fontSize(14)
              .backgroundColor(0xF5DEB3)
          }
          .width('100%')
          .height(80)
          .backgroundColor(0xEFEFEF)
    
          Row({ space: 10 }) {
            Button('增加间距')
              .fontSize(14)
              .onClick(() => {
                // 修改space属性触发重新布局
                const currentSpace = this.algorithm.space?.value;
                this.algorithm.space = LengthMetrics.vp(currentSpace as number + 5);
              })
            Button('居中对齐')
              .fontSize(14)
              .onClick(() => {
                // 修改justifyContent属性触发重新布局
                this.algorithm.justifyContent = FlexAlign.Center;
              })
            Button('两端对齐')
              .fontSize(14)
              .onClick(() => {
                this.algorithm.justifyContent = FlexAlign.SpaceBetween;
              })
          }
          .margin({ top: 20 })
        }
        .padding(20)
      }
    }
    

  • 响应式布局算法切换。

    开发者可以结合mediaquery接口监听屏幕方向变化,自动切换商品列表的布局方式。竖屏时使用列表视图(每行一个商品),横屏时使用网格视图(2x2网格布局)。

    import {
      DynamicLayout, DynamicLayoutAttribute, ColumnLayoutAlgorithm, LayoutAlgorithm, LengthMetrics, mediaquery,
      GridLayoutAlgorithm
    } from '@kit.ArkUI';
    
    // 商品数据模型
    interface Product {
      id: string;
      name: string;
      price: string;
      image: string;
    }
    
    @Entry
    @ComponentV2
    struct ProductListExample {
      @Local algorithm: LayoutAlgorithm = new ColumnLayoutAlgorithm({
        space: LengthMetrics.vp(12)
      });
      @Local currentOrientation: string = '竖屏';
      // 商品数据
      private products: Product[] = [
        { id: '1', name: '智能手机 Pro', price: '¥5999', image: '商品' },
        { id: '2', name: '无线耳机', price: '¥899', image: '商品' },
        { id: '3', name: '智能手表', price: '¥1999', image: '商品' },
        { id: '4', name: '平板电脑', price: '¥3999', image: '商品' }
      ];
    
      // 监听横屏事件
      listener: mediaquery.MediaQueryListener = this.getUIContext().getMediaQuery().matchMediaSync('(orientation: landscape)');
    
      // 当满足媒体查询条件时,触发回调
      onOrientationChange(mediaQueryResult: mediaquery.MediaQueryResult) {
        if (mediaQueryResult.matches) {
          // 横屏:使用2x2网格布局
          this.algorithm = new GridLayoutAlgorithm({
            columnsTemplate: '1fr 1fr',
            columnsGap: LengthMetrics.vp(10),
            rowsGap: LengthMetrics.vp(10)
          });
          this.currentOrientation = '横屏';
        } else {
          // 竖屏:使用列表布局(每行一个商品)
          this.algorithm = new ColumnLayoutAlgorithm({
            space: LengthMetrics.vp(12)
          });
          this.currentOrientation = '竖屏';
        }
      }
    
      aboutToAppear() {
        // 绑定回调函数
        this.listener.on('change', (mediaQueryResult: mediaquery.MediaQueryResult) => {
          this.onOrientationChange(mediaQueryResult);
        });
      }
    
      aboutToDisappear() {
        // 解绑listener中注册的回调函数
        this.listener.off('change');
      }
    
      // 商品卡片组件
      @Builder ProductCard(product: Product) {
        Row() {
          Text(product.image)
            .fontSize(20)
            .margin({ right: 12 })
          Column() {
            Text(product.name)
              .fontSize(16)
              .fontWeight(FontWeight.Medium)
              .fontColor(0x333333)
              .margin({ bottom: 4 })
            Text(product.price)
              .fontSize(18)
              .fontColor(0xFF6B35)
              .fontWeight(FontWeight.Bold)
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ right: 12 })
          Button('购买')
            .fontSize(14)
            .height(32)
        }
        .width('100%')
        .padding(16)
        .backgroundColor(0xFAFAFA)
        .borderRadius(8)
        .border({ width: 1, color: 0xE0E0E0 })
      }
    
      build() {
        Column() {
          // 标题栏
          Row() {
            Text('商品列表')
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
              .fontColor(0x333333)
            Blank()
            Text(`${this.currentOrientation}`)
              .fontSize(12)
              .fontColor(0x999999)
              .padding({ left: 8, right: 8, top: 4, bottom: 4 })
              .backgroundColor(0xF0F0F0)
              .borderRadius(4)
          }
          .width('100%')
          .padding({ left: 16, right: 16, top: 12, bottom: 12 })
          .backgroundColor(Color.White)
    
          // 商品列表
          Scroll() {
            DynamicLayout(this.algorithm) {
              ForEach(this.products, (product: Product) => {
                this.ProductCard(product)
              })
            }
            .width('100%')
            .layoutWeight(1)
            .padding(12)
          }
          .layoutWeight(1)
          .width('100%')
          .backgroundColor(0xF5F5F5)
    
          // 提示信息
          Text('旋转设备可查看不同布局效果')
            .fontSize(12)
            .fontColor(0x999999)
            .textAlign(TextAlign.Center)
            .padding(12)
            .width('100%')
            .backgroundColor(Color.White)
        }
        .width('100%')
        .height('100%')
      }
    }
    

你可能感兴趣的鸿蒙文章

openharmony 鸿蒙 arkts-common-components-text-input

openharmony 鸿蒙 arkts-select-component-faq

openharmony 鸿蒙 arkui-overview

openharmony 鸿蒙 js-framework-syntax-css

openharmony 鸿蒙 arkts-popup-and-menu-components-popup

openharmony 鸿蒙 arkts-navigation-animation-faq

openharmony 鸿蒙 arkts-rotation-transition-animation

openharmony 鸿蒙 arkts-popup-and-menu-components-uicontext-popup

openharmony 鸿蒙 arkts-styled-string

openharmony 鸿蒙 arkts-attribute-animation-overview

  • 所属分类: 后端技术
  • 本文标签: 软件 鸿蒙
  • 版权声明: 本文链接 https://seaxiang.com/blog/tjBf7Is9