ArkTs的容器布局

组件按照布局的要求依次排列,构成应用的页面。在声明式UI中,所有的页面都是由自定义组件构成,开发中可以根据自己的需求,选择合适的布局进行页面开发。

一. 如何选择布局

声明式UI提供了以下10种常见布局,开发者可根据实际应用场景选择合适的布局进行页面开发。

布局应用场景
线性布局(ROW、Column)如果布局内子元素超过1个时,且能够以某种方式线性排列时优先考虑此布局
层叠布局(Stack)组件需要有堆叠效果时优先考虑此布局。层叠布局的堆叠效果不会占用或影响其他同容器内子组件的布局空间。
弹性布局(Flex)弹性布局是与线性布局类似的布局方式。区别在于弹性布局默认能够使子组件压缩或拉伸。在子组件需要计算拉伸或压缩比例时优先使用此布局,可使得多个容器内子组件能够有更好的视觉上的填充效果
相对布局(RelativeContainer)

相对布局是在二维空间中的布局方式,不需要遵循线性布局的规则,布局方式更为自由。通过在子组件上设置锚点规则(AlignRules)使子组件能够将自己在横轴、纵轴中的位置对其。设置的锚点规则可以天然支持子元素压缩、拉伸、堆叠或形成多行效果。在页面元素分布复杂或通过线性布局会使容器嵌套层数过深时推荐使用。

栅格布局(GridRow、GridCol)栅格时多设备场景下通用的辅助定位工具,可将空间分割为有规律的栅格。栅格不同于网格布局固定的空间划分,可以实现不同设备下不同的布局,空间划分更随心所欲,从而显著降低适配不同屏幕尺寸的设计及开发成本,使得整体设计和开发流程更有秩序和节奏感,同时也保证多设备上应用显示的协调性和一致性,提升用户体验。推荐内容相同但布局不同时使用。
媒体查询(@ohos.mediaquery)媒体查询可根据不同设备类型或同设备不同状态修改应用的样式。例如根据设备和应用的不同属性信息设计不同属性信息设计不同的布局,以及屏幕发生动态改变时更新应用的页面布局
列表(List)使用列表可以高效地显示结构化、可滚动的信息。在ArkUI中,列表具有垂直和水平布局能力和自适应交叉轴方向上排列个数的布局能力,超出屏幕时可以滚动。列表适合用于呈现同类数据类型或数据类型集,例如图片和文本
网格(Grid)网格布局具有较强的页面均分能力、子元素占比控制能力。网格布局可以控制元素所占的网格数量、设置子元素横跨几行或者几列,当网格容器尺寸发生变化时,所有子元素以及间距等比例调整。推荐在需要按照固定比例或者均匀分配空间的布局场景下使用,例如计算器、相册、日历等。
轮播(Swiper)轮播组件通常用于实现广告轮播、图片预览等。
选项卡(Tabs)选项卡可以在一个页面内快速实现试图内容的切换,一方面提升查找信息的效率,另一方面精简用户单次获取到的信息量。

二. 布局的使用

1.线性布局(Row、Column)

线性布局是开发中最常用的布局,通过线性布局Row和Column构建。线性布局是其他布局的基础,其子元素在线性方向上(水平方向和垂直方向)依次排列。线性布局的排列方向由所选组件决定,Column容器内子元素按照垂直方向排列,Row容器内子元素按照水平方向排列。根据不同的排列方向,开发者可选择使用Row和Column容器创建线性布局。

(1).Row布局

Row容器内子元素按照水平方向排列。根据不同的排列方向

Row(){
        Text().width(50).height(50).backgroundColor('red')
        Text().width(50).height(50).backgroundColor('green')
        Text().width(50).height(50).backgroundColor('blue')
        Text().width(50).height(50).backgroundColor('#faf')
        Text().width(50).height(50).backgroundColor('#bdd')
      }
Row布局

(2)Column布局

Column容器内子元素按照垂直方向排列

Column(){
        Text().width('100%').height(50).backgroundColor('red')
        Text().width('100%').height(50).backgroundColor('green')
        Text().width('100%').height(50).backgroundColor('blue')
        Text().width('100%').height(50).backgroundColor('#faf')
        Text().width('100%').height(50).backgroundColor('#bdd')

    }
Column布局

(3).基本概念

布局容器:具有布局能力的容器组件、可以承载其他元素作为其子元素,布局容器会对其子元素进行尺寸计算和布局排列

布局子元素:布局容器内部的元素。

主轴:线性布局容器在布局方向上的轴线,子元素默认沿主轴排列。Row容器主轴为水平方向,Column容器主轴为垂直方向。

交叉轴:垂直于主轴方向的轴线。Row容器交叉轴为垂直方向,Colunn容器交叉轴为水平方向。

间距:布局子元素的间距。

(4)space属性

在布局容器内,可以通过space属性设置排列方向子元素的间距,使各子元素在排列方向上有等间距效果。

Row({space:25}){
        Text().width(50).height(50).backgroundColor('red')
        Text().width(50).height(50).backgroundColor('green')
        Text().width(50).height(50).backgroundColor('blue')
        Text().width(50).height(50).backgroundColor('#faf')
        Text().width(50).height(50).backgroundColor('#bdd')
      }
Row容器间距图

Column({space:25}){
        Text().width('100%').height(50).backgroundColor('red')
        Text().width('100%').height(50).backgroundColor('green')
        Text().width('100%').height(50).backgroundColor('blue')
        Text().width('100%').height(50).backgroundColor('#faf')
        Text().width('100%').height(50).backgroundColor('#bdd')

      }
Column容器间距图

(5)布局子元素在交叉轴上与主轴的对齐方式

在布局容器内,可以通过alignItems属性设置子元素在交叉轴(排列方向的垂直方向)上的对其方式。且在各类尺寸屏幕中,表现一致。其中,交叉轴为垂直方向时,取值为VerticalAlign类型,水平方向取值为HorizontalAlign类型。

alignSeIf属性用于控制单个子元素在容器交叉轴上的对齐方式,其优先级高于alignItems属性,如果设置了alignSelf属性,则在单个子元素上会覆盖alignItems属性。

在布局容器内,可以通过justifyContent属性设置子元素在容器主轴上的排列方式。可以从主轴起始位置开始排布,也可以从主轴结束位置开始排布,或者均匀分割主轴的空间。

Column布局子元素在交叉轴对齐方式
属性说明
HorizontalAlign.Start子元素在水平方向左对齐。
HorizontalAlign.Center子元素在水平方向居中对齐。
HorizontalAlign.End子元素在水平方向右对齐。
 Column(){
        Text().width(50).height(50).backgroundColor('red')
        Text().width(50).height(50).backgroundColor('green')
        Text().width(50).height(50).backgroundColor('blue')
        Text().width(50).height(50).backgroundColor('#faf')
        Text().width(50).height(50).backgroundColor('#bdd')
      }
      .width('100%')
      .height('100%')
      .alignItems(HorizontalAlign.Start)

HorizontalAlign.Start

Column(){
        Text().width(50).height(50).backgroundColor('red')
        Text().width(50).height(50).backgroundColor('green')
        Text().width(50).height(50).backgroundColor('blue')
        Text().width(50).height(50).backgroundColor('#faf')
        Text().width(50).height(50).backgroundColor('#bdd')
      }
      .width('100%')
      .height('100%')
      .alignItems(HorizontalAlign.Center)
HorizontalAlign.Center

Column(){
        Text().width(50).height(50).backgroundColor('red')
        Text().width(50).height(50).backgroundColor('green')
        Text().width(50).height(50).backgroundColor('blue')
        Text().width(50).height(50).backgroundColor('#faf')
        Text().width(50).height(50).backgroundColor('#bdd')
      }
      .width('100%')
      .height('100%')
      .alignItems(HorizontalAlign.End)
HorizontalAlign.End

Column布局子元素在主轴排列方式
属性说明
justifyContent(FlexAlign.Start)元素在垂直方向首端对齐,第一个元素与行首对齐,同时后续的元素与前一个对齐。
justifyContent(FlexAlign.Center)元素在垂直方向中心对齐,第一个元素与行首的距离与最后一个元素与行尾距离相同。
justifyContent(FlexAlign.End)元素在垂直方向尾部对齐,最后一个元素与行尾对齐,其他元素与后一个对齐
justifyContent(FlexAlign.SpaceBetween)垂直方向均匀分配元素,相邻元素之间距离相同。第一个元素与行首对齐,最后一个元素与行尾对齐。
justifyContent(FlexAlign.SpaceAround)垂直方向均匀分配元素,相邻元素之间距离相同。第一个元素到行首的距离和最后一个元素到行尾的距离是相邻元素之间距离的一半。
justifyContent(FlexAlign.SpaceEvenly)垂直方向均匀分配元素,相邻元素之间的距离、第一个元素与行首的间距、最后一个元素到行尾的间距都完全一样。
Column(){
        Text().width('100%').height(50).backgroundColor('red')
        Text().width('100%').height(50).backgroundColor('green')
        Text().width('100%').height(50).backgroundColor('blue')
        Text().width('100%').height(50).backgroundColor('#faf')
        Text().width('100%').height(50).backgroundColor('#bdd')
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.Start)
justifyContent(FlexAlign.Start)

Column(){
        Text().width('100%').height(50).backgroundColor('red')
        Text().width('100%').height(50).backgroundColor('green')
        Text().width('100%').height(50).backgroundColor('blue')
        Text().width('100%').height(50).backgroundColor('#faf')
        Text().width('100%').height(50).backgroundColor('#bdd')
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.Center)
justifyContent(FlexAlign.Center)

Column(){
        Text().width('100%').height(50).backgroundColor('red')
        Text().width('100%').height(50).backgroundColor('green')
        Text().width('100%').height(50).backgroundColor('blue')
        Text().width('100%').height(50).backgroundColor('#faf')
        Text().width('100%').height(50).backgroundColor('#bdd')
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.End)
justifyContent(FlexAlign.End)
Column(){
        Text().width('100%').height(50).backgroundColor('red')
        Text().width('100%').height(50).backgroundColor('green')
        Text().width('100%').height(50).backgroundColor('blue')
        Text().width('100%').height(50).backgroundColor('#faf')
        Text().width('100%').height(50).backgroundColor('#bdd')
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.SpaceBetween)
justifyContent(FlexAlign.SpaceBetween)

Column(){
        Text().width('100%').height(50).backgroundColor('red')
        Text().width('100%').height(50).backgroundColor('green')
        Text().width('100%').height(50).backgroundColor('blue')
        Text().width('100%').height(50).backgroundColor('#faf')
        Text().width('100%').height(50).backgroundColor('#bdd')
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.SpaceAround)

justifyContent(FlexAlign.SpaceAround)

Column(){
        Text().width('100%').height(50).backgroundColor('red')
        Text().width('100%').height(50).backgroundColor('green')
        Text().width('100%').height(50).backgroundColor('blue')
        Text().width('100%').height(50).backgroundColor('#faf')
        Text().width('100%').height(50).backgroundColor('#bdd')
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.SpaceEvenly)
justifyContent(FlexAlign.SpaceEvenly)

Row容器内子元素在垂直方向上的排列
属性说明
VerticalAlign.Top子元素在垂直方向顶部对齐。
VerticalAlign.Center子元素在垂直方向居中对齐
VerticalAlign.Bottom子元素在垂直方向底部对齐
Row(){
        Text().width(50).height(50).backgroundColor('red')
        Text().width(50).height(50).backgroundColor('green')
        Text().width(50).height(50).backgroundColor('blue')
        Text().width(50).height(50).backgroundColor('#faf')
        Text().width(50).height(50).backgroundColor('#bdd')
      }
      .width('100%')
      .height('100%')
      .alignItems(VerticalAlign.Top)

alignItems(VerticalAlign.Top)

 Row(){
        Text().width(50).height(50).backgroundColor('red')
        Text().width(50).height(50).backgroundColor('green')
        Text().width(50).height(50).backgroundColor('blue')
        Text().width(50).height(50).backgroundColor('#faf')
        Text().width(50).height(50).backgroundColor('#bdd')
      }
      .width('100%')
      .height('100%')
      .alignItems(VerticalAlign.Center)
alignItems(VerticalAlign.Center)

Row(){
        Text().width(50).height(50).backgroundColor('red')
        Text().width(50).height(50).backgroundColor('green')
        Text().width(50).height(50).backgroundColor('blue')
        Text().width(50).height(50).backgroundColor('#faf')
        Text().width(50).height(50).backgroundColor('#bdd')
      }
      .width('100%')
      .height('100%')
      .alignItems(VerticalAlign.Bottom)
alignItems(VerticalAlign.Bottom)

