logo头像

学如逆水行舟

网格与瀑布流布局浅析

网格布局

网格布局从布局结构上看与栅格布局类似,其核心也是将页面分为指定的行和列,相比栅格布局,网格布局更加灵活,不仅可以进行列的合并,也可以对行进行合并。
网格布局的核心是提供行列定义模版,模板本身是一个字符串,行和列分别进行定义,字符串中的fr个数表示行或列的个数,fr前的数字表示此行或列对应的尺寸占比,例如下面的代码:

1
2
3
4
5
6
7
Grid() {
ForEach([1, 2, 3, 4, 5, 6, 7, 8, 9], (index: number)=>{
GridItem() {
Text(`数据${index}`).fontSize(30).textAlign(TextAlign.Center)
}.backgroundColor(Color.Red)
})
}.rowsTemplate('1fr 2fr 1fr').columnsTemplate('1fr 2fr 1fr').columnsGap(10).rowsGap(10)

上面代码中的’1fr 2fr 1fr’即是模版的定义,运行效果如图所示。
431.png

Grid在使用时,要注意有如下规则:
1.Grid容器的子组件必须为GridItem组件。
2.行列模板同时进行了设置时,将会展示固定行列个数的元素,整个容器不可滚动。
3.当只设置了行或列的模板时,元素按照设置的方向进行布局,超出的内容可以滚动。
4.当行和列的模版都没有设置时,其行列数由子元素的尺寸决定,整个容器不可滚动。
通过行列模版,我们定义的是规则的网格结构,Grid容器也支持定义不规则的网格结构,在构造Grid实例时,其有两个参数,分别为:
|参数|类型|意义|
|-|-|-|
|scroller| Scroller| 可滚动时,控制容器的滚动以及获取相关信息。|
|layoutOptions| GridLayoutOptions| 布局参数。|

layoutOptions布局参数中可以实现onGetRectByIndex配置项,此配置项需要设置为一个函数,用来返回指定索引数据的布局规则。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Grid(undefined, {
regularSize:[1, 1],
onGetRectByIndex: (index: number)=>{
// 注意此处的index从0开始,表示数据下标
if (index == 0) {
// 从第1行第1列开始,占1行3列
return [0, 0, 1, 3]
}
if (index == 3) {
// 从第2行第3列开始,占2行1列
return [1, 2, 2, 1]
}
if (index == 7) {
// 从第4行第2列开始,占1行2列
return [3, 1, 1, 2]
}
return undefined
}
}) {
ForEach([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], (index: number)=>{
GridItem() {
Text(`数据${index}`).fontSize(30).textAlign(TextAlign.Center)
}.backgroundColor(Color.Red)
})
}.rowsTemplate('1fr 1fr 1fr 1fr').columnsTemplate('1fr 1fr 1fr').columnsGap(10).rowsGap(10)

其中regularSize用来指定标准的子组件布局尺寸,目前只支持一个子组件占单行单列,此参数为后续扩展预留。onGetRectByIndex用来具体设置某个元素的位置和占用行列数,其设置的函数的参数index为元素索引,从0开始,此函数如果返回undefined则表示默认的布局规则,不做额外改变。我们也可以返回一个包含4个数值元素的数组,4个数值分别表示要布局子组件的:起始行索引,起始列索引,占用行数、占用列数。运行上面的代码,效果如图:
432.png
需要注意,在设置某个索引位置元素的布局规则时,需要符合布局逻辑,子组件将按照顺序进行布局,如果设置的位置已经被其他子组件占用,则设置无效。同样如果起始位置没有被占用但是其他位置被占用,则只会展示GridItem子组件的一部分。
关于使用Scroller对Grid容器的滚动进行控制,以及相关滚动事件的监听,其用法与List容器完全一致,这里就不再赘述。

瀑布流布局

瀑布流布局常用在展示宽度或高度不规则的信息流中,常见于电商或资讯类软件中。在ArkUI中使用WaterFlow创建瀑布流容器,WaterFlow支持横向滚动也支持纵向滚动,对于横向滚动的瀑布流,可以通过rowsTemplate方法设置行数与对应占比的模版。对于纵向滚动的瀑布流,可以通过columnsTemplate设置列数以及对应占比的模版。
纵向滚动的瀑布流在布局时,通常子元素的宽度固定,高度动态,当发生换行时,新的元素会被布局在高度最小的列下面,同理对于横向布局的瀑布流,子元素高度固定,宽度动态,发生换列时,新的元素会优先布局在宽度最小的行后面。我们若要手动实现这样的布局逻辑会非常复杂,使用WaterFlow则非常简单。例如:

