鸿蒙开发(十五)Grid(网格布局)、@BuilderParam(构建参数装饰器),构建方法的传参问题

159 阅读5分钟

网格布局

网格布局是由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局。网格布局具有较强的页面均分能力,子组件占比控制能力,是一种重要自适应布局。

  • 网格布局所使用的容器组件为Grid。Grid的子组件必须是GridItem。下面的代码演示了一个横向排列的Grid中有九个GridItem的场景:
//Index.ets

@Entry
@Component
struct Index {
  @State itemNumList:number[] = [1,2,3,4,5,6,7,8,9]

  build() {
    Grid() {
      ForEach(this.itemNumList, (item:number, index)=>{
        GridItem(){
          Text('GridItem'+item)
            .textAlign(TextAlign.Center)
            .width(100)
            .height(100)
            .backgroundColor('#ffd99999')
            .border({width:{right:1,bottom:1}, color:Color.Black})
        }
      })
    }
    .height('100%')
    .width('100%')
  }
}

image.png

  • 在这个例子中,我们没有对Grid进行任何设置,可以看到Grid中的GridItem默认是从左到右横向排列的,当一行不足以放下所有的GridItem时,就会进行换行。我们可以通过.layoutDirection()这个属性方法来改变Grid的排列方向:
// Index.ets

@Entry
@Component
struct Index {
  @State itemNumList:number[] = [1,2,3,4,5,6,7,8,9]

  build() {
    Grid() {...}
    // 从上到下竖向排列
    .layoutDirection(GridDirection.Column)
    .height('100%')
    .width('100%')
  }
}

image.png

还可以设置从右到左排列(GridDirection.RowReverse)和从下到上排列(GridDirection.ColumnReverse),在此不做演示。

  • Grid可以通过.rowsTemplate()设置网格中列的数量和各列的宽度占比。例如'1fr 2fr 1fr'表示Grid水平方向上可以摆放3列,Grid的宽被均分为1+2+1=4份,第一列占1份,第二列占2份,第三列占3份。可以看到列中的元素在列中默认是居中摆放的:
// Index.ets

@Entry
@Component
struct Index {
  @State itemNumList:number[] = [1,2,3,4,5,6,7,8,9]

  build() {
    Grid() {
      ForEach(this.itemNumList, (item:number, index)=>{
        GridItem(){
          Text('GridItem'+item)
            .textAlign(TextAlign.Center)
            .width(100)
            .height(100)
            .backgroundColor('#ffd99999')
            .border({width:1, color:Color.Black})
        }
      })
    }
    .height('100%')
    .width('100%')
    .columnsTemplate('1fr 2fr 1fr')
  }
}

image.png

  • 如果列的宽度不足以容纳元素时,元素会被之后的元素遮挡。例如下面的代码把Grid分成了5列:
// Index.ets

@Entry
@Component
struct Index {
  @State itemNumList:number[] = [1,2,3,4,5,6,7,8,9]

  build() {
    Grid() {...}
    .height('100%')
    .width('100%')
    .columnsTemplate('1fr 1fr 1fr 1fr 1fr')
  }
}

image.png

  • 同样的,也可以通过.rowsTemplate()这个属性方法设置网格的行数量和各行的高度占比。如果只设置rowsTemplate而不设置columnsTemplate,Grid的排列方向会变更为从上到下纵向排列,且无法通过.layoutDirection()更改:
// Index.ets

@Entry
@Component
struct Index {
  @State itemNumList:number[] = [1,2,3,4,5,6,7,8,9]

  build() {
    Grid() {...}
    .height('100%')
    .width('100%')
    .rowsTemplate('1fr 1fr 1fr')
    .layoutDirection(GridDirection.Row)
  }
}

image.png

  • 单独设置rowsTemplate亦是如此。如果同时设置了rowsTemplate和columnTemplate,则最多只会显示行*列个元素,剩余的元素不会显示:
// Index.ets

@Entry
@Component
struct Index {
  @State itemNumList:number[] = [1,2,3,4,5,6,7,8,9]

  build() {
    Grid() {...}
    .height('100%')
    .width('100%')
    .rowsTemplate('1fr 1fr')
    .columnsTemplate('1fr 1fr 1fr')
    .layoutDirection(GridDirection.Row)
  }
}

image.png

  • .minCount().maxCount()可以设置组件在布局方向上的最小或者最大摆放数量,如果同时设置且minCount > maxCount时,则两个属性都无效:
// Index.ets

@Entry
@Component
struct Index {
  @State itemNumList:number[] = [1,2,3,4,5,6,7,8,9]

  build() {
    Grid() {...}
    .height('100%')
    .width('100%')
    .layoutDirection(GridDirection.Row)
    .minCount(4)
  }
}

image.png

// Index.ets

@Entry
@Component
struct Index {
  @State itemNumList:number[] = [1,2,3,4,5,6,7,8,9]