Row容器内子元素在水平方向上的排列
属性说明
justifyContent(FlexAlign.Start)元素在水平方向首端对齐,第一个元素与行首对齐,同时后续的元素与前一个对齐。
justifyContent(FlexAlign.Center)元素在水平方向中心对齐,第一个元素与行首的距离与最后一个元素与行尾距离相同。
justifyContent(FlexAlign.End)元素在水平方向尾部对齐,最后一个元素与行尾对齐,其他元素与后一个对齐。
justifyContent(FlexAlign.SpaceBetween)水平方向均匀分配元素,相邻元素之间距离相同。第一个元素与行首对齐,最后一个元素与行尾对齐
justifyContent(FlexAlign.SpaceAround)水平方向均匀分配元素,相邻元素之间距离相同。第一个元素到行首的距离和最后一个元素到行尾的距离是相邻元素之间距离的一半。
justifyContent(FlexAlign.SpaceEvenly)水平方向均匀分配元素,相邻元素之间的距离、第一个元素与行首的间距、最后一个元素到行尾的间距都完全一样。
Row(){
        Text().width(50).height(50).backgroundColor('red')
        Text().width(50).height(50).backgroundColor('green')
        Text().width(50).height(50).backgroundColor('blue')
        Text().width(50).height(50).backgroundColor('#faf')
        Text().width(50).height(50).backgroundColor('#bdd')
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.Start)
justifyContent(FlexAlign.Start)

 Row(){
        Text().width(50).height(50).backgroundColor('red')
        Text().width(50).height(50).backgroundColor('green')
        Text().width(50).height(50).backgroundColor('blue')
        Text().width(50).height(50).backgroundColor('#faf')
        Text().width(50).height(50).backgroundColor('#bdd')
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.Center)

justifyContent(FlexAlign.Center)

Row(){
        Text().width(50).height(50).backgroundColor('red')
        Text().width(50).height(50).backgroundColor('green')
        Text().width(50).height(50).backgroundColor('blue')
        Text().width(50).height(50).backgroundColor('#faf')
        Text().width(50).height(50).backgroundColor('#bdd')
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.End)
justifyContent(FlexAlign.End)

Row(){
        Text().width(50).height(50).backgroundColor('red')
        Text().width(50).height(50).backgroundColor('green')
        Text().width(50).height(50).backgroundColor('blue')
        Text().width(50).height(50).backgroundColor('#faf')
        Text().width(50).height(50).backgroundColor('#bdd')
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.SpaceBetween)
justifyContent(FlexAlign.SpaceBetween)

 Row(){
        Text().width(50).height(50).backgroundColor('red')
        Text().width(50).height(50).backgroundColor('green')
        Text().width(50).height(50).backgroundColor('blue')
        Text().width(50).height(50).backgroundColor('#faf')
        Text().width(50).height(50).backgroundColor('#bdd')
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.SpaceAround)
justifyContent(FlexAlign.SpaceAround)

Row(){
        Text().width(50).height(50).backgroundColor('red')
        Text().width(50).height(50).backgroundColor('green')
        Text().width(50).height(50).backgroundColor('blue')
        Text().width(50).height(50).backgroundColor('#faf')
        Text().width(50).height(50).backgroundColor('#bdd')
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.SpaceEvenly)
justifyContent(FlexAlign.SpaceEvenly)

(6)自适应拉伸

在线性布局下,常用空白填充组件Blank,在容器主轴方向自动填充空白空间,达到自适应拉伸效果。Row和Column作为容器,只需要添加宽高为百分比,当屏幕宽高发生变化时,会产生自适应效果。

Row(){
        Text().width(50).height(50).backgroundColor('red')
        Blank()
        Text().width(50).height(50).backgroundColor('green')
      }
      .width('100%')
blank:自适应拉伸

(7)自适应缩放

自适应缩放是指子元素随容器尺寸的变化而按照预设的比例自动调整尺寸,适应各种不同大小的设备。在线性布局中,可以使用以下两种方法实现自适应缩放。

  • 父容器尺寸确定时,使用layoutWeight属性设置子元素和兄弟元素在主轴上的权重,忽略元素本身尺寸设置,使它们在任意尺寸的设备下自适应占满剩余空间。

Column(){
          Text().width(50).height(50).backgroundColor('red')
            
          Text().width(50).backgroundColor('green').layoutWeight(1)
      }
      .width('100%')
      .height('100%')
layoutWeight:自适应缩放

(8)自适应延伸

自适应延伸是指在不同尺寸设备下,当页面的内容超出屏幕大小而无法完全显示时,可以通过滚动条进行拖动展示。这种方法适用于线性布局中内容无法一屏展示的场景。通常有以下两种实现方式。

  • 在List中添加滚动条:当List子项过多一屏放不下时,可以将每一项子元素放置在不同的组件中,通过滚动条进行拖动展示。可以通过scrollBar属性设置滚动条的常驻状态,edgeEffect属性设置拖动到内容最末端的回弹效果。

  • 使用Scroll组件:在线性布局中,开发者可以进行垂直方向或者水平方向的布局。当一屏无法完全显示时,可以在Column或Row组件的外层包裹一个可滚动的容器组件Scroll来实现可滑动的线性布局。

 Scroll(){
      Column(){
        Text().width('100%').height(300).backgroundColor('red')
        Text().width('100%').height(300).backgroundColor('green')
        Text().width('100%').height(300).backgroundColor('blue')
        Text().width('100%').height(300).backgroundColor(Color.Orange)
        Text().width('100%').height(300).backgroundColor(Color.Pink)
        Text().width('100%').height(300).backgroundColor(Color.Yellow)
      }
    }
    .height('100%')
    .width('100%')

Scroll组件属性
属性说明
scrollable(ScrollDirection.Vertical)滚动方向为垂直方向
scrollBar(BarState.On)滚动条常驻显示
scrollBarColor(Color.Gray)滚动条颜色
scrollBarWidth(10)滚动条宽度
edgeEffect(EdgeEffect.Spring)滚动到边沿后回弹

 2. 层叠布局(Stack)

层叠布局用于在屏幕上预留一块区域来显示组件中的元素,提供元素可以重叠的布局。层叠布局通过Stack容器组件实现位置的固定定位与层叠,容器中的子元素依次入栈,后一个子元素覆盖前一个子元素,子元素就可以叠加,也可以设置位置。

层叠布局具有较强的页面层叠、位置定位能力,其使用场景有广告、卡片层叠效果等。

Stack(){
      Text().width(200).height(200).backgroundColor('red')
      Text().width(150).height(150).backgroundColor('blue')
      Text().width(100).height(100).backgroundColor('green')
    }
    .height('100%')
    .width('100%')

Stack

(1)Stack对齐方式

Stack组件通过alignContent参数实现位置的相对移动,支持九种对齐方式。

属性说明
TopStart左上
Top正上
TopEnd右上
Start正中间左边
Center中间
End正中间右边
BottomStart下左
Bottom正下
BottomEnd下右

(2)Z序控制(ZIndex)

Stack容器中兄弟组件显示层级关系可以通过Z序控制的ZIndex属性改变。ZIndex值越大,显示层级越高,即ZIndex值大的组件会覆盖在ZIndex值小的组件上方。

在层叠布局中,如果后面子元素尺寸大于前面子元素尺寸,则前面子元素完全隐藏。

Stack(){
      Text().width(200).height(200).backgroundColor('red').zIndex(1)
      Text().width(150).height(150).backgroundColor('blue')
      Text().width(100).height(100).backgroundColor('green')
    }
    .height('100%')
    .width('100%')
层级ZIndex

3.弹性布局(FLex)

弹性布局(Flex)提供更加有效的方式对容器中的子元素进行排列、对齐和分配剩余空间。常用于页面头部导航栏的均匀分布、页面框架的搭建、多行数据的排列等。

容器默认存在主轴与交叉轴,子元素默认沿主轴排列,子元素在主轴方向的尺寸称为主轴尺寸,在交叉轴方向的尺寸成为交叉轴尺寸。

(1)基本概念

主轴:Flex组件布局方向的轴线,子元素默认沿着主轴排列。主轴开始的位置称为主轴起始点,结束位置称为主轴结束点。

交叉轴:垂直于主轴方向的轴线。交叉轴开始的位置成为交叉轴起始点,结束位置成为交叉轴结束点。

(2)布局方向

在弹性布局中,容器的子元素可以按照任意方向排列。通过设置参数direction,可以决定主轴的方向,从而控制子元素的排列方向。

Flex布局方向
属性说明
FlexDirection.Row(默认值)主轴为水平方向,子元素从起始端沿着水平方向开始排布。
FlexDirection.RowReverse主轴为水平方向,子元素从终点端沿着FlexDirection. Row相反的方向开始排布。
FlexDirection.Column主轴为垂直方向,子元素从起始端沿着垂直方向开始排布
FlexDirection.ColumnReverse主轴为垂直方向,子元素从终点端沿着FlexDirection. Column相反的方向开始排布。
Flex(){
      Text('1').width('33%').height(50).backgroundColor(0xF5DEB3)
      Text('2').width('33%').height(50).backgroundColor(0xD2B48C)
      Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
    }
    .height('100%')
    .width('100%')
默认布局方向

 Flex({direction:FlexDirection.Column}){
      Text('1').width('33%').height(50).backgroundColor(0xF5DEB3)
      Text('2').width('33%').height(50).backgroundColor(0xD2B48C)
      Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
    }
    .height('100%')
    .width('100%')
FlexDirection.Column

Flex({direction:FlexDirection.RowReverse}){
      Text('1').width('33%').height(50).backgroundColor(0xF5DEB3)
      Text('2').width('33%').height(50).backgroundColor(0xD2B48C)
      Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
    }
    .height('100%')
    .width('100%')
FlexDirection.RowReverse
 Flex({direction:FlexDirection.ColumnReverse}){
      Text('1').width('33%').height(50).backgroundColor(0xF5DEB3)
      Text('2').width('33%').height(50).backgroundColor(0xD2B48C)
      Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
    }
    .height('100%')
    .width('100%')
FlexDirection.ColumnReverse

(3)布局换行

弹性布局分为单行布局和多行布局。默认情况下,Flex容器中的子元素都排在一条线(又称“轴线”)上。wrap属性控制当子元素主轴尺寸之和大于容器主轴尺寸时,Flex是单行布局还是多行布局。在多行布局时,通过交叉轴方向,确认新行排列方向。

布局换行
属性说明
FlexWrap. NoWrap(默认值)不换行。如果子元素的宽度总和大于父元素的宽度,则子元素会被压缩宽度。
FlexWrap. Wrap换行,每一行子元素按照主轴方向排列。
FlexWrap. WrapReverse换行,每一行子元素按照主轴反方向排列。
Flex({wrap:FlexWrap.Wrap}){
      Text('1').width('50%').height(50).backgroundColor(0xF5DEB3)
      Text('2').width('50%').height(50).backgroundColor(0xD2B48C)
      Text('3').width('50%').height(50).backgroundColor(0xF5DEB3)
    }
    .height('100%')
    .width('100%')
FlexWrap.Wrap
 Flex({wrap:FlexWrap.NoWrap}){
      Text('1').width('50%').height(50).backgroundColor(0xF5DEB3)
      Text('2').width('50%').height(50).backgroundColor(0xD2B48C)
      Text('3').width('50%').height(50).backgroundColor(0xF5DEB3)
    }
    .height('100%')
    .width('100%')
wrap:FlexWrap.NoWrap
Flex({wrap:FlexWrap.WrapReverse}){
      Text('1').width('50%').height(50).backgroundColor(0xF5DEB3)
      Text('2').width('50%').height(50).backgroundColor(0xD2B48C)
      Text('3').width('50%').height(50).backgroundColor(0xF5DEB3)
    }
    .height('100%')
    .width('100%')
FlexWrap.WrapReverse

(4)主轴对齐方式

通过justifyContent参数设置子元素在主轴方向的对齐方式。

Flex主轴对齐方式
属性说明
FlexAlign.Start(默认值)子元素在主轴方向起始端对齐, 第一个子元素与父元素边沿对齐,其他元素与前一个元素对齐。
FlexAlign.Center子元素在主轴方向居中对齐
FlexAlign.End子元素在主轴方向终点端对齐, 最后一个子元素与父元素边沿对齐,其他元素与后一个元素对齐。
FlexAlign.SpaceBetweenFlex主轴方向均匀分配弹性元素,相邻子元素之间距离相同。第一个子元素和最后一个子元素与父元素边沿对齐
FlexAlign.SpaceAroundFlex主轴方向均匀分配弹性元素,相邻子元素之间距离相同。第一个子元素到主轴起始端的距离和最后一个子元素到主轴终点端的距离是相邻元素之间距离的一半。
FlexAlign.SpaceEvenlyFlex主轴方向元素等间距布局,相邻子元素之间的间距、第一个子元素与主轴起始端的间距、最后一个子元素到主轴终点端的间距均相等。
 Flex({justifyContent:FlexAlign.Start}){
      Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
      Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
      Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
    }
    .height('100%')
    .width('100%')
FlexAlign.Start

Flex({justifyContent:FlexAlign.Center}){
      Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
      Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
      Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
    }
    .height('100%')
    .width('100%')
FlexAlign.Center
Flex({justifyContent:FlexAlign.End}){
      Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
      Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
      Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
    }
    .height('100%')
    .width('100%')
FlexAlign.End
Flex({justifyContent:FlexAlign.SpaceBetween}){
      Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
      Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
      Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
    }
    .height('100%')
    .width('100%')
FlexAlign.SpaceBetween

Flex({justifyContent:FlexAlign.SpaceAround}){
      Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
      Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
      Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
    }
    .height('100%')
    .width('100%')
FlexAlign.SpaceAround
Flex({justifyContent:FlexAlign.SpaceEvenly}){
      Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
      Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
      Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
    }
    .height('100%')
    .width('100%')