1
2
3
4
5
6
7
8
9
10
WaterFlow() {
ForEach(this.data, (title: string)=>{
FlowItem() {
Text(title).textAlign(TextAlign.Center).fontSize(35)
}.width('100%')
.height(Math.max(Math.random() * 300, 50))
.backgroundColor(Color.Red)
})
}.rowsGap(10).columnsGap(10)
.layoutDirection(FlexDirection.Column).columnsTemplate('1fr 1fr')

433.png
将子组件的宽度改为随机,并设置WaterFlow的布局方向为横向,即可创建横向的瀑布流,如下:

1
2
3
4
5
6
7
8
9
10
WaterFlow() {
ForEach(this.data, (title: string)=>{
FlowItem() {
Text(title).textAlign(TextAlign.Center).fontSize(35)
}.height('100%')
.width(Math.max(Math.random() * 300, 50))
.backgroundColor(Color.Red)
})
}.rowsGap(10).columnsGap(10)
.layoutDirection(FlexDirection.Row).rowsTemplate('1fr 1fr 1fr 1fr')

434.png
WaterFlow的子组件必须时FlowItem,这种布局结构常用来渲染无限滚动的信息流,尤其是在有大量的图片信息时,由于图片的宽高比例不定,使用瀑布流布局效果会非常美观。在实际项目开发中,WaterFlow通常会和LazyForEach懒加载组合使用。
WaterFlow在构造时参数支持的配置项如下:
|配置项|类型|意义|
|-|-|-|
|footer| CustomBuilder| 设置尾视图。|
|scroller| Scroller| 滚动控制器。|
|sections| WaterFlowSections| 分组控制器。|
|layoutMode| WaterFlowLayoutMode| 瀑布流布局模式。|

WaterFlow也支持配置尾视图以及分组,通过分组允许不同的组采用不同的布局规则,通过sections参数,我们可以为不同的分组设置不同的行/列数,不同的间距等。例如下面的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Component
struct WaterFlowLayout {
data: string[] = (() => {
let d: string[] = []
for (let i = 0; i < 100; i++) {
d.push(`${i}`)
}
return d
})()

sections = new WaterFlowSections()

aboutToAppear(): void {
let sec1: SectionOptions = {
itemsCount: 4,
crossCount: 2,
columnsGap: 5,
rowsGap: 5,
margin: {top: 30}
}
let sec2: SectionOptions = {
itemsCount: 8,
crossCount: 4,
columnsGap: 10,
rowsGap: 10,
margin: {top: 30}
}

let sec3: SectionOptions = {
itemsCount: 88,
crossCount: 1,
rowsGap: 15,
margin: {top: 30}
}
let sections = [
sec1, sec2, sec3
]
this.sections.splice(0, 0, sections)
}

build() {
WaterFlow({sections: this.sections}) {
ForEach(this.data, (title: string)=>{
FlowItem() {
Text(title).textAlign(TextAlign.Center).fontSize(35)
}.width('100%')
.height(Math.max(Math.random() * 100, 50))
.backgroundColor(Color.Red)
})
}.rowsGap(10).columnsGap(10)
.layoutDirection(FlexDirection.Column).columnsTemplate('1fr 1fr')
}
}

其中WaterFlowSections可以理解为分组管理器,其中封装了方法来操作分组数据,如示例代码中的splice方法,此方法用来删除、替换或新增分组数据,第1个参数为要操作的分组索引起始位置,第2个参数为要删除的分组个数,第3个参数为添加一组分组数据。具体的分组数据由SectionOptions 类进行定义,此类中提供的配置属性如下:
|属性名| 类型| 意义|
|-|-|-|
|itemsCount| number| 当前分组中的元素个数。|
|crossCount| number| 轨数,即交叉轴方向上的行/列数。|
|columnsGap| Dimension| 设置列间距。|
|rowsGap| Dimension| 设置行间距。|
|margin |Margin | Dimension| 设置当前分组的外边距。|

WaterFlowSections实例操作分组的方法列举如下:
|方法名| 参数| 意义|
|-|-|-|
|splice| start: number deleteCount: number sections: Array| 从指定的索引开始删除多个分组,同时添加多个分组。|
|push| SectionOptions| 添加一个分组。|
|update| sectionIndex: number section: SectionOptions| 更新指定位置的分组。|
|values| 无| 获取当前所有分组数据。|
|length| 无| 获取当前分组个数。|

上面的分组示例代码运行效果如图所示。
435.png