  build() {
    Grid() {...}
    .height('100%')
    .width('100%')
    .layoutDirection(GridDirection.Row)
    .maxCount(2)
  }
}

image.png

// Index.ets

@Entry
@Component
struct Index {
  @State itemNumList:number[] = [1,2,3,4,5,6,7,8,9]

  build() {
    Grid() {...}
    .height('100%')
    .width('100%')
    .layoutDirection(GridDirection.Row)
    .minCount(4)
    .maxCount(2)
  }
}

image.png

  • 需要注意的是,它们比.columnsTemplate().rowsTemplate()的优先级要低,也就是说当调用了.columnsTemplate().rowsTemplate()时,.minCount().maxCount()无效:
// Index.ets

@Entry
@Component
struct Index {
  @State itemNumList:number[] = [1,2,3,4,5,6,7,8,9]

  build() {
    Grid() {...}
    .height('100%')
    .width('100%')
    .layoutDirection(GridDirection.Row)
    .columnsTemplate('1fr 1fr 1fr')
    .minCount(4)
  }
}

image.png

  • .cellLength()可以设置行的高度或者列的宽度,当布局方向为水平时(Row/RowReverse),设置的是行高度,当布局方向为垂直时(Column/ColumnReverse),设置的是列宽度:
@Entry
@Component
struct Index {
  @State itemNumList:number[] = [1,2,3,4,5,6,7,8,9]

  build() {
    Grid() {...}
    .height('100%')
    .width('100%')
    .layoutDirection(GridDirection.Row)
    .cellLength(60)
  }
}

image.png

@Entry
@Component
struct Index {
  @State itemNumList:number[] = [1,2,3,4,5,6,7,8,9]

  build() {
    Grid() {...}
    .height('100%')
    .width('100%')
    .layoutDirection(GridDirection.Column)
    .cellLength(25)
  }
}

image.png

构建参数装饰器

我们开发的时候经常会碰到多个页面含有部分组件相同而剩余部分不同的情况,我们可以把相同的部分整合到自定义组件中,作为一个组件模板,然后利用构建参数装饰器,把不同的部分当作一个构建参数传到模板中,实现快速构建。

  • 我们新建文件ets/templates/SearchBarContainer.ets,用以下代码创建一个带搜索栏的组件模板:
@Preview
@Component
export struct SearchBarContainer {
  // 构建参数,参数类型是@Builder装饰的方法,且必须有默认值
  @BuilderParam builder: () => void = this.defaultBuilder

  build() {
    Column(){
      Search({ placeholder: '请输入关键字' }).searchButton('搜索')
      this.builder()
    }
  }

  @Builder
  defaultBuilder() {
  }
}
  • 现在我们回到Index页面使用这个模板创建两个自定义组件:
import { SearchBarContainer } from '../templates/SearchContainer';

@Entry
@Component
struct Index {
  build() {
    Column() {
      SearchBarContainer({ builder: this.imageBuilder })
      SearchBarContainer({ builder: this.textBuilder })
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  imageBuilder() {
    Image($r('app.media.startIcon'))
      .width('50%')
      .alt('我是图片')
  }

  @Builder
  textBuilder() {
    Text('我是文本')
  }
}

image.png

可以看到两个带有自定义组件被创建了,一个是搜索栏+图片,一个是搜索栏+文本。

构建方法的传参问题

@Builder装饰的方法虽然可以通过传参进行组件模块的定制,但是却会丢失响应式数据的特性,比如下面的例子:

import { promptAction } from '@kit.ArkUI'

@Entry
@Component
struct Index {
  @State testContent:string = '我是初始文本'

  build() {
    Column() {
      this.textBuilder(this.testContent)
      Button('点击修改文本')
        .onClick(()=>{
          this.testContent = '我是修改后的文本'
          promptAction.showDialog({
            message:'文本内容已被修改为:' + this.testContent
          })
        })
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  textBuilder(text:string) {
    Text(text)
  }
}

PixPin_2024-12-31_00-08-02.gif

可以看到Text组件并未重新渲染,内容也未刷新。我们可以看看官方对@Builder参数的说明:

image.png

因此我们想要保留响应式数据的特性,需要给构建方法传递单个参数,且该参数必须为对象的字面量:

import { promptAction } from '@kit.ArkUI'

interface BuilderParams {
  param1:string
}

@Entry
@Component
struct Index {
  @State testContent:string = '我是初始文本'

  build() {
    Column() {
      this.textBuilder({param1:this.testContent})
      Button('点击修改文本')
        .onClick(()=>{
          this.testContent = '我是修改后的文本'
          promptAction.showDialog({
            message:'文本内容已被修改为:' + this.testContent
          })
        })
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  textBuilder(params:BuilderParams) {
    Text(params.param1)
  }
}

PixPin_2024-12-31_00-18-25.gif