FlexAlign.SpaceEvenly

(5)交叉轴对齐方式

容器和子元素都可以设置交叉轴对齐方式,且子元素设置的对齐方式优先级较高。

可以通过Flex组件的alignItems参数设置子元素在交叉轴的对齐方式。

容器组件设置交叉轴对齐
属性说明
ItemAlign.Auto使用Flex容器中默认配置
ItemAlign.Start交叉轴方向首部对齐。
ItemAlign.Center交叉轴方向居中对齐。
ItemAlign.End交叉轴方向底部对齐。
ItemAlign.Stretch交叉轴方向拉伸填充,在未设置尺寸时,拉伸到容器尺寸
ItemAlign. Baseline交叉轴方向文本基线对齐
Flex({ alignItems: ItemAlign.Auto }){
      Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
      Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
      Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
    }
    .height('100%')
    .width('100%')
alignItems: ItemAlign.Auto

 Flex({ alignItems: ItemAlign.Start }){
      Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
      Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
      Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
    }
    .height('100%')
    .width('100%')
ItemAlign.Start

 Flex({ alignItems: ItemAlign.Center }){
      Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
      Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
      Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
    }
    .height('100%')
    .width('100%')
ItemAlign.Center

Flex({ alignItems: ItemAlign.End }){
      Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
      Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
      Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
    }
    .height('100%')
    .width('100%')
ItemAlign.End

Flex({ alignItems: ItemAlign.Baseline}){
      Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
      Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
      Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
    }
    .height('100%')
    .width('100%')
ItemAlign.Baseline
 Flex({ alignItems: ItemAlign.Stretch}){
      Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
      Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
      Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
    }
    .height('100%')
    .width('100%')
ItemAlign.Stretch

(6)子元素设置交叉轴对齐

子元素的alignSelf属性也可以设置子元素在父容器交叉轴的对齐格式,且会覆盖Flex布局容器中alignItems配置

Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center }) { // 容器组件设置子元素居中
      Text().width('25%').height(80)
        .alignSelf(ItemAlign.Start)
        .backgroundColor(0xF5DEB3)
      Text()
        .alignSelf(ItemAlign.Baseline)
        .width('25%')
        .height(80)
        .backgroundColor(0xD2B48C)
      Text().width('25%').height(100)
        .backgroundColor(0xF5DEB3)
        .alignSelf(ItemAlign.Baseline)
      Text().width('25%').height(100)
        .backgroundColor(0xD2B48C)
      Text().width('25%').height(100)
        .backgroundColor(0xF5DEB3)

    }.width('90%').height(220).backgroundColor(0xAFEEEE)

Flex容器中alignItems设置交叉轴子元素的对齐方式为居中,子元素自身设置了alignSelf属性的情况,覆盖父组件的alignItems值,表现为alignSelf的定义。

 (7)内容对齐

可以通过alignContent参数设置子元素各行在交叉轴剩余空间内的对齐方式,只在多行的Flex布局中生效

内容对齐
属性说明
FlexAlign.Start子元素各行与交叉轴起点对齐。
FlexAlign.Center子元素各行在交叉轴方向居中对齐。
FlexAlign.End子元素各行与交叉轴终点对齐。
FlexAlign.SpaceBetween子元素各行与交叉轴两端对齐,各行间垂直间距平均分布。
FlexAlign.SpaceAround子元素各行间距相等,是元素首尾行与交叉轴两端距离的两倍
FlexAlign.SpaceEvenly子元素各行间距,子元素首尾行与交叉轴两端距离都相等
 Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.Start }) {
      Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
      Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
      Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
      Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
      Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
    }
    .width('90%')
    .height(100)
FlexAlign.Start
 Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, 
      alignContent: FlexAlign.Center }) {
      Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
      Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
      Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
      Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
      Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
    }
    .width('90%')
    .height(100)
FlexAlign.Center
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap,
      alignContent: FlexAlign.End }) {
      Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
      Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
      Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
      Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
      Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
    }
    .width('90%')
    .height(100)
FlexAlign.End
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap,
      alignContent: FlexAlign.SpaceBetween }) {
      Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
      Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
      Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
      Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
      Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
    }
    .width('90%')
    .height(100)
FlexAlign.SpaceBetween
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap,
      alignContent: FlexAlign.SpaceAround }) {
      Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
      Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
      Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
      Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
      Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
    }
    .width('90%')
    .height(100)
FlexAlign.SpaceAround

 Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap,
      alignContent: FlexAlign.SpaceEvenly }) {
      Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
      Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
      Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
      Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
      Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
    }
    .width('90%')
    .height(100)
FlexAlign.SpaceEvenly

(8)自适应拉伸

在弹性布局父组件尺寸过小时,通过子元素的以下属性设置其在父容器的占比,达到自适应布局

自适应拉伸
属性说明
flexBasis设置子元素在父容器主轴方向上的基准尺寸。如果设置了该属性,则子项占用的空间为该属性所设置的值;如果没设置该属性,那子项的空间为width/height的值。
flexGrow设置父容器的剩余空间分配给此属性所在组件的比例。用于分配父组件的剩余空间。
flexShrink当父容器空间不足时,子元素的压缩比例
Flex({
         direction:FlexDirection.Row,justifyContent:FlexAlign.Center,
         alignItems:ItemAlign.Stretch
       }){ //从左到右排列
         Text('子元素1')
           .width(100)
           .flexBasis(100)
           .border({width:1,color:'red',style:BorderStyle.Solid})
         Text('子元素2')
           .height(50)
           .width(210)
           .flexBasis('auto')
           .border({width:1,color:'red',style:BorderStyle.Solid})
         Text('子元素3')
           .height(50)
           .width(100)
           .flexBasis('auto')
           .border({width:1,color:'red',style:BorderStyle.Solid})

       }
       .height(100)
       .width('100%')
       .backgroundColor('gray')
flexBasis
Flex() {
  Text('flexGrow(2)')
    .flexGrow(2)
    .width(100)
    .height(100)
    .backgroundColor(0xF5DEB3)
  Text('flexGrow(3)')
    .flexGrow(3)
    .width(100)
    .height(100)
    .backgroundColor(0xD2B48C)

  Text('no flexGrow')
    .width(100)
    .height(100)
    .backgroundColor(0xF5DEB3)
}.width(420).height(120).padding(10).backgroundColor(0xAFEEEE)
flexGrow
Flex({ direction: FlexDirection.Row }) {
  Text('flexShrink(3)')
    .flexShrink(3)
    .width(200)
    .height(100)
    .backgroundColor(0xF5DEB3)
  
  Text('no flexShrink')
    .width(200)
    .height(100)
    .backgroundColor(0xD2B48C)

  Text('flexShrink(2)')
    .flexShrink(2)
    .width(200)
    .height(100)
    .backgroundColor(0xF5DEB3)  
}.width(400).height(120).padding(10).backgroundColor(0xAFEEEE) 
flexShrink

4.相对布局(RelativeContainer)

在应用的开发过程中,经常需要设计复杂界面,此时涉及到多个相同或不同组件之间的嵌套。如果布局组件嵌套深度过深,或者嵌套组件数过多,会带来额外的开销。如果在布局的方式上进行优化,就可以有效的提升性能,减少时间开销。

RelativeContainer为采用相对布局的容器,支持容器内部的子元素设置相对位置关系,适用于界面复杂场景的情况,对多个子组件进行对齐和排列。子元素支持指定兄弟元素作为锚点,也支持指定父容器作为锚点,基于锚点做相对位置布局。下图是一个RelativeContainer的概念图,图中的虚线表示位置的依赖关系。

(1)基本概念

  • 锚点:通过锚点设置当前元素基于哪个元素确定位置。

  • 对齐方式:通过对齐方式,设置当前元素是基于锚点的上中下对齐,还是基于锚点的左中右对齐。

(2)锚点设置

锚点设置是指设置子元素相对于父元素或兄弟元素的位置依赖关系。在水平方向上,可以设置left、middle、right的锚点。在竖直方向上,可以设置top、center、bottom的锚点。

为了明确定义锚点,必须为RelativeContainer及其子元素设置ID,用于指定锚点信息。ID默认为“__container__”,其余子元素的ID通过id属性设置。不设置id的组件能显示,但是不能被其他子组件作为锚点,相对布局容器会为其拼接id,此id的规律无法被应用感知。互相依赖,环形依赖时容器内子组件全部不绘制。同方向上两个以上位置设置锚点,但锚点位置逆序时此子组件大小为0,即不绘制。

说明

在使用锚点时要注意子元素的相对位置关系,避免出现错位或遮挡的情况。

RelativeContainer父组件为锚点,__container__代表父容器的ID。

let AlignRus:Record<string,Record<string,string|VerticalAlign|HorizontalAlign>> = {
  'top': { 'anchor': '__container__', 'align': VerticalAlign.Top },
  'left': { 'anchor': '__container__', 'align': HorizontalAlign.Start }
}
let AlignRue:Record<string,Record<string,string|VerticalAlign|HorizontalAlign>> = {
  'top': { 'anchor': '__container__', 'align': VerticalAlign.Top },
  'right': { 'anchor': '__container__', 'align': HorizontalAlign.End }
}
let Mleft:Record<string,number> = { 'left': 20 }
let BWC:Record<string,number|string> = { 'width': 2, 'color': '#6699FF' }

@Entry
@Component
struct Index {
  build() {
    RelativeContainer() {
      Row(){Text('row1')}.justifyContent(FlexAlign.Center).width(100).height(100)
      .backgroundColor("#FF3333")
      .alignRules(AlignRus)
      .id("row1")

      Row(){Text('row2')}.justifyContent(FlexAlign.Center).width(100).height(100)
      .backgroundColor("#FFCC00")
      .alignRules(AlignRue)
      .id("row2")
    }.width(300).height(300)
    .margin(Mleft)
    .border(BWC)
  }
}

 以兄弟元素为锚点

let AlignRus:Record<string,Record<string,string|VerticalAlign|HorizontalAlign>> = {
  'top': { 'anchor': '__container__', 'align': VerticalAlign.Top },
  'left': { 'anchor': '__container__', 'align': HorizontalAlign.Start }
}
let RelConB:Record<string,Record<string,string|VerticalAlign|HorizontalAlign>> = {
  'top': { 'anchor': 'row1', 'align': VerticalAlign.Bottom },
  'left' : { 'anchor': 'row1', 'align': HorizontalAlign.Start }
}
let Mleft:Record<string,number> = { 'left': 20 }
let BWC:Record<string,number|string> = { 'width': 2, 'color': '#6699FF' }

@Entry
@Component
struct Index {
  build() {
    RelativeContainer() {
      Row(){Text('row1')}.justifyContent(FlexAlign.Center).width(100).height(100)
      .backgroundColor("#FF3333")
      .alignRules(AlignRus)
      .id("row1")

      Row(){Text('row2')}.justifyContent(FlexAlign.Center).width(100).height(100)
      .backgroundColor("#FFCC00")
      .alignRules(RelConB)
      .id("row2")
    }.width(300).height(300)
    .margin(Mleft)
    .border(BWC)
  }
}

子组件锚点可以任意选择,但需注意不要相互依赖。

@Entry
@Component
struct Index {
  build() {
    Row() {
      RelativeContainer() {
        Row(){Text('row1')}.justifyContent(FlexAlign.Center).width(100).height(100)
          .backgroundColor('#ff3339ff')
          .alignRules({
            top: {anchor: "__container__", align: VerticalAlign.Top},
            left: {anchor: "__container__", align: HorizontalAlign.Start}
          })
          .id("row1")

        Row(){Text('row2')}.justifyContent(FlexAlign.Center).width(100)
          .backgroundColor('#ff298e1e')
          .alignRules({
            top: {anchor: "__container__", align: VerticalAlign.Top},
            right: {anchor: "__container__", align: HorizontalAlign.End},
            bottom: {anchor: "row1", align: VerticalAlign.Center},
          })
          .id("row2")

        Row(){Text('row3')}.justifyContent(FlexAlign.Center).height(100)
          .backgroundColor('#ffff6a33')
          .alignRules({
            top: {anchor: "row1", align: VerticalAlign.Bottom},
            left: {anchor: "row1", align: HorizontalAlign.Start},
            right: {anchor: "row2", align: HorizontalAlign.Start}
          })
          .id("row3")

        Row(){Text('row4')}.justifyContent(FlexAlign.Center)
          .backgroundColor('#ffff33fd')
          .alignRules({
            top: {anchor: "row3", align: VerticalAlign.Bottom},
            left: {anchor: "row1", align: HorizontalAlign.Center},
            right: {anchor: "row2", align: HorizontalAlign.End},
            bottom: {anchor: "__container__", align: VerticalAlign.Bottom}
          })
          .id("row4")
      }
      .width(300).height(300)
      .margin({left: 50})
      .border({width:2, color: "#6699FF"})
    }
    .height('100%')
  }
}

(3)设置相对于锚点的对齐位置

设置了锚点之后,可以通过align设置相对于锚点的对齐位置。

在水平方向上,对齐位置可以设置为HorizontalAlign.Start、HorizontalAlign.Center、HorizontalAlign.End。

在竖直方向上,对齐位置可以设置为VerticalAlign.Top、VerticalAlign.Center、VerticalAlign.Bottom。

(4)子组件位置偏移

子组件经过相对位置对齐后,位置可能还不是目标位置,开发者可根据需要进行额外偏移设置offset。

@Entry
@Component
struct Index {
  build() {
    Row() {
      RelativeContainer() {
        Row(){Text('row1')}.justifyContent(FlexAlign.Center).width(100).height(100)
          .backgroundColor("#FF3333")
          .alignRules({
            top: {anchor: "__container__", align: VerticalAlign.Top},
            left: {anchor: "__container__", align: HorizontalAlign.Start}
          })
          .id("row1")

        Row(){Text('row2')}.justifyContent(FlexAlign.Center).width(100)
          .backgroundColor("#FFCC00")
          .alignRules({
            top: {anchor: "__container__", align: VerticalAlign.Top},
            right: {anchor: "__container__", align: HorizontalAlign.End},
            bottom: {anchor: "row1", align: VerticalAlign.Center},
          })
          .offset({
            x:-40,
            y:-20
          })
          .id("row2")

        Row(){Text('row3')}.justifyContent(FlexAlign.Center).height(100)
          .backgroundColor("#FF6633")
          .alignRules({
            top: {anchor: "row1", align: VerticalAlign.Bottom},
            left: {anchor: "row1", align: HorizontalAlign.End},
            right: {anchor: "row2", align: HorizontalAlign.Start}
          })
          .offset({
            x:-10,
            y:-20
          })
          .id("row3")

        Row(){Text('row4')}.justifyContent(FlexAlign.Center)
          .backgroundColor("#FF9966")
          .alignRules({
            top: {anchor: "row3", align: VerticalAlign.Bottom},
            bottom: {anchor: "__container__", align: VerticalAlign.Bottom},
            left: {anchor: "__container__", align: HorizontalAlign.Start},
            right: {anchor: "row1", align: HorizontalAlign.End}
          })
          .offset({
            x:-10,
            y:-30
          })
          .id("row4")

        Row(){Text('row5')}.justifyContent(FlexAlign.Center)
          .backgroundColor("#FF66FF")
          .alignRules({
            top: {anchor: "row3", align: VerticalAlign.Bottom},
            bottom: {anchor: "__container__", align: VerticalAlign.Bottom},
            left: {anchor: "row2", align: HorizontalAlign.Start},
            right: {anchor: "row2", align: HorizontalAlign.End}
          })
          .offset({
            x:10,
            y:20
          })
          .id("row5")

        Row(){Text('row6')}.justifyContent(FlexAlign.Center)
          .backgroundColor('#ff33ffb5')
          .alignRules({
            top: {anchor: "row3", align: VerticalAlign.Bottom},
            bottom: {anchor: "row4", align: VerticalAlign.Bottom},
            left: {anchor: "row3", align: HorizontalAlign.Start},
            right: {anchor: "row3", align: HorizontalAlign.End}
          })
          .offset({
            x:-15,
            y:10
          })
          .backgroundImagePosition(Alignment.Bottom)
          .backgroundImageSize(ImageSize.Cover)
          .id("row6")
      }
      .width(300).height(300)
      .margin({left: 50})
      .border({width:2, color: "#6699FF"})
    }
    .height('100%')
  }
}

(5)多种组件的对齐布局

Row、Column、Flex、Stack等多种布局组件,可按照RelativeContainer组件规则进行对其排布。

@Entry
@Component
struct Index {
  @State value: number = 0
  build() {
    Row() {

      RelativeContainer() {
        Row().width(100).height(100)
          .backgroundColor('#ff33ffcc')
          .alignRules({
            top: {anchor: "__container__", align: VerticalAlign.Top},
            left: {anchor: "__container__", align: HorizontalAlign.Start}
          })
          .id("row1")

        Column().width('50%').height(30).backgroundColor(0xAFEEEE)
          .alignRules({
            top: {anchor: "__container__", align: VerticalAlign.Top},
            left: {anchor: "__container__", align: HorizontalAlign.Center}
          }).id("row2")

        Flex({ direction: FlexDirection.Row }) {
          Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
          Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
          Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
          Text('4').width('20%').height(50).backgroundColor(0xD2B48C)
        }
        .padding(10)
        .backgroundColor('#ffedafaf')
        .alignRules({
          top: {anchor: "row2", align: VerticalAlign.Bottom},
          left: {anchor: "__container__", align: HorizontalAlign.Start},
          bottom: {anchor: "__container__", align: VerticalAlign.Center},
          right: {anchor: "row2", align: HorizontalAlign.Center}
        })
        .id("row3")

        Stack({ alignContent: Alignment.Bottom }) {
          Text('First child, show in bottom').width('90%').height('100%').backgroundColor(0xd2cab3).align(Alignment.Top)
          Text('Second child, show in top').width('70%').height('60%').backgroundColor(0xc1cbac).align(Alignment.Top)
        }
        .margin({ top: 5 })
        .alignRules({
          top: {anchor: "row3", align: VerticalAlign.Bottom},
          left: {anchor: "__container__", align: HorizontalAlign.Start},
          bottom: {anchor: "__container__", align: VerticalAlign.Bottom},
          right: {anchor: "row3", align: HorizontalAlign.End}
        })
        .id("row4")

      }
      .width(300).height(300)
      .margin({left: 50})
      .border({width:2, color: "#6699FF"})
    }
    .height('100%')
  }
}

(6)组件尺寸

子组件尺寸大小不会受到相对布局规则的影响。若子组件某个方向上设置两个或以上alignRules时最好不设置此方向尺寸大小,否则对齐规则确定的组件尺寸与开发者设置的尺寸可能产生冲突。

@Entry
@Component
struct Index {
  build() {
    Row() {
      RelativeContainer() {
        Row(){Text('row1')}.justifyContent(FlexAlign.Center)
          .width(100).height(100)
          .backgroundColor("#FF3333")
          .alignRules({
            top: {anchor: "__container__", align: VerticalAlign.Top},
            left: {anchor: "__container__", align: HorizontalAlign.Start}
          })
          .id("row1")

        Row(){Text('row2')}.justifyContent(FlexAlign.Center).width(100)
          .backgroundColor("#FFCC00")
          .alignRules({
            top: {anchor: "__container__", align: VerticalAlign.Top},
            right: {anchor: "__container__", align: HorizontalAlign.End},
            bottom: {anchor: "row1", align: VerticalAlign.Center},
          })
          .id("row2")

        Row(){Text('row3')}.justifyContent(FlexAlign.Center).height(100)
          .backgroundColor("#FF6633")
          .alignRules({
            top: {anchor: "row1", align: VerticalAlign.Bottom},
            left: {anchor: "row1", align: HorizontalAlign.End},
            right: {anchor: "row2", align: HorizontalAlign.Start}
          })
          .id("row3")

        Row(){Text('row4')}.justifyContent(FlexAlign.Center)
          .backgroundColor("#FF9966")
          .alignRules({
            top: {anchor: "row3", align: VerticalAlign.Bottom},
            bottom: {anchor: "__container__", align: VerticalAlign.Bottom},
            left: {anchor: "__container__", align: HorizontalAlign.Start},
            right: {anchor: "row1", align: HorizontalAlign.End}
          })
          .id("row4")

        Row(){Text('row5')}.justifyContent(FlexAlign.Center)
          .backgroundColor("#FF66FF")
          .alignRules({
            top: {anchor: "row3", align: VerticalAlign.Bottom},
            bottom: {anchor: "__container__", align: VerticalAlign.Bottom},
            left: {anchor: "row2", align: HorizontalAlign.Start},
            right: {anchor: "row2", align: HorizontalAlign.End}
          })
          .id("row5")

        Row(){Text('row6')}.justifyContent(FlexAlign.Center)
          .backgroundColor('#ff33ffb5')
          .alignRules({
            top: {anchor: "row3", align: VerticalAlign.Bottom},
            bottom: {anchor: "row4", align: VerticalAlign.Bottom},
            left: {anchor: "row3", align: HorizontalAlign.Start},
            right: {anchor: "row3", align: HorizontalAlign.End}
          })
          .id("row6")
          .backgroundImagePosition(Alignment.Bottom)
          .backgroundImageSize(ImageSize.Cover)
      }
      .width(300).height(300)
      .margin({left: 50})
      .border({width:2, color: "#6699FF"})
    }
    .height('100%')
  }
}

5.栅格布局(GridRow/GirdCol)

(1)概述

栅格布局是一种通用的辅助定位工具,对移动设备的界面设计有较好的借鉴作用。主要优势包括:

  1. 提供可循的规律:栅格布局可以为布局提供规律性的结构,解决多尺寸多设备的动态布局问题。通过将页面划分为等宽的列数和行数,可以方便地对页面元素进行定位和排版。

  2. 统一的定位标注:栅格布局可以为系统提供一种统一的定位标注,保证不同设备上各个模块的布局一致性。这可以减少设计和开发的复杂度,提高工作效率。

  3. 灵活的间距调整方法:栅格布局可以提供一种灵活的间距调整方法,满足特殊场景布局调整的需求。通过调整列与列之间和行与行之间的间距,可以控制整个页面的排版效果。

  4. 自动换行和自适应:栅格布局可以完成一对多布局的自动换行和自适应。当页面元素的数量超出了一行或一列的容量时,他们会自动换到下一行或下一列,并且在不同的设备上自适应排版,使得页面布局更加灵活和适应性强。

GridRow为栅格容器组件,需与栅格子组件GridCol在栅格布局场景中联合使用。

(2)栅格系统断点

栅格系统以设备的水平宽度(屏幕密度像素值,单位vp)作为断点依据,定义设备的宽度类型,形成了一套断点规则。开发者可根据需求在不同的断点区间实现不同的页面布局效果。

栅格系统默认断点将设备宽度分为xs、sm、md、lg四类,尺寸范围如下:

断点名称取值范围(vp)设备描述
xs[0, 320)最小宽度类型设备。
sm[320, 520)小宽度类型设备。
md[520, 840)中等宽度类型设备。
lg[840, +∞)大宽度类型设备。

在GridRow栅格组件中,允许开发者使用breakpoints自定义修改断点的取值范围,最多支持6个断点,除了默认的四个断点外,还可以启用xl,xxl两个断点,支持六种不同尺寸(xs, sm, md, lg, xl, xxl)设备的布局设置。

断点名称设备描述
xs最小宽度类型设备。
sm小宽度类型设备。
md中等宽度类型设备。
lg大宽度类型设备。
xl特大宽度类型设备。
xxl超大宽度类型设备。

 使用栅格的默认列数12列,通过断点设置将应用宽度分成六个区间,在各区间中,每个栅格子元素占用的列数均不同。

@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown];
...
GridRow({
  breakpoints: {
    value: ['200vp', '300vp', '400vp', '500vp', '600vp'],
    reference: BreakpointsReference.WindowSize
  }
}) {
   ForEach(this.bgColors, (color:Color, index?:number|undefined) => {
     GridCol({
       span: {
         xs: 2, // 在最小宽度类型设备上,栅格子组件占据的栅格容器2列。
         sm: 3, // 在小宽度类型设备上,栅格子组件占据的栅格容器3列。
         md: 4, // 在中等宽度类型设备上,栅格子组件占据的栅格容器4列。
         lg: 6, // 在大宽度类型设备上,栅格子组件占据的栅格容器6列。
         xl: 8, // 在特大宽度类型设备上,栅格子组件占据的栅格容器8列。
         xxl: 12 // 在超大宽度类型设备上,栅格子组件占据的栅格容器12列。
       }
     }) {
       Row() {
         Text(`${index}`)
       }.width("100%").height('50vp')
     }.backgroundColor(color)
   })
}                                                                    

(3)布局的总列数

GridRow中通过columns设置栅格布局的总列数。

  • columns默认值为12,即在未设置columns时,任何断点下,栅格布局被分成12列。

@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown,Color.Red, Color.Orange, Color.Yellow, Color.Green];
...
GridRow() {
  ForEach(this.bgColors, (item:Color, index?:number|undefined) => {
    GridCol() {
      Row() {
          Text(`${index}`)
      }.width('100%').height('50')
    }.backgroundColor(item)
  })
}           

当columns为自定义值,栅格布局在任何尺寸设备下都被分为columns列。下面分别设置栅格布局列数为4和8,子元素默认占一列,效果如下:

class CurrTmp{
  currentBp: string = 'unknown';
  set(val:string){
    this.currentBp = val
  }
}
let BorderWH:Record<string,Color|number> = { 'color': Color.Blue, 'width': 2 }
@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown];
@State currentBp: string = 'unknown';
...
Row() {
  GridRow({ columns: 4 }) {
    ForEach(this.bgColors, (item:Color, index?:number|undefined) => {
      GridCol() {
        Row() {
          Text(`${index}`)
        }.width('100%').height('50')
      }.backgroundColor(item)
    })
  }
  .width('100%').height('100%')
  .onBreakpointChange((breakpoint:string) => {
    let CurrSet:CurrTmp = new CurrTmp()
    CurrSet.set(breakpoint)
  })
}
.height(160)
.border(BorderWH)
.width('90%')

Row() {
  GridRow({ columns: 8 }) {
    ForEach(this.bgColors, (item:Color, index?:number|undefined) => {
        GridCol() {
          Row() {
            Text(`${index}`)
          }.width('100%').height('50')
        }.backgroundColor(item)
    })
  }
  .width('100%').height('100%')
  .onBreakpointChange((breakpoint:string) => {
    let CurrSet:CurrTmp = new CurrTmp()
    CurrSet.set(breakpoint)
  })
}
.height(160)
.border(BorderWH)
.width('90%')

当columns类型为GridRowColumnOption时,支持下面六种不同尺寸(xs, sm, md, lg, xl, xxl)设备的总列数设置,各个尺寸下数值可不同。

@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown]
GridRow({ columns: { sm: 4, md: 8 }, breakpoints: { value: ['200vp', '300vp', '400vp', '500vp', '600vp'] } }) {
  ForEach(this.bgColors, (item:Color, index?:number|undefined) => {
    GridCol() {
      Row() {
        Text(`${index}`)
      }.width('100%').height('50')
    }.backgroundColor(item)
  })
}

若只设置sm, md的栅格总列数,则较小的尺寸使用默认columns值12,较大的尺寸使用前一个尺寸的columns。这里只设置sm:4, md:8,则较小尺寸的xs:12,较大尺寸的参照md的设置,lg:8, xl:8, xxl:8

(4)排列方向

栅格布局中,可以通过设置GridRow的direction属性来指定栅格子组件在栅格容器中的排列方向。该属性可以设置为GridRowDirection.Row(从左往右排列)或GridRowDirection.RowReverse(从右往左排列),以满足不同的布局需求。通过合理的direction属性设置,可以使得页面布局更加灵活和符合设计要求。

  • 子组件默认从左往右排列。

GridRow({ direction: GridRowDirection.Row }){}

子组件从右往左排列。

GridRow({ direction: GridRowDirection.RowReverse }){}

(5)子组件间距

GridRow中通过gutter属性设置子元素在水平和垂直方向的间距。

  • 当gutter类型为number时,同时设置栅格子组件间水平和垂直方向边距且相等。下例中,设置子组件水平与垂直方向距离相邻元素的间距为10。

 GridRow({ gutter: 10 }){}

当gutter类型为GutterOption时,单独设置栅格子组件水平垂直边距,x属性为水平方向间距,y为垂直方向间距。

当gutter类型为GutterOption时,单独设置栅格子组件水平垂直边距,x属性为水平方向间距,y为垂直方向间距。

(6)子组件GridCol

GridCol组件作为GridRow组件的子组件,通过给GridCol传参或者设置属性两种方式,设置span(占用列数),offset(偏移列数),order(元素序号)的值。

1.span

子组件占栅格布局的列数,决定了子组件的宽度,默认为1。

  • 当类型为number时,子组件在所有尺寸设备下占用的列数相同。

@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown];
...
GridRow({ columns: 8 }) {
  ForEach(this.bgColors, (color:Color, index?:number|undefined) => {
    GridCol({ span: 2 }) {      
      Row() {
        Text(`${index}`)
      }.width('100%').height('50vp')          
    }
    .backgroundColor(color)
  })
}                

当类型为GridColColumnOption时,支持六种不同尺寸(xs, sm, md, lg, xl, xxl)设备中子组件所占列数设置,各个尺寸下数值可不同。

@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown];
...
GridRow({ columns: 8 }) {
  ForEach(this.bgColors, (color:Color, index?:number|undefined) => {
    GridCol({ span: { xs: 1, sm: 2, md: 3, lg: 4 } }) {      
      Row() {
        Text(`${index}`)
      }.width('100%').height('50vp')          
    }
    .backgroundColor(color)
  })
}                

2.offset

栅格子组件相对于前一个子组件的偏移列数,默认为0。

  • 当类型为number时,子组件偏移相同列数。

@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown];
...
GridRow() {
  ForEach(this.bgColors, (color:Color, index?:number|undefined) => {
    GridCol({ offset: 2 }) {      
      Row() {
        Text('' + index)
      }.width('100%').height('50vp')          
    }
    .backgroundColor(color)
  })
}                

  • 栅格默认分成12列,每一个子组件默认占1列,偏移2列,每个子组件及间距共占3列,一行放四个子组件。

  • 当类型为GridColColumnOption时,支持六种不同尺寸(xs, sm, md, lg, xl, xxl)设备中子组件所占列数设置,各个尺寸下数值可不同。

@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown];
...

GridRow() {
  ForEach(this.bgColors, (color:Color, index?:number|undefined) => {
    GridCol({ offset: { xs: 1, sm: 2, md: 3, lg: 4 } }) {      
      Row() {
        Text('' + index)
      }.width('100%').height('50vp')          
    }
    .backgroundColor(color)
  })
}                 

3.order

栅格子组件的序号,决定子组件排列次序。当子组件不设置order或者设置相同的order, 子组件按照代码顺序展示。当子组件设置不同的order时,order较小的组件在前,较大的在后。

当子组件部分设置order,部分不设置order时,未设置order的子组件依次排序靠前,设置了order的子组件按照数值从小到大排列。

  • 当类型为number时,子组件在任何尺寸下排序次序一致。

GridRow() {
  GridCol({ order: 4 }) {
    Row() {
      Text('1')
    }.width('100%').height('50vp')
  }.backgroundColor(Color.Red)
  GridCol({ order: 3 }) {
    Row() {
      Text('2')
    }.width('100%').height('50vp')
  }.backgroundColor(Color.Orange)
  GridCol({ order: 2 }) {
    Row() {
      Text('3')
    }.width('100%').height('50vp')
  }.backgroundColor(Color.Yellow)
  GridCol({ order: 1 }) {
    Row() {
      Text('4')
    }.width('100%').height('50vp')
  }.backgroundColor(Color.Green)
}            

当类型为GridColColumnOption时,支持六种不同尺寸(xs, sm, md, lg, xl, xxl)设备中子组件排序次序设置。在xs设备中,子组件排列顺序为1234;sm为2341,md为3412,lg为2431。

GridRow() {
  GridCol({ order: { xs:1, sm:5, md:3, lg:7}}) {
    Row() {
      Text('1')
    }.width('100%').height('50vp')
  }.backgroundColor(Color.Red)
  GridCol({ order: { xs:2, sm:2, md:6, lg:1} }) {
    Row() {
      Text('2')
    }.width('100%').height('50vp')
  }.backgroundColor(Color.Orange)
  GridCol({ order: { xs:3, sm:3, md:1, lg:6} }) {
    Row() {
      Text('3')
    }.width('100%').height('50vp')
  }.backgroundColor(Color.Yellow)
  GridCol({ order: { xs:4, sm:4, md:2, lg:5} }) {
    Row() {
      Text('4')
    }.width('100%').height('50vp')
  }.backgroundColor(Color.Green)
} 

(7)栅格组件的嵌套使用

栅格组件也可以嵌套使用,完成一些复杂的布局。

以下示例中,栅格把整个空间分为12份。第一层GridRow嵌套GridCol,分为中间大区域以及“footer”区域。第二层GridRow嵌套GridCol,分为“left”和“right”区域。子组件空间按照上一层父组件的空间划分,粉色的区域是屏幕空间的12列,绿色和蓝色的区域是父组件GridCol的12列,依次进行空间的划分。

@Entry
@Component
struct GridRowExample {
  build() {
    GridRow() {
      GridCol({ span: { sm: 12 } }) {
        GridRow() {
          GridCol({ span: { sm: 2 } }) {
            Row() {
              Text('left').fontSize(24)
            }
            .justifyContent(FlexAlign.Center)
            .height('90%')
          }.backgroundColor('#ff41dbaa')

          GridCol({ span: { sm: 10 } }) {
            Row() {
              Text('right').fontSize(24)
            }
            .justifyContent(FlexAlign.Center)
            .height('90%')
          }.backgroundColor('#ff4168db')
        }
        .backgroundColor('#19000000')
      }

      GridCol({ span: { sm: 12 } }) {
        Row() {
          Text('footer').width('100%').textAlign(TextAlign.Center)
        }.width('100%').height('10%').backgroundColor(Color.Pink)
      }
    }.width('100%').height(300)
  }
}

综上所述,栅格组件提供了丰富的自定义能力,功能异常灵活和强大。只需要明确栅格在不同断点下的Columns、Margin、Gutter及span等参数,即可确定最终布局,无需关心具体的设备类型及设备状态(如横竖屏)等。

6.媒体查询 (@ohos.mediaquery)

(1)概述

媒体查询作为响应式设计的核心,在移动设备上应用十分广泛。媒体查询可根据不同设备类型或同设备不同状态修改应用的样式。媒体查询常用于下面两种场景:

  1. 针对设备和应用的属性信息(比如显示区域、深浅色、分辨率),设计出相匹配的布局。

  2. 当屏幕发生动态改变时(比如分屏、横竖屏切换),同步更新应用的页面布局。

(2)引入与使用流程

媒体查询通过mediaquery模块接口,设置查询条件并绑定回调函数,任一媒体特征改变时,均会触发回调函数,返回匹配结果,根据返回值更改页面布局或者实现业务逻辑,实现页面的响应式设计。具体步骤如下:

首先导入媒体查询模块。

import { mediaquery } from '@kit.ArkUI';

 通过matchMediaSync接口设置媒体查询条件,保存返回的条件监听句柄listener。例如监听横屏事件

let listener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync('(orientation: landscape)');

给条件监听句柄listener绑定回调函数onPortrait,当listener检测设备状态变化时执行回调函数。在回调函数内,根据不同设备状态更改页面布局或者实现业务逻辑。

onPortrait(mediaQueryResult: mediaquery.MediaQueryResult) {
  if (mediaQueryResult.matches as boolean) {
    // do something here
  } else {
    // do something here
  }
}

listener.on('change', onPortrait);

(3)媒体查询条件

媒体查询条件由媒体类型、逻辑操作符、媒体特征组成,其中媒体类型可省略,逻辑操作符用于连接不同媒体类型与媒体特征,其中,媒体特征要使用“()”包裹且可以有多个。

1.语法规则

语法规则包括媒体类型(media-type)媒体逻辑操作(media-logic-operations)媒体特征(media-feature)

[media-type] [media-logic-operations] [(media-feature)]

例如:

  • screen and (round-screen: true) :表示当设备屏幕是圆形时条件成立。

  • (max-height: 800px) :表示当高度小于等于800px时条件成立。

  • (height <= 800px) :表示当高度小于等于800px时条件成立。

  • screen and (device-type: tv) or (resolution < 2) :表示包含多个媒体特征的多条件复杂语句查询,当设备类型为tv或设备分辨率小于2时条件成立。

  • (dark-mode: true) :表示当系统为深色模式时成立。

2.媒体类型(media-type)

查询条件未写媒体类型时,默认为screen。媒体类型必须写在查询条件开头

类型说明
screen按屏幕相关参数进行媒体查询。

媒体逻辑操作(media-logic-operations)

媒体逻辑操作符:and、or、not、only用于构成复杂媒体查询,也可以通过comma(, )将其组合起来,详细解释说明如下表。

表1 媒体逻辑操作符

类型说明
and将多个媒体特征(Media Feature)以“与”的方式连接成一个媒体查询,只有当所有媒体特征都为true,查询条件成立。另外,它还可以将媒体类型和媒体功能结合起来。例如:screen and (device-type: wearable) and (max-height: 600px) 表示当设备类型是智能穿戴且应用的最大高度小于等于600个像素单位时成立。
or将多个媒体特征以“或”的方式连接成一个媒体查询,如果存在结果为true的媒体特征,则查询条件成立。例如:screen and (max-height: 1000px) or (round-screen: true) 表示当应用高度小于等于1000个像素单位或者设备屏幕是圆形时,条件成立。
notnot操作符必须搭配screen使用,取反媒体查询结果,媒体查询结果不成立时返回true,否则返回false。例如:not screen and (min-height: 50px) and (max-height: 600px) 表示当应用高度小于50个像素单位或者大于600个像素单位时成立。
onlyonly操作符必须搭配screen使用, 当前效果与单独使用screen相同。例如:only screen and (height <= 50)
comma(, )将多个媒体特征以“或”的方式连接成一个媒体查询,如果存在结果为true的媒体特征,则查询条件成立。其效果等同于or运算符。例如:screen and (min-height: 1000px), (round-screen: true) 表示当应用高度大于等于1000个像素单位或者设备屏幕是圆形时,条件成立。

媒体范围操作符包括<=,>=,<,>,详细解释说明如下表。

表2 媒体逻辑范围操作符

类型说明
<=小于等于,例如:screen and (height <= 50)。
>=大于等于,例如:screen and (height >= 600)。
<小于,例如:screen and (height < 50)。
>大于,例如:screen and (height > 600)。

媒体特征(media-feature)

媒体特征包括应用显示区域的宽高、设备分辨率以及设备的宽高等属性,详细说明如下表。

表3 媒体特征说明表

比较height、width等宽高尺寸时,支持vp和px单位,无单位默认为px。

类型说明
height应用页面可绘制区域的高度。
min-height应用页面可绘制区域的最小高度。
max-height应用页面可绘制区域的最大高度。
width应用页面可绘制区域的宽度。
min-width应用页面可绘制区域的最小宽度。
max-width应用页面可绘制区域的最大宽度。
resolution

设备的分辨率,支持dpi,dppx和dpcm单位。其中:

- dpi表示每英寸中物理像素个数,1dpi ≈ 0.39dpcm;

- dpcm表示每厘米上的物理像素个数,1dpcm ≈ 2.54dpi;

- dppx表示每个px中的物理像素数(此单位按96px = 1英寸为基准,与页面中的px单位计算方式不同),1dppx = 96dpi。

min-resolution设备的最小分辨率。
max-resolution设备的最大分辨率。
orientation

屏幕的方向。

可选值:

- orientation: portrait(设备竖屏);

- orientation: landscape(设备横屏)。

device-height设备的高度。
min-device-height设备的最小高度。
max-device-height设备的最大高度。
device-width设备的宽度。当前仅在应用初始化时保存一次,不会随设备宽度变化实时更新,例如折叠屏的折叠展开场景。
device-type

设备的类型。

可选值:default、tablet。

min-device-width设备的最小宽度。
max-device-width设备的最大宽度。
round-screen屏幕类型,圆形屏幕为true,非圆形屏幕为false。
dark-mode

系统当前的深浅模式。可选值:true、false。

深色模式为true,浅色模式为false。

说明

目前在卡片中使用媒体查询,只支持height、width。

7.创建列表 (List)

概述

列表是一种复杂的容器,当列表项达到一定数量,内容超过屏幕大小时,可以自动提供滚动功能。它适合用于呈现同类数据类型或数据类型集,例如图片和文本。在列表中显示数据集合是许多应用程序中的常见要求(如通讯录、音乐列表、购物清单等)。

使用列表可以轻松高效地显示结构化、可滚动的信息。通过在List组件中按垂直或者水平方向线性排列子组件ListItemGroupListItem,为列表中的行或列提供单个视图,或使用循环渲染迭代一组行或列,或混合任意数量的单个视图和ForEach结构,构建一个列表。List组件支持使用条件渲染、循环渲染、懒加载等渲染控制方式生成子组件。

布局与约束

列表作为一种容器,会自动按其滚动方向排列子组件,向列表中添加组件或从列表中移除组件会重新排列子组件。

如下图所示,在垂直列表中,List按垂直方向自动排列ListItemGroup或ListItem。

ListItemGroup用于列表数据的分组展示,其子组件也是ListItem。ListItem表示单个列表项,可以包含单个子组件。

图1 List、ListItemGroup和ListItem组件关系

说明

List的子组件必须是ListItemGroup或ListItem,ListItem和ListItemGroup必须配合List来使用。

布局

List除了提供垂直和水平布局能力、超出屏幕时可以滚动的自适应延伸能力之外,还提供了自适应交叉轴方向上排列个数的布局能力。

利用垂直布局能力可以构建单列或者多列垂直滚动列表

垂直滚动列表(左:单列;右:多列)

利用水平布局能力可以是构建单行或多行水平滚动列表

水平滚动列表(左:单行;右:多行)

Grid和WaterFlow也可以实现单列、多列布局,如果布局每列等宽,且不需要跨行跨列布局,相比Gird和WaterFlow,则更推荐使用List。

约束

列表的主轴方向是指子组件列的排列方向,也是列表的滚动方向。垂直于主轴的轴称为交叉轴,其方向与主轴方向相互垂直。

如下图所示,垂直列表的主轴是垂直方向,交叉轴是水平方向;水平列表的主轴是水平方向,交叉轴是垂直方向。

列表的主轴与交叉轴

如果List组件主轴或交叉轴方向设置了尺寸,则其对应方向上的尺寸为设置值。

如果List组件主轴方向没有设置尺寸,当List子组件主轴方向总尺寸小于List的父组件尺寸时,List主轴方向尺寸自动适应子组件的总尺寸。

如下图所示,一个垂直列表B没有设置高度时,其父组件A高度为200vp,若其所有子组件C的高度总和为150vp,则此时列表B的高度为150vp。

 列表主轴高度约束示例1(A: List的父组件; B: List组件; C: List的所有子组件)

如果子组件主轴方向总尺寸超过List父组件尺寸时,List主轴方向尺寸适应List的父组件尺寸。

如下图所示,同样是没有设置高度的垂直列表B,其父组件A高度为200vp,若其所有子组件C的高度总和为300vp,则此时列表B的高度为200vp。

列表主轴高度约束示例2(A: List的父组件; B: List组件; C: List的所有子组件)

List组件交叉轴方向在没有设置尺寸时,其尺寸默认自适应父组件尺寸。

开发布局

设置主轴方向

List组件主轴默认是垂直方向,即默认情况下不需要手动设置List方向,就可以构建一个垂直滚动列表。

若是水平滚动列表场景,将List的listDirection属性设置为Axis.Horizontal即可实现。listDirection默认为Axis.Vertical,即主轴默认是垂直方向。

设置交叉轴布局

List组件的交叉轴布局可以通过lanes和alignListItem属性进行设置,lanes属性用于确定交叉轴排列的列表项数量,alignListItem用于设置子组件在交叉轴方向的对齐方式。

List组件的lanes属性通常用于在不同尺寸的设备自适应构建不同行数或列数的列表,即一次开发、多端部署的场景,例如歌单列表。lanes属性的取值类型是"number | LengthConstrain",即整数或者LengthConstrain类型。以垂直列表为例,如果将lanes属性设为2,表示构建的是一个两列的垂直列表,如图2中右图所示。lanes的默认值为1,即默认情况下,垂直列表的列数是1。

当其取值为LengthConstrain类型时,表示会根据LengthConstrain与List组件的尺寸自适应决定行或列数。

在列表中显示数据

列表视图垂直或水平显示项目集合,在行或列超出屏幕时提供滚动功能,使其适合显示大型数据集合。在最简单的列表形式中,List静态地创建其列表项ListItem的内容。

城市列表

@Entry
@Component
struct CityList {
  build() {
    List() {
      ListItem() {
        Text('北京').fontSize(24)
      }

      ListItem() {
        Text('杭州').fontSize(24)
      }

      ListItem() {
        Text('上海').fontSize(24)
      }
    }
    .backgroundColor('#FFF1F3F5')
    .alignListItem(ListItemAlign.Center)
  }
}

由于在ListItem中只能有一个根节点组件,不支持以平铺形式使用多个组件。因此,若列表项是由多个组件元素组成的,则需要将这多个元素组合到一个容器组件内或组成一个自定义组件。

联系人列表项示例

如上图所示,联系人列表的列表项中,每个联系人都有头像和名称。此时,需要将Image和Text封装到一个Row容器内。

List() {
  ListItem() {
    Row() {
      Image($r('app.media.iconE'))
        .width(40)
        .height(40)
        .margin(10)

      Text('小明')
        .fontSize(20)
    }
  }

  ListItem() {
    Row() {
      Image($r('app.media.iconF'))
        .width(40)
        .height(40)
        .margin(10)

      Text('小红')
        .fontSize(20)
    }
  }
}

迭代列表内容

通常,应用通过数据集合动态地创建列表。使用循环渲染可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件,降低代码复杂度。

ArkTS通过ForEach提供了组件的循环渲染能力。以简单形式的联系人列表为例,将联系人名称和头像数据以Contact类结构存储到contacts数组,使用ForEach中嵌套ListItem的形式来代替多个平铺的、内容相似的ListItem,从而减少重复代码。

import { util } from '@kit.ArkTS'

class Contact {
  key: string = util.generateRandomUUID(true);
  name: string;
  icon: Resource;

  constructor(name: string, icon: Resource) {
    this.name = name;
    this.icon = icon;
  }
}

@Entry
@Component
struct SimpleContacts {
  private contacts: Array<object> = [
    new Contact('小明', $r("app.media.iconA")),
    new Contact('小红', $r("app.media.iconB")),
  ]

  build() {
    List() {
      ForEach(this.contacts, (item: Contact) => {
        ListItem() {
          Row() {
            Image(item.icon)
              .width(40)
              .height(40)
              .margin(10)
            Text(item.name).fontSize(20)
          }
          .width('100%')
          .justifyContent(FlexAlign.Start)
        }
      }, (item: Contact) => JSON.stringify(item))
    }
    .width('100%')
  }
}

在List组件中,ForEach除了可以用来循环渲染ListItem,也可以用来循环渲染ListItemGroup。ListItemGroup的循环渲染详细使用请参见支持分组列表

8.创建网格 (Grid/GridItem)

概述

网格布局是由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局。网格布局具有较强的页面均分能力,子组件占比控制能力,是一种重要自适应布局,其使用场景有九宫格图片展示、日历、计算器等。

ArkUI提供了Grid容器组件和子组件GridItem,用于构建网格布局。Grid用于设置网格布局相关参数,GridItem定义子组件相关特征。Grid组件支持使用条件渲染循环渲染懒加载等方式生成子组件。

布局与约束

Grid组件为网格容器,其中容器内各条目对应一个GridItem组件

说明

Grid的子组件必须是GridItem组件。

网格布局是一种二维布局。Grid组件支持自定义行列数和每行每列尺寸占比、设置子组件横跨几行或者几列,同时提供了垂直和水平布局能力。当网格容器组件尺寸发生变化时,所有子组件以及间距会等比例调整,从而实现网格布局的自适应能力。根据Grid的这些布局能力,可以构建出不同样式的网格布局,如下图所示。

如果Grid组件设置了宽高属性,则其尺寸为设置值。如果没有设置宽高属性,Grid组件的尺寸默认适应其父组件的尺寸。

Grid组件根据行列数量与占比属性的设置,可以分为三种布局情况:

  • 行、列数量与占比同时设置:Grid只展示固定行列数的元素,其余元素不展示,且Grid不可滚动。(推荐使用该种布局方式)

  • 只设置行、列数量与占比中的一个:元素按照设置的方向进行排布,超出的元素可通过滚动的方式展示。

  • 行列数量与占比都不设置:元素在布局方向上排布,其行列数由布局方向、单个网格的宽高等多个属性共同决定。超出行列容纳范围的元素不展示,且Grid不可滚动。

设置排列方式
设置行列数量与占比

通过设置行列数量与尺寸占比可以确定网格布局的整体排列方式。Grid组件提供了rowsTemplate和columnsTemplate属性用于设置网格布局行列数量与尺寸占比。

rowsTemplate和columnsTemplate属性值是一个由多个空格和'数字+fr'间隔拼接的字符串,fr的个数即网格布局的行或列数,fr前面的数值大小,用于计算该行或列在网格布局宽度上的占比,最终决定该行或列宽度。

如上图所示,构建的是一个三行三列的网格布局,其在垂直方向上分为三等份,每行占一份;在水平方向上分为四等份,第一列占一份,第二列占两份,第三列占一份。

只要将rowsTemplate的值为'1fr 1fr 1fr',同时将columnsTemplate的值为'1fr 2fr 1fr',即可实现上述网格布局。

Grid() {
  ...
}
.rowsTemplate('1fr 1fr 1fr')
.columnsTemplate('1fr 2fr 1fr')
设置子组件所占行列数

除了大小相同的等比例网格布局,由不同大小的网格组成不均匀分布的网格布局场景在实际应用中十分常见,如下图所示。在Grid组件中,可以通过创建Grid时传入合适的GridLayoutOptions实现如图所示的单个网格横跨多行或多列的场景,其中,irregularIndexes和onGetIrregularSizeByIndex可对仅设置rowsTemplate或columnsTemplate的Grid使用;onGetRectByIndex可对同时设置rowsTemplate和columnsTemplate的Grid使用。

不均匀网格布局

例如计算器的按键布局就是常见的不均匀网格布局场景。如下图,计算器中的按键“0”和“=”,按键“0”横跨第一、二两列,按键“=”横跨第五、六两行。使用Grid构建的网格布局,其行列标号从0开始,依次编号。

计算器

在网格中,可以通过onGetRectByIndex返回的[rowStart,columnStart,rowSpan,columnSpan]来实现跨行跨列布局,其中rowStart和columnStart属性表示指定当前元素起始行号和起始列号,rowSpan和columnSpan属性表示指定当前元素的占用行数和占用列数。

所以“0”按键横跨第一列和第二列,“=”按键横跨第五行和第六行,只要将“0”对应onGetRectByIndex的rowStart和columnStart设为5和0,rowSpan和columnSpan设为1和2,将“=”对应onGetRectByIndex的rowStart和columnStart设为4和3,rowSpan和columnSpan设为2和1即可。

layoutOptions: GridLayoutOptions = {
  regularSize: [1, 1],
  onGetRectByIndex: (index: number) => {
    if (index == key1) { // key1是“0”按键对应的index
      return [5, 0, 1, 2]
    } else if (index == key2) { // key2是“=”按键对应的index
      return [4, 3, 2, 1]
    }
    // ...
    // 这里需要根据具体布局返回其他item的位置
  }
}

Grid(undefined, this.layoutOptions) {
  // ...
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsTemplate('2fr 1fr 1fr 1fr 1fr 1fr')
设置主轴方向

使用Grid构建网格布局时,若没有设置行列数量与占比,可以通过layoutDirection设置网格布局的主轴方向,决定子组件的排列方式。此时可以结合minCount和maxCount属性来约束主轴方向上的网格数量。

主轴方向示意图

当前layoutDirection设置为Row时,先从左到右排列,排满一行再排下一行。当前layoutDirection设置为Column时,先从上到下排列,排满一列再排下一列,如上图所示。此时,将maxCount属性设为3,表示主轴方向上最大显示的网格单元数量为3。

说明

  • layoutDirection属性仅在不设置rowsTemplate和columnsTemplate时生效,此时元素在layoutDirection方向上排列。
  • 仅设置rowsTemplate时,Grid主轴为水平方向,交叉轴为垂直方向。
  • 仅设置columnsTemplate时,Grid主轴为垂直方向,交叉轴为水平方向。
在网格布局中显示数据

网格布局采用二维布局的方式组织其内部元素

通用办公服务

Grid组件可以通过二维布局的方式显示一组GridItem子组件。

Grid() {
  GridItem() {
    Text('会议')
      ...
  }

  GridItem() {
    Text('签到')
      ...
  }

  GridItem() {
    Text('投票')
      ...
  }

  GridItem() {
    Text('打印')
      ...
  }
}
.rowsTemplate('1fr 1fr')
.columnsTemplate('1fr 1fr')

对于内容结构相似的多个GridItem,通常更推荐使用ForEach语句中嵌套GridItem的形式,来减少重复代码。

@Entry
@Component
struct OfficeService {
  @State services: Array<string> = ['会议', '投票', '签到', '打印']

  build() {
    Column() {
      Grid() {
        ForEach(this.services, (service:string) => {
          GridItem() {
            Text(service)
          }
        }, (service:string):string => service)
      }
      .rowsTemplate(('1fr 1fr') as string)
      .columnsTemplate(('1fr 1fr') as string)
    }
  }
}
设置行列间距

在两个网格单元之间的网格横向间距称为行间距,网格纵向间距称为列间距,

构建可滚动的网格布局

可滚动的网格布局常用在文件管理、购物或视频列表等页面中,如下图所示。在设置Grid的行列数量与占比时,如果仅设置行、列数量与占比中的一个,即仅设置rowsTemplate或仅设置columnsTemplate属性,网格单元按照设置的方向排列,超出Grid显示区域后,Grid拥有可滚动能力。

横向可滚动网格布局

如果设置的是columnsTemplate,Grid的滚动方向为垂直方向;如果设置的是rowsTemplate,Grid的滚动方向为水平方向。

如上图所示的横向可滚动网格布局,只要设置rowsTemplate属性的值且不设置columnsTemplate属性,当内容超出Grid组件宽度时,Grid可横向滚动进行内容展示。

@Entry
@Component
struct Shopping {
  @State services: Array<string> = ['直播', '进口']

  build() {
    Column({ space: 5 }) {
      Grid() {
        ForEach(this.services, (service: string, index) => {
          GridItem() {
          }
          .width('25%')
        }, (service:string):string => service)
      }
      .rowsTemplate('1fr 1fr') // 只设置rowsTemplate属性,当内容超出Grid区域时,可水平滚动。
      .rowsGap(15)
    }
  }
}
控制滚动位置

与新闻列表的返回顶部场景类似,控制滚动位置功能在网格布局中也很常用,例如下图所示日历的翻页功能。

 日历翻页

Grid组件初始化时,可以绑定一个Scroller对象,用于进行滚动控制,例如通过Scroller对象的scrollPage方法进行翻页。

private scroller: Scroller = new Scroller()

在日历页面中,用户在点击“下一页”按钮时,应用响应点击事件,通过指定scrollPage方法的参数next为true,滚动到下一页。

Column({ space: 5 }) {
  Grid(this.scroller) {
  }
  .columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr')

  Row({space: 20}) {
    Button('上一页')
      .onClick(() => {
        this.scroller.scrollPage({
          next: false
        })
      })

    Button('下一页')
      .onClick(() => {
        this.scroller.scrollPage({
          next: true
        })
      })
  }
}
性能优化

长列表的处理类似,循环渲染适用于数据量较小的布局场景,当构建具有大量网格项的可滚动网格布局时,推荐使用数据懒加载方式实现按需迭代加载数据,从而提升列表性能。

关于按需加载优化的具体实现可参考数据懒加载章节中的示例。

当使用懒加载方式渲染网格时,为了更好的滚动体验,减少滑动时出现白块,Grid组件中也可通过cachedCount属性设置GridItem的预加载数量,只在懒加载LazyForEach中生效。

设置预加载数量后,会在Grid显示区域前后各缓存cachedCount*列数个GridItem,超出显示和缓存范围的GridItem会被释放。

Grid() {
  LazyForEach(this.dataSource, () => {
    GridItem() {
    }
  })
}
.cachedCount(3)

说明

cachedCount的增加会增大UI的CPU、内存开销。使用时需要根据实际情况,综合性能和用户体验进行调整。

9.创建轮播 (Swiper)

Swiper组件提供滑动轮播显示的能力。Swiper本身是一个容器组件,当设置了多个子组件后,可以对这些子组件进行轮播显示。通常,在一些应用首页显示推荐的内容时,需要用到轮播显示的能力。

针对复杂页面场景,可以使用 Swiper 组件的预加载机制,利用主线程的空闲时间来提前构建和布局绘制组件,优化滑动体验。

布局与约束

Swiper作为一个容器组件,如果设置了自身尺寸属性,则在轮播显示过程中均以该尺寸生效。如果自身尺寸属性未被设置,则分两种情况:如果设置了prevMargin或者nextMargin属性,则Swiper自身尺寸会跟随其父组件;如果未设置prevMargin或者nextMargin属性,则会自动根据子组件的大小设置自身的尺寸。

循环播放

通过loop属性控制是否循环播放,该属性默认值为true。

当loop为true时,在显示第一页或最后一页时,可以继续往前切换到前一页或者往后切换到后一页。如果loop为false,则在第一页或最后一页时,无法继续向前或者向后切换页面。

  • loop为true
Swiper() {
  Text('0')
    .width('90%')
    .height('100%')
    .backgroundColor(Color.Gray)
    .textAlign(TextAlign.Center)
    .fontSize(30)

  Text('1')
    .width('90%')
    .height('100%')
    .backgroundColor(Color.Green)
    .textAlign(TextAlign.Center)
    .fontSize(30)

  Text('2')
    .width('90%')
    .height('100%')
    .backgroundColor(Color.Pink)
    .textAlign(TextAlign.Center)
    .fontSize(30)
}
.loop(true)

  • loop为false
Swiper() {
  // ...
}
.loop(false)

自动轮播

Swiper通过设置autoPlay属性,控制是否自动轮播子组件。该属性默认值为false。

autoPlay为true时,会自动切换播放子组件,子组件与子组件之间的播放间隔通过interval属性设置。interval属性默认值为3000,单位毫秒。

Swiper() {
  // ...
}
.loop(true)
.autoPlay(true)
.interval(1000)

导航点样式

Swiper提供了默认的导航点样式和导航点箭头样式,导航点默认显示在Swiper下方居中位置,开发者也可以通过indicator属性自定义导航点的位置和样式,导航点箭头默认不显示。

通过indicator属性,开发者可以设置导航点相对于Swiper组件上下左右四个方位的位置,同时也可以设置每个导航点的尺寸、颜色、蒙层和被选中导航点的颜色。

  • 导航点使用默认样式
Swiper() {
  Text('0')
    .width('90%')
    .height('100%')
    .backgroundColor(Color.Gray)
    .textAlign(TextAlign.Center)
    .fontSize(30)

  Text('1')
    .width('90%')
    .height('100%')
    .backgroundColor(Color.Green)
    .textAlign(TextAlign.Center)
    .fontSize(30)

  Text('2')
    .width('90%')
    .height('100%')
    .backgroundColor(Color.Pink)
    .textAlign(TextAlign.Center)
    .fontSize(30)
}

  • 自定义导航点样式

导航点直径设为30vp,左边距为0,导航点颜色设为红色。

Swiper() {
  // ...
}
.indicator(
  Indicator.dot()
    .left(0)
    .itemWidth(15)
    .itemHeight(15)
    .selectedItemWidth(30)
    .selectedItemHeight(15)
    .color(Color.Red)
    .selectedColor(Color.Blue)
)

Swiper通过设置displayArrow属性,可以控制导航点箭头的大小、位置、颜色,底板的大小及颜色,以及鼠标悬停时是否显示箭头。

  • 箭头使用默认样式
Swiper() {
  // ...
}
.displayArrow(true, false)

  • 自定义箭头样式

箭头显示在组件两侧,大小为18vp,导航点箭头颜色设为蓝色。

Swiper() {
  // ...
}
.displayArrow({ 
  showBackground: true,
  isSidebarMiddle: true,
  backgroundSize: 24,
  backgroundColor: Color.White,
  arrowSize: 18,
  arrowColor: Color.Blue
  }, false)

页面切换方式

Swiper支持手指滑动、点击导航点和通过控制器三种方式切换页面,以下示例展示通过控制器切换页面的方法。

@Entry
@Component
struct SwiperDemo {
  private swiperController: SwiperController = new SwiperController();

  build() {
    Column({ space: 5 }) {
      Swiper(this.swiperController) {
        Text('0')
          .width(250)
          .height(250)
          .backgroundColor(Color.Gray)
          .textAlign(TextAlign.Center)
          .fontSize(30)
        Text('1')
          .width(250)
          .height(250)
          .backgroundColor(Color.Green)
          .textAlign(TextAlign.Center)
          .fontSize(30)
        Text('2')
          .width(250)
          .height(250)
          .backgroundColor(Color.Pink)
          .textAlign(TextAlign.Center)
          .fontSize(30)
      }
      .indicator(true)

      Row({ space: 12 }) {
        Button('showNext')
          .onClick(() => {
            this.swiperController.showNext(); // 通过controller切换到后一页
          })
        Button('showPrevious')
          .onClick(() => {
            this.swiperController.showPrevious(); // 通过controller切换到前一页
          })
      }.margin(5)
    }.width('100%')
    .margin({ top: 5 })
  }
}

轮播方向

Swiper支持水平和垂直方向上进行轮播,主要通过vertical属性控制。

当vertical为true时,表示在垂直方向上进行轮播;为false时,表示在水平方向上进行轮播。vertical默认值为false。

  • 设置水平方向上轮播。
Swiper() {
  // ...
}
.indicator(true)
.vertical(false)

  • 设置垂直方向轮播。
Swiper() {
  // ...
}
.indicator(true)
.vertical(true)

每页显示多个子页面

Swiper支持在一个页面内同时显示多个子组件,通过displayCount属性设置。

Swiper() {
  Text('0')
    .width(250)
    .height(250)
    .backgroundColor(Color.Gray)
    .textAlign(TextAlign.Center)
    .fontSize(30)
  Text('1')
    .width(250)
    .height(250)
    .backgroundColor(Color.Green)
    .textAlign(TextAlign.Center)
    .fontSize(30)
  Text('2')
    .width(250)
    .height(250)
    .backgroundColor(Color.Pink)
    .textAlign(TextAlign.Center)
    .fontSize(30)
  Text('3')
    .width(250)
    .height(250)
    .backgroundColor(Color.Blue)
    .textAlign(TextAlign.Center)
    .fontSize(30)
}
.indicator(true)
.displayCount(2)

自定义切换动画

Swiper支持通过customContentTransition设置自定义切换动画,可以在回调中对视窗内所有页面逐帧设置透明度、缩放比例、位移、渲染层级等属性实现自定义切换动画。

@Entry
@Component
struct SwiperCustomAnimationExample {
  private DISPLAY_COUNT: number = 2
  private MIN_SCALE: number = 0.75

  @State backgroundColors: Color[] = [Color.Green, Color.Blue, Color.Yellow, Color.Pink, Color.Gray, Color.Orange]
  @State opacityList: number[] = []
  @State scaleList: number[] = []
  @State translateList: number[] = []
  @State zIndexList: number[] = []

  aboutToAppear(): void {
    for (let i = 0; i < this.backgroundColors.length; i++) {
      this.opacityList.push(1.0)
      this.scaleList.push(1.0)
      this.translateList.push(0.0)
      this.zIndexList.push(0)
    }
  }

  build() {
    Column() {
      Swiper() {
        ForEach(this.backgroundColors, (backgroundColor: Color, index: number) => {
          Text(index.toString()).width('100%').height('100%').fontSize(50).textAlign(TextAlign.Center)
            .backgroundColor(backgroundColor)
            .opacity(this.opacityList[index])
            .scale({ x: this.scaleList[index], y: this.scaleList[index] })
            .translate({ x: this.translateList[index] })
            .zIndex(this.zIndexList[index])
        })
      }
      .height(300)
      .indicator(false)
      .displayCount(this.DISPLAY_COUNT, true)
      .customContentTransition({
        timeout: 1000,
        transition: (proxy: SwiperContentTransitionProxy) => {
          if (proxy.position <= proxy.index % this.DISPLAY_COUNT || proxy.position >= this.DISPLAY_COUNT + proxy.index % this.DISPLAY_COUNT) {
            // 同组页面完全滑出视窗外时,重置属性值
            this.opacityList[proxy.index] = 1.0
            this.scaleList[proxy.index] = 1.0
            this.translateList[proxy.index] = 0.0
            this.zIndexList[proxy.index] = 0
          } else {
            // 同组页面未滑出视窗外时,对同组中左右两个页面,逐帧根据position修改属性值
            if (proxy.index % this.DISPLAY_COUNT === 0) {
              this.opacityList[proxy.index] = 1 - proxy.position / this.DISPLAY_COUNT
              this.scaleList[proxy.index] = this.MIN_SCALE + (1 - this.MIN_SCALE) * (1 - proxy.position / this.DISPLAY_COUNT)
              this.translateList[proxy.index] = - proxy.position * proxy.mainAxisLength + (1 - this.scaleList[proxy.index]) * proxy.mainAxisLength / 2.0
            } else {
              this.opacityList[proxy.index] = 1 - (proxy.position - 1) / this.DISPLAY_COUNT
              this.scaleList[proxy.index] = this.MIN_SCALE + (1 - this.MIN_SCALE) * (1 - (proxy.position - 1) / this.DISPLAY_COUNT)
              this.translateList[proxy.index] = - (proxy.position - 1) * proxy.mainAxisLength - (1 - this.scaleList[proxy.index]) * proxy.mainAxisLength / 2.0
            }
            this.zIndexList[proxy.index] = -1
          }
        }
      })
    }.width('100%')
  }
}

10.选项卡 (Tabs)

当页面信息较多时,为了让用户能够聚焦于当前显示的内容,需要对页面内容进行分类,提高页面空间利用率。Tabs组件可以在一个页面内快速实现视图内容的切换,一方面提升查找信息的效率,另一方面精简用户单次获取到的信息量。

基本布局

Tabs组件的页面组成包含两个部分,分别是TabContent和TabBar。TabContent是内容页,TabBar是导航页签栏,页面结构如下图所示,根据不同的导航类型,布局会有区别,可以分为底部导航、顶部导航、侧边导航,其导航栏分别位于底部、顶部和侧边。

Tabs组件布局示意图

说明

  • TabContent组件不支持设置通用宽度属性,其宽度默认撑满Tabs父组件。

  • TabContent组件不支持设置通用高度属性,其高度由Tabs父组件高度与TabBar组件高度决定。

Tabs使用花括号包裹TabContent,如图2,其中TabContent显示相应的内容页。

Tabs与TabContent使用

每一个TabContent对应的内容需要有一个页签,可以通过TabContent的tabBar属性进行配置。在如下TabContent组件上设置tabBar属性,可以设置其对应页签中的内容,tabBar作为内容的页签。

 TabContent() {
   Text('首页的内容').fontSize(30)
 }
.tabBar('首页')

设置多个内容时,需在Tabs内按照顺序放置。

Tabs() {
  TabContent() {
    Text('首页的内容').fontSize(30)
  }
  .tabBar('首页')

  TabContent() {
    Text('推荐的内容').fontSize(30)
  }
  .tabBar('推荐')

  TabContent() {
    Text('发现的内容').fontSize(30)
  }
  .tabBar('发现')
  
  TabContent() {
    Text('我的内容').fontSize(30)
  }
  .tabBar("我的")
}
底部导航

底部导航是应用中最常见的一种导航方式。底部导航位于应用一级页面的底部,用户打开应用,能够分清整个应用的功能分类,以及页签对应的内容,并且其位于底部更加方便用户单手操作。底部导航一般作为应用的主导航形式存在,其作用是将用户关心的内容按照功能进行分类,迎合用户使用习惯,方便在不同模块间的内容切换。

底部导航栏

导航栏位置使用Tabs的barPosition参数进行设置。默认情况下,导航栏位于顶部,此时,barPosition为BarPosition.Start。设置为底部导航时,需要将barPosition设置为BarPosition.End。

Tabs({ barPosition: BarPosition.End }) {
  // TabContent的内容:首页、发现、推荐、我的
  ...
}
顶部导航

当内容分类较多,用户对不同内容的浏览概率相差不大,需要经常快速切换时,一般采用顶部导航模式进行设计,作为对底部导航内容的进一步划分,常见一些资讯类应用对内容的分类为关注、视频、数码,或者主题应用中对主题进行进一步划分为图片、视频、字体等。

顶部导航栏

Tabs({ barPosition: BarPosition.Start }) {
  // TabContent的内容:关注、视频、游戏、数码、科技、体育、影视
  ...
}
侧边导航

侧边导航是应用较为少见的一种导航模式,更多适用于横屏界面,用于对应用进行导航操作,由于用户的视觉习惯是从左到右,侧边导航栏默认为左侧侧边栏。

实现侧边导航栏需要将Tabs的vertical属性设置为true,vertical默认值为false,表明内容页和导航栏垂直方向排列。

Tabs({ barPosition: BarPosition.Start }) {
  // TabContent的内容:首页、发现、推荐、我的
  ...
}
.vertical(true)
.barWidth(100)
.barHeight(200)

说明

  • vertical为false时,tabbar的宽度默认为撑满屏幕的宽度,需要设置barWidth为合适值。

  • vertical为true时,tabbar的高度默认为实际内容的高度,需要设置barHeight为合适值。

限制导航栏的滑动切换

默认情况下,导航栏都支持滑动切换,在一些内容信息量需要进行多级分类的页面,如支持底部导航+顶部导航组合的情况下,底部导航栏的滑动效果与顶部导航出现冲突,此时需要限制底部导航的滑动,避免引起不好的用户体验。

限制底部导航栏滑动

控制滑动切换的属性为scrollable,默认值为true,表示可以滑动,若要限制滑动切换页签则需要设置为false。

Tabs({ barPosition: BarPosition.End }) {
  TabContent(){
    Column(){
      Tabs(){
        // 顶部导航栏内容
        ...
      }
    }
    .backgroundColor('#ff08a8f1')
    .width('100%')
  }
  .tabBar('首页')

  // 其他TabContent内容:发现、推荐、我的
  ...
}
.scrollable(false)
固定导航栏

当内容分类较为固定且不具有拓展性时,例如底部导航内容分类一般固定,分类数量一般在3-5个,此时使用固定导航栏。固定导航栏不可滚动,无法被拖拽滚动,内容均分tabBar的宽度

Tabs的barMode属性用于控制导航栏是否可以滚动,默认值为BarMode.Fixed。

Tabs({ barPosition: BarPosition.End }) {
  // TabContent的内容:首页、发现、推荐、我的
  ...
}
.barMode(BarMode.Fixed)
滚动导航栏

滚动导航栏可以用于顶部导航栏或者侧边导航栏的设置,内容分类较多,屏幕宽度无法容纳所有分类页签的情况下,需要使用可滚动的导航栏,支持用户点击和滑动来加载隐藏的页签内容。

滚动导航栏需要设置Tabs组件的barMode属性,默认值为BarMode.Fixed表示为固定导航栏,BarMode.Scrollable表示可滚动导航栏。

Tabs({ barPosition: BarPosition.Start }) {
  // TabContent的内容:关注、视频、游戏、数码、科技、体育、影视、人文、艺术、自然、军事
  ...
}
.barMode(BarMode.Scrollable)
自定义导航栏

对于底部导航栏,一般作为应用主页面功能区分,为了更好的用户体验,会组合文字以及对应语义图标表示页签内容,这种情况下,需要自定义导航页签的样式。

系统默认情况下采用了下划线标志当前活跃的页签,而自定义导航栏需要自行实现相应的样式,用于区分当前活跃页签和未活跃页签。

设置自定义导航栏需要使用tabBar的参数,以其支持的CustomBuilder的方式传入自定义的函数组件样式。例如这里声明tabBuilder的自定义函数组件,传入参数包括页签文字title,对应位置index,以及选中状态和未选中状态的图片资源。通过当前活跃的currentIndex和页签对应的targetIndex匹配与否,决定UI显示的样式。

@Builder tabBuilder(title: string, targetIndex: number, selectedImg: Resource, normalImg: Resource) {
  Column() {
    Image(this.currentIndex === targetIndex ? selectedImg : normalImg)
      .size({ width: 25, height: 25 })
    Text(title)
      .fontColor(this.currentIndex === targetIndex ? '#1698CE' : '#6B6B6B')
  }
  .width('100%')
  .height(50)
  .justifyContent(FlexAlign.Center)
}

在TabContent对应tabBar属性中传入自定义函数组件,并传递相应的参数。

TabContent() {
  Column(){
    Text('我的内容')  
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#007DFF')
}
.tabBar(this.tabBuilder('我的', 0, $r('app.media.mine_selected'), $r('app.media.mine_normal')))
切换至指定页签

在不使用自定义导航栏时,默认的Tabs会实现切换逻辑。在使用了自定义导航栏后,默认的Tabs仅实现滑动内容页和点击页签时内容页的切换逻辑,页签切换逻辑需要自行实现。即用户滑动内容页和点击页签时,页签栏需要同步切换至内容页对应的页签。

 内容页和页签不联动

此时需要使用Tabs提供的onChange事件方法,监听索引index的变化,并将当前活跃的index值传递给currentIndex,实现页签的切换。

@Entry
@Component
struct TabsExample1 {
  @State currentIndex: number = 2

  @Builder tabBuilder(title: string, targetIndex: number) {
    Column() {
      Text(title)
        .fontColor(this.currentIndex === targetIndex ? '#1698CE' : '#6B6B6B')
    }
  }

  build() {
    Column() {
      Tabs({ barPosition: BarPosition.End }) {
        TabContent() {
          ...
        }.tabBar(this.tabBuilder('首页', 0))

        TabContent() {
          ...
        }.tabBar(this.tabBuilder('发现', 1))

        TabContent() {
          ...
        }.tabBar(this.tabBuilder('推荐', 2))

        TabContent() {
          ...
        }.tabBar(this.tabBuilder('我的', 3))
      }
      .animationDuration(0)
      .backgroundColor('#F1F3F5')
      .onChange((index: number) => {
        this.currentIndex = index
      })
    }.width('100%')
  }
}

内容页和页签联动

若希望不滑动内容页和点击页签也能实现内容页和页签的切换,可以将currentIndex传给Tabs的index参数,通过改变currentIndex来实现跳转至指定索引值对应的TabContent内容。也可以使用TabsController,TabsController是Tabs组件的控制器,用于控制Tabs组件进行内容页切换。通过TabsController的changeIndex方法来实现跳转至指定索引值对应的TabContent内容。

@State currentIndex: number = 2
private controller: TabsController = new TabsController()

Tabs({ barPosition: BarPosition.End, index: this.currentIndex, controller: this.controller }) {
  ...
}
.height(600)
.onChange((index: number) => {
   this.currentIndex = index
})

Button('动态修改index').width('50%').margin({ top: 20 })
  .onClick(()=>{
    this.currentIndex = (this.currentIndex + 1) % 4
})

Button('changeIndex').width('50%').margin({ top: 20 })
  .onClick(()=>{
    let index = (this.currentIndex + 1) % 4
    this.controller.changeIndex(index)
})

切换指定页签

开发者可以通过Tabs组件的onContentWillChange接口,设置自定义拦截回调函数。拦截回调函数在下一个页面即将展示时被调用,如果回调返回true,新页面可以展示;如果回调返回false,新页面不会展示,仍显示原来页面。

Tabs({ barPosition: BarPosition.End, controller: this.controller, index: this.currentIndex }) {...}
.onContentWillChange((currentIndex, comingIndex) => {
  if (comingIndex == 2) {
    return false
  }
  return true
})

支持开发者自定义页面切换拦截事件

### ArkTS 底层容器实现原理 ArkTSHarmonyOS 中用于构建用户界面的一种声明式编程语言。其底层容器技术涉及多个方面,包括但不限于内存管理、对象生命周期管理和布局引擎等。 #### 容器的数据结构设计 为了高效支持UI组件的动态更新和渲染,ArkTS 使用了一种基于树形结构的设计来表示视图层次[^1]。这种树形结构不仅有助于快速定位特定节点,还便于进行局部刷新操作而无需重绘整个页面。具体来说: - **虚拟DOM**:类似于Web前端框架中的概念,在ArkTS里也有类似的机制用来优化性能并减少不必要的绘制开销。 ```typescript class VirtualNode { type: string; props: any; children: Array<VirtualNode>; constructor(type, props, children){ this.type = type; this.props = props || {}; this.children = children || []; } } ``` - **Diff算法**:当状态发生变化时,系统会比较新旧两棵虚拟DOM树之间的差异,并仅对有变化的部分做实际修改[^2]。这一步骤对于提高应用响应速度至关重要。 #### 对象生命周期管理 考虑到移动设备资源有限的特点,ArkTS 非常重视如何有效地管理各个组件及其关联的对象实例的生命期。为此引入了一些特殊的标记位(如`key`属性),确保即使是在列表项频繁增删的情况下也能保持良好的用户体验。 ```typescript // 组件定义示例 @Component({ template: `<div key="{{item.id}}">{{item.name}}</div>`, }) class MyComponent {} ``` #### 布局与渲染引擎集成 最后但同样重要的是,这些精心设计的数据结构最终会被传递给负责物理像素输出的渲染管道处理。在这个过程中,ArkTS 的编译工具链会对源代码进行一系列转换,使其能够更好地适应目标平台的具体需求[^4]。 通过上述措施,ArkTS 成功实现了既灵活又高效的UI开发体验,同时也为开发者提供了足够的控制力去定制所需的功能特性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值