ArkUI组件

布局

布局指用特定的组件或者属性来管理用户页面所放置UI组件的大小和位置。在实际的开发过程中,需要遵守以下流程保证整体的布局效果:

  • 确定页面的布局结构。
  • 分析页面中的元素构成。
  • 选用适合的布局容器组件或属性控制页面中各个元素的位置和大小约束。

布局结构

布局的结构通常是分层级的,代表了用户界面中的整体架构。一个常见的页面结构如下

为实现上述效果,开发者需要在页面中声明对应的元素。其中,Page表示页面的根节点,Column/Row等元素为系统组件。针对不同的页面结构,ArkUI提供了不同的布局组件来帮助开发者实现对应布局的效果。声明式UI提供了以9种常见布局,开发者可根据实际应用场景选择合适的布局进行页面开发。

官网地址

布局

应用场景

线性布局(Row、Column)

如果布局内子元素超过1个,且能够以某种方式线性排列时优先考虑此布局。

层叠布局(Stack)

组件需要有堆叠效果时优先考虑此布局,层叠布局的堆叠效果不会占用或影响其他同容器内子组件的布局空间。例如:Panel作为子组件弹出时将其他组件覆盖更为合理,则优先考虑在外层使用堆叠布局。

弹性布局(Flex)

弹性布局是与线性布局类似的布局方式。区别在于弹性布局默认能够使子组件压缩或拉伸。在子组件需要计算拉伸或压缩比例时优先使用此布局,可使得多个容器内子组件能有更好的视觉上的填充容器效果

相对布局(RelativeContainer)

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

栅格布局(GridRow、GridCol

栅格是多设备场景下通用的辅助定位工具,通过将空间分割为有规律的栅格。栅格不同于网格布局固定的空间划分,它可以实现不同设备下不同的布局,空间划分更随心所欲,从而显著降低适配不同屏幕尺寸的设计及开发成本,使得整体设计和开发流程更有秩序和节奏感,同时也保证多设备上应用显示的协调性和一致性,提升用户体验。推荐手机、大屏、平板等不同设备,内容相同但布局不同时使用

媒体查询(@ohos.mediaquery)

媒体查询可根据不同设备类型或同设备不同状态修改应用的样式。例如根据设备和应用的不同属性信息设计不同的布局,以及屏幕发生动态改变时更新应用的页面布局

列表(List)

使用列表可以轻松高效地显示结构化、可滚动的信息。在ArkUI中,列表具有垂直和水平布局能力和自适应交叉轴方向上排列个数的布局能力,超出屏幕时可以滚动。列表适合用于呈现同类数据类型或数据类型集,例如图片和文本。

网格(Grid)

网格布局具有较强的页面均分能力,子组件占比控制能力,是一种重要自适应布局。网格布局可以控制元素所占的网格数量、设置子组件横跨几行或者几列,当网格容器尺寸发生变化时,所有子组件以及间距等比例调整。推荐在需要按照固定比例或者均匀分配空间的布局场景下使用,例如计算器、相册、日历等

轮播(Swiper)

轮播组件通常用于实现广告轮播、图片预览、可滚动应用等。

选项卡(Tabs)

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

线性布局

线性布局(LinearLayout)是开发中最常用的布局,通过线性容器Row和Column构建。

线性布局是其他布局的基础,其子元素在线性方向上(水平方向和垂直方向)依次排列。

线性布局的排列方向由所选容器组件决定,Column容器内子元素按照垂直方向排列,Row容器内子元素按照水平方向排列。

根据不同的排列方向,开发者可选择使用Row或Column容器创建线性布局。
 

Row容器内子元素排列

build() {
  Row(){
    Row().width('20%').height(400).backgroundColor(Color.Green)
    Row().width('20%').height(400).backgroundColor(Color.Yellow)
    Row().width('20%').height(400).backgroundColor(Color.Green)
    Row().width('20%').height(400).backgroundColor(Color.Yellow)
    Row().width('20%').height(400).backgroundColor(Color.Green)
  }
}

Column容器内子元素排列

build() {
  Column(){
   Column().width('100%').height(50).backgroundColor(Color.Green)
   Column().width('100%').height(50).backgroundColor(Color.Yellow)
   Column().width('100%').height(50).backgroundColor(Color.Green)
   Column().width('100%').height(50).backgroundColor(Color.Yellow)
   Column().width('100%').height(50).backgroundColor(Color.Green)
    }
  }

布局子元素在排列方向上的间距

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

Column容器内排列方向上的间距
build() {
  Column({ space: 40 }) {
   Row().width('90%').height(50).backgroundColor(Color.Green)
   Row().width('90%').height(50).backgroundColor(Color.Green)
   Row().width('90%').height(50).backgroundColor(Color.Green)
   }.width('100%')
  }

Row容器内排列方向上的间距
Row({ space: 45 }) {
   Row().width('10%').height(400).backgroundColor(Color.Green)
   Row().width('10%').height(400).backgroundColor(Color.Green)
   Row().width('10%').height(400).backgroundColor(Color.Green)
   Row().width('10%').height(400).backgroundColor(Color.Green)
   Row().width('10%').height(400).backgroundColor(Color.Green)
}.width('100%')

布局子元素在交叉轴上的对齐方式

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

Column容器内子元素在水平方向上的排列

HorizontalAlign取值:Start、Center、End

Column({}) {
 Column() {
  }.width('80%').height(50).backgroundColor(0xF5DEB3)


 Column() {
  }.width('80%').height(50).backgroundColor(0xD2B48C)


 Column() {
  }.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').alignItems(HorizontalAlign.Start).backgroundColor('rgb(242,242,242)')

Row容器内子元素在垂直方向上的排列

VerticalAlign取值:Top、Center、Bottom

Row({}) {
 Column() {
  }.width('20%').height(30).backgroundColor(0xF5DEB3)


 Column() {
  }.width('20%').height(30).backgroundColor(0xD2B48C)


 Column() {
  }.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).alignItems(VerticalAlign.Top).backgroundColor('rgb(242,242,242)')

布局子元素在主轴上的排列方式

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

Column容器内子元素在垂直方向上的排列

Column({}) {
 Column() {
  }.width('80%').height(50).backgroundColor(0xF5DEB3)


 Column() {
  }.width('80%').height(50).backgroundColor(0xD2B48C)


 Column() {
  }.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').height(300).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.Start)

Row容器内子元素在水平方向上的排列

Row({}) {
 Column() {
  }.width('20%').height(30).backgroundColor(0xF5DEB3)


 Column() {
  }.width('20%').height(30).backgroundColor(0xD2B48C)


 Column() {
  }.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.Start)

自适应拉伸

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

@Entry
@Component
struct BlankExample {
 build() {
  Column() {
   Row() {
    Text('Bluetooth').fontSize(18)
    Blank()
    Toggle({ type: ToggleType.Switch, isOn: true })
    }.backgroundColor(0xFFFFFF).borderRadius(15).padding({ left: 12 }).width('100%')
   }.backgroundColor(0xEFEFEF).padding(20).width('100%')
  }
}

自适应缩放

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

  • 父容器尺寸确定时,使用layoutWeight属性设置子组件和兄弟元素在主轴上的权重,忽略元素本身尺寸设置,使它们在任意尺寸的设备下自适应占满剩余空间
  • 父容器尺寸确定时,使用百分比设置子组件和兄弟元素的宽度,使他们在任意尺寸的设备下保持固定的自适应占比
Row() {
   Column() {
    Text('layoutWeight(1)')
      .textAlign(TextAlign.Center)
    }.layoutWeight(1).backgroundColor(0xF5DEB3).height('100%')


   Column() {
    Text('layoutWeight(2)')
      .textAlign(TextAlign.Center)
    }.layoutWeight(2).backgroundColor(0xD2B48C).height('100%')


   Column() {
    Text('layoutWeight(3)')
      .textAlign(TextAlign.Center)
    }.layoutWeight(3).backgroundColor(0xF5DEB3).height('100%')


   }.height('30%')

自适应延伸

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

  • 在List中添加滚动条:当List子项过多一屏放不下时,可以将每一项子元素放置在不同的组件中,通过滚动条进行拖动展示。可以通过scrollBar属性设置滚动条的常驻状态,edgeEffect属性设置拖动到内容最末端的回弹效果。
  • 使用Scroll组件:在线性布局中,开发者可以进行垂直方向或者水平方向的布局。当一屏无法完全显示时,可以在Column或Row组件的外层包裹一个可滚动的容器组件Scroll来实现可滑动的线性布局。
垂直方向布局中使用Scroll组件

@Entry
@Component
struct ScrollExample {
 scroller: Scroller = new Scroller();
 private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];


 build() {
  Scroll(this.scroller) {
   Column() {
    ForEach(this.arr, (item) => {
     Text(item.toString())
       .width('90%')
       .height(150)
       .backgroundColor(0xFFFFFF)
       .borderRadius(15)
       .fontSize(16)
       .textAlign(TextAlign.Center)
       .margin({ top: 10 })
     }, item => item)
    }.width('100%')
   }
   .backgroundColor(0xDCDCDC)
   .scrollable(ScrollDirection.Vertical) // 滚动方向为垂直方向
   .scrollBar(BarState.On) // 滚动条常驻显示
   .scrollBarColor(Color.Gray) // 滚动条颜色
   .scrollBarWidth(10) // 滚动条宽度
   .edgeEffect(EdgeEffect.Spring) // 滚动到边沿后回弹
  }
}

水平方向布局中使用Scroll组件

@Entry
@Component
struct ScrollExample {
 scroller: Scroller = new Scroller();
 private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];


 build() {
  Scroll(this.scroller) {
   Row() {
    ForEach(this.arr, (item) => {
     Text(item.toString())
       .height('90%')
       .width(150)
       .backgroundColor(0xFFFFFF)
       .borderRadius(15)
       .fontSize(16)
       .textAlign(TextAlign.Center)
       .margin({ left: 10 })
     })
    }.height('100%')
   }
   .backgroundColor(0xDCDCDC)
   .scrollable(ScrollDirection.Horizontal) // 滚动方向为水平方向
   .scrollBar(BarState.On) // 滚动条常驻显示
   .scrollBarColor(Color.Gray) // 滚动条颜色
   .scrollBarWidth(10) // 滚动条宽度
   .edgeEffect(EdgeEffect.Spring) // 滚动到边沿后回弹
  }
}

层叠布局(Stack)

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

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

开发布局

Stack组件为容器组件,容器内可包含各种子组件。其中子组件默认进行居中堆叠。子元素被约束在Stack下,进行自己的样式定义以及排列。

Column(){
  Stack(){
    Column(){}.width("90%").height("100%").backgroundColor(Color.Red)
    Text('文本组件').width("60%").height("60%").backgroundColor(Color.Blue).fontColor(Color.White)
    Button("按钮").width("30%").height("30%").backgroundColor(Color.Green).fontSize(20)
   }.width("100%").height(200)
}

let MTop:Record<string,number> = { 'top': 50 };

@Entry
@Component
struct Index {
  build() {
    Column({ space: 20 }) {
      Stack({}) {
        Column(){}.width('90%').height('100%').backgroundColor('#ff58b87c')
        Text('text').width('60%').height('60%').backgroundColor('#ffc3f6aa')
        Button('button').width('30%').height('30%').backgroundColor('#ff8ff3eb').fontColor('#000')
      }.width('100%').height(150).margin(MTop)
    }.width('100%')
  }
}

对齐方式

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

Column(){
  Stack({alignContent:Alignment.TopStart}){
    Column(){}.width("90%").height("100%").backgroundColor(Color.Red)
    Text('文本组件').width("60%").height("60%").backgroundColor(Color.Blue).fontColor(Color.White)
    Button("按钮").width("30%").height("30%").backgroundColor(Color.Green).fontSize(20)
   }.width("100%").height(200)
}

Z序控制

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

Column(){
  Stack({alignContent:Alignment.TopStart}){
    Column(){}.width("90%").height("100%").backgroundColor(Color.Red).zIndex(10)
    Text('文本组件').width("60%").height("60%").backgroundColor(Color.Blue).fontColor(Color.White).zIndex(11)
    Button("按钮").width("30%").height("30%").backgroundColor(Color.Green).fontSize(20).zIndex(12)
   }.width("100%").height(200)
}

场景示例

使用层叠布局快速搭建页面。

@Entry
@Component
struct Index {
  private arr: string[] = ['APP1', 'APP2', 'APP3', 'APP4', 'APP5', 'APP6', 'APP7', 'APP8'];

  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(this.arr, (item:string) => {
          Text(item)
            .width(100)
            .height(100)
            .fontSize(16)
            .margin(10)
            .textAlign(TextAlign.Center)
            .borderRadius(10)
            .backgroundColor(0xFFFFFF)
        }, (item:string):string => item)
      }.width('100%').height('100%')
      Flex({ justifyContent: FlexAlign.SpaceAround, alignItems: ItemAlign.Center }) {
        Text('联系人').fontSize(16)
        Text('设置').fontSize(16)
        Text('短信').fontSize(16)
      }
      .width('50%')
      .height(50)
      .backgroundColor('#16302e2e')
      .margin({ bottom: 15 })
      .borderRadius(15)
    }.width('100%').height('100%').backgroundColor('#CFD0CF')
  }
}

弹性布局

弹性布局(Flex)提供更加有效的方式对容器中的子元素进行排列、对齐和分配剩余空间。

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

弹性布局在开发场景中用例特别多,比如页面头部导航栏的均匀分布、页面框架的搭建、多行数据的排列等等


 

基本概念

  • 主轴:Flex组件布局方向的轴线,子元素默认沿着主轴排列。主轴开始的位置称为主轴起始点,结束位置称为主轴结束点。
  • 交叉轴:垂直于主轴方向的轴线。交叉轴开始的位置称为交叉轴起始点,结束位置称为交叉轴结束点。
Flex() {
  Text('1').width('33%').height(80).backgroundColor(0xF5DEB3).fontSize(40)
  Text('2').width('33%').height(80).backgroundColor(0xD2B48C).fontSize(40)
  Text('3').width('33%').height(80).backgroundColor(0xF5DEB3).fontSize(40)
}
.height(100)
  .width('100%')
  .padding(10)
  .backgroundColor(0xAFEEEE)

布局方向

在弹性布局中,容器的子元素可以按照任意方向排列。

通过设置参数direction,可以决定主轴的方向,从而控制子组件的排列方向

  • FlexDirection.Row(默认值):主轴为水平方向,子元素从起始端沿着水平方向开始排布。
  • FlexDirection.RowReverse:主轴为水平方向,子元素从终点端沿着FlexDirection. Row相反的方向开始排布。
  • FlexDirection.Column:主轴为垂直方向,子元素从起始端沿着垂直方向开始排布。
  • FlexDirection.ColumnReverse:主轴为垂直方向,子元素从终点端沿着FlexDirection. Column相反的方向开始排布。

Flex({ direction: FlexDirection.Row }) {
  Text('1').width('33%').height(80).backgroundColor(0xF5DEB3).fontSize(40)
  Text('2').width('33%').height(80).backgroundColor(0xD2B48C).fontSize(40)
  Text('3').width('33%').height(80).backgroundColor(0xF5DEB3).fontSize(40)
  }
  .height(100)
  .width('100%')
  .padding(10)
  .backgroundColor(0xAFEEEE)

布局换行

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

  • FlexWrap. NoWrap(默认值):不换行。如果子元素的宽度总和大于父元素的宽度,则子元素会被压缩宽度。
  • FlexWrap. Wrap:换行,每一行子元素按照主轴方向排列。
  • FlexWrap. WrapReverse:换行,每一行子元素按照主轴反方向排列。

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)
  }
  .width('90%')
  .padding(10)
  .backgroundColor(0xAFEEEE)

主轴对齐方式

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

  • FlexAlign.Start(默认值):子元素在主轴方向起始端对齐, 第一个子元素与父元素边沿对齐,其他元素与前一个元素对齐。
  • FlexAlign.Center:子元素在主轴方向居中对齐。
  • FlexAlign.End:子元素在主轴方向终点端对齐, 最后一个子元素与父元素边沿对齐,其他元素与后一个元素对齐。
  • FlexAlign.SpaceBetween:Flex主轴方向均匀分配弹性元素,相邻子元素之间距离相同。第一个子元素和最后一个子元素与父元素边沿对齐。
  • FlexAlign.SpaceAround:Flex主轴方向均匀分配弹性元素,相邻子元素之间距离相同。第一个子元素到主轴起始端的距离和最后一个子元素到主轴终点端的距离是相邻元素之间距离的一半。
  • FlexAlign.SpaceEvenly:Flex主轴方向元素等间距布局,相邻子元素之间的间距、第一个子元素与主轴起始端的间距、最后一个子元素到主轴终点端的间距均相等。

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)
}
.width('90%')
.padding({ top: 10, bottom: 10 })
.backgroundColor(0xAFEEEE)

交叉轴对齐方式

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

容器组件设置交叉轴对齐

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

  • ItemAlign.Auto:使用Flex容器中默认配置。
  • ItemAlign.Start:交叉轴方向首部对齐
  • ItemAlign.Center:交叉轴方向居中对齐
  • ItemAlign.End:交叉轴方向底部对齐
  • ItemAlign.Stretch:交叉轴方向拉伸填充,拉伸到容器尺寸
  • ItemAlign. Baseline:交叉轴方向文本基线对齐
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)
    }
    .size({ width: '90%', height: 80 })
    .padding(10)
    .backgroundColor(0xAFEEEE)

子组件设置交叉轴对齐

子组件的alignSelf属性也可以设置子组件在父容器交叉轴的对齐格式,且会覆盖Flex布局容器中alignItems配置(权重大)

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

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

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

内容对齐

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

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

自适应拉伸

在弹性布局父组件尺寸不够大的时候,通过子组件的下面几个属性设置其在父容器的占比,达到自适应布局能力

  • flexBasis:设置子组件在父容器主轴方向上的基准尺寸。如果设置了该值,则子项占用的空间为设置的值;如果没设置该属性,那子项的空间为width/height的值
  • flexGrow:设置父容器的剩余空间分配给此属性所在组件的比例。用于“瓜分”父组件的剩余空间
  • flexShrink: 当父容器空间不足时,子组件的压缩比例
Flex() {
 Text('flexBasis("auto")')
   .flexBasis('auto') // 未设置width以及flexBasis值为auto,内容自身宽度
   .height(100)
   .backgroundColor(0xF5DEB3)
 Text('flexBasis("auto")' + ' width("40%")')
   .width('40%')
   .flexBasis('auto') //设置width以及flexBasis值auto,使用width的值
   .height(100)
   .backgroundColor(0xD2B48C)


 Text('flexBasis(100)') // 未设置width以及flexBasis值为100,宽度为100vp
   .fontSize(15)
   .flexBasis(100)
   .height(100)
   .backgroundColor(0xF5DEB3)


 Text('flexBasis(100)')
   .fontSize(15)
   .flexBasis(100)
   .width(200) // flexBasis值为100,覆盖width的设置值,宽度为100vp
   .height(100)
   .backgroundColor(0xD2B48C)
}.width('90%').height(120).padding(10).backgroundColor(0xAFEEEE)

vp概念

vp:虚拟像素(virtual pixel)是一台设备针对应用而言所具有的虚拟尺寸(区别于屏幕硬件本身的像素单位)。它提供了一种灵活的方式来适应不同屏幕密度的显示效果。

使用虚拟像素vp,使元素在不同密度的设备上具有一致的视觉体量

flexGrow瓜分标准

父容器宽度420vp,三个子元素原始宽度为100vp,左右padding为20vp,总和320vp,剩余空间100vp根据flexGrow值的占比分配给子元素,未设置flexGrow的子元素不参与“瓜分”。

第一个元素以及第二个元素以2:3分配剩下的100vp。第一个元素为100vp+100vp2/5=140vp,第二个元素为100vp+100vp3/5=160vp。

场景示例

使用弹性布局,可以实现子元素沿水平方向排列,两端对齐,子元素间距平分,垂直方向上子元素居中的效果。

@Entry  
@Component
struct FlexExample {
  build() {
    Column() {
      Column({ space: 5 }) {
        Flex({ direction: FlexDirection.Row, wrap: FlexWrap.NoWrap, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
          Text('1').width('30%').height(50).backgroundColor(0xF5DEB3)
          Text('2').width('30%').height(50).backgroundColor(0xD2B48C)
          Text('3').width('30%').height(50).backgroundColor(0xF5DEB3)
        }
        .height(70)
        .width('90%')
        .backgroundColor(0xAFEEEE)
      }.width('100%').margin({ top: 5 })
    }.width('100%') 
 }
}

相对布局

RelativeContainer是采用相对布局的容器,支持容器内部的子元素设置相对位置关系。子元素支持指定兄弟元素作为锚点,也支持指定父容器作为锚点,基于锚点做相对位置布局。下图是一个RelativeContainer的概念图,图中的虚线表示位置的依赖关系

子元素并不完全是上图中的依赖关系。比如,Item4可以以Item2为依赖锚点,也可以以RelativeContainer父容器为依赖锚点。

基本概念

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

设置依赖关系

锚点设置
  • 锚点设置是指设置子元素相对于父元素或兄弟元素的位置依赖关系。在水平方向上,可以设置left、middle、right的锚点。在竖直方向上,可以设置top、center、bottom的锚点。
  • 为了明确定义锚点,必须为RelativeContainer及其子元素设置ID,用于指定锚点信息。ID默认为__container__,其余子元素的ID通过id属性设置。未设置ID的子元素在RelativeContainer中不会显示。
  • 互相依赖,环形依赖时容器内子组件全部不绘制。同方向上两个以上位置设置锚点,但锚点位置逆序时此子组件大小为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' }
RelativeContainer() {
  Row().width(100).height(100)
    .backgroundColor("#FF3333")
    .alignRules(AlignRus)
    .id("row1")

  Row().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' }
RelativeContainer() {
  Row().width(100).height(100)
    .backgroundColor("#FF3333")
    .alignRules(AlignRus)
    .id("row1")

  Row().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().width(100).height(100)
          .backgroundColor('#ff3339ff')
          .alignRules({
            top: {anchor: "__container__", align: VerticalAlign.Top},
            left: {anchor: "__container__", align: HorizontalAlign.Start}
          })
          .id("row1")

        Row().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().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()
          .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%')
  }
}

设置相对于锚点的对齐位置

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

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

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

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


    Row()
      .width(100)
      .height(100)
      .backgroundColor("#FFCC00")
      .alignRules({
      top:{anchor:"__container__",align:VerticalAlign.Top},
      right:{anchor:"__container__",align:HorizontalAlign.End}
      })
      .id("row2")


    Row()
      .width(100)
      .height(100)
      .backgroundColor("#FFCCFF")
      .alignRules({
      top:{anchor:"row1",align:VerticalAlign.Bottom},
      left:{anchor:"row1",align:HorizontalAlign.End}
      })
      .id("row3")


    Row()
      .width(100)
      .height(100)
      .backgroundColor("#FF00FF")
      .alignRules({
      top:{anchor:"row3",align:VerticalAlign.Bottom},
      right:{anchor:"row3",align:HorizontalAlign.End}
      })
      .id("row4")


    }
    .height(500).width("100%").backgroundColor(0xF3F4F5)

子组件位置偏移

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

@Entry
@Component
struct Index {
  private arr: string[] = ['APP1', 'APP2', 'APP3', 'APP4', 'APP5', 'APP6', 'APP7', 'APP8'];

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

        Row().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().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()
          .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()
          .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()
          .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%')
  }
}

多种组件的对齐布局

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%')
  }
}

组件尺寸

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

@Entry
@Component
struct Index {
  build() {
    Row() {

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

        Row().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().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()
          .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()
          .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()
          .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%')
  }
}

栅格布局

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

主要优势包括:

  1. 提供可循的规律:栅格布局可以为布局提供规律性的结构,解决多尺寸多设备的动态布局问题。通过将页面划分为等宽的列数和行数,可以方便地对页面元素进行定位和排版。
  2. 统一的定位标注:栅格布局可以为系统提供一种统一的定位标注,保证不同设备上各个模块的布局一致性。这可以减少设计和开发的复杂度,提高工作效率。
  3. 灵活的间距调整方法:栅格布局可以提供一种灵活的间距调整方法,满足特殊场景布局调整的需求。通过调整列与列之间和行与行之间的间距,可以控制整个页面的排版效果。
  4. 自动换行和自适应:栅格布局可以完成一对多布局的自动换行和自适应。当页面元素的数量超出了一行或一列的容量时,他们会自动换到下一行或下一列,并且在不同的设备上自适应排版,使得页面布局更加灵活和适应性强。

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

栅格容器GridRow

栅格系统断点

栅格系统以设备的水平宽度(屏幕密度像素值,单位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

超大宽度类型设备。

针对断点位置,开发者根据实际使用场景,通过一个单调递增数组设置。由于breakpoints最多支持六个断点,单调递增数组长度最大为5。

breakpoints: {value: ['100vp', '200vp']}

表示启用xs、sm、md共3个断点,小于100vp为xs,100vp-200vp为sm,大于200vp为md。

breakpoints: {value: ['320vp', '520vp', '840vp', '1080vp']}

表示启用xs、sm、md、lg、xl共5个断点,小于320vp为xs,320vp-520vp为sm,520vp-840vp为md,840vp-1080vp为lg,大于1080vp为xl。

栅格系统通过监听窗口或容器的尺寸变化进行断点,通过reference设置断点切换参考物。 考虑到应用可能以非全屏窗口的形式显示,以应用窗口宽度为参照物更为通用。

@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, index) => {
   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)
  })
}             

温馨提示

vp(虚拟像素)

vp=(px*160)/DPI

代表的分辨率

DPI

换算(px/vp)

240*320

120

1vp=0.75px

320*480

160

1vp=1px

480*800

240

1vp=1.5px

720*1280

320

1vp=2px

1920*1280

480

1vp=3px

如何算屏幕的宽度?

例如上面的,编辑手机后可以看到其DPI为480,Resolution中是1080*2340

那么参照上面的 温馨提示 找到 DPI 对应为 480 的:1vp=3px

即这里的屏幕宽度就为:1080/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, index) => {
    GridCol() {
      Row() {
        Text(`${index + 1}`)
      }.width('100%').height('50')
    }.backgroundColor(item)
          })
}      

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

GridRow({ columns: 4 }) {
  ForEach(this.bgColors, (color, index) => {
    GridCol() {
      Row() {
        Text(`${index + 1}`)
       }.width('100%').height('100')
     }.backgroundColor(color)
   })
}
-------------------------------------------------------------
GridRow({ 
  
  columns: 8 }) {
  ForEach(this.bgColors, (color, index) => {
    GridCol() {
      Row() {
        Text(`${index + 1}`)
       }.width('100%').height('100')
     }.backgroundColor(color)
   })
}

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

GridRow({ columns: { sm: 4, md: 8 }, breakpoints: { value: ['200vp', '300vp', '400vp', '500vp', '600vp'] } }) {
  ForEach(this.bgColors, (item, index) => {
    GridCol() {
      Row() {
        Text(`${index + 1}`)
      }.width('100%').height('50')
    }.backgroundColor(item)
          })
}

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

排列方向

栅格布局中,可以通过设置GridRow的direction属性来指定栅格子组件在栅格容器中的排列方向。

该属性可以设置为GridRowDirection.Row(从左往右排列)或GridRowDirection.RowReverse(从右往左排列),以满足不同的布局需求。通过合理的direction属性设置,可以使得页面布局更加灵活和符合设计要求

子组件默认从左往右排列

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

子组件从右往左排列

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

子组件间距

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

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

GridRow({ gutter: 10 }){}

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

GridRow({ gutter: { x: 20, y: 50 } }){}

子组件GridCol

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

let Gspan:Record<string,number> = { 'xs': 1, 'sm': 2, 'md': 3, 'lg': 4 }
GridCol({ span: 2 }){}
GridCol({ span: { xs: 1, sm: 2, md: 3, lg: 4 } }){}
GridCol(){}.span(2)
GridCol(){}.span(Gspan)

let Goffset:Record<string,number> = { 'xs': 1, 'sm': 2, 'md': 3, 'lg': 4 }
GridCol({ offset: 2 }){}
GridCol({ offset: { xs: 2, sm: 2, md: 2, lg: 2 } }){}
GridCol(){}.offset(Goffset) 

let Gorder:Record<string,number> = { 'xs': 1, 'sm': 2, 'md': 3, 'lg': 4 }
GridCol({ order: 2 }){}
GridCol({ order: { xs: 1, sm: 2, md: 3, lg: 4 } }){}
GridCol(){}.order(2)
GridCol(){}.order(Gorder)

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, index) => {
  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, index) => {
  GridCol({ span: { xs: 1, sm: 2, md: 3, lg: 4 } }) {   
   Row() {
    Text(`${index}`)
    }.width('100%').height('50vp')     
   }
   .backgroundColor(color)
  })
}    

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, index) => {
  GridCol({ offset: 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() {
 ForEach(this.bgColors, (color, index) => {
  GridCol({ offset: { xs: 1, sm: 2, md: 3, lg: 4 } }) {   
   Row() {
    Text('' + index)
    }.width('100%').height('50vp')     
   }
   .backgroundColor(color)
  })
}     

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)
} 

栅格组件的嵌套使用

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

以下示例中,栅格把整个空间分为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')
        .height('100%')
      }

      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等参数,即可确定最终布局,无需关心具体的设备类型及设备状态(如横竖屏)等。

媒体查询

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

媒体查询常用于下面两种场景:

  1. 针对设备和应用的属性信息(比如显示区域、深浅色、分辨率),设计出相匹配的布局
  2. 当屏幕发生动态改变时(比如分屏、横竖屏切换),同步更新应用的页面布局

引入与使用流程

媒体查询通过mediaquery模块接口,设置查询条件并绑定回调函数,在对应的条件的回调函数里更改页面布局或者实现业务逻辑,实现页面的响应式设计。具体步骤如下:

首先导入媒体查询模块

import mediaquery from '@ohos.mediaquery';

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

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

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

changeText(mediaQueryResult) {
 if (mediaQueryResult.matches) {
  // do something here
  } else {
  // do something here
  }
}
listener.on('change', changeText);

完整代码

import mediaquery from '@ohos.mediaquery';
import window from '@ohos.window';
import common from '@ohos.app.ability.common';


let portraitFunc = null;


@Entry
@Component
struct MediaQueryExample {
 @State color: string = '#DB7093';
 @State text: string = 'Portrait';
 // 当设备横屏时条件成立
 listener = mediaquery.matchMediaSync('(orientation: landscape)');


 // 当满足媒体查询条件时,触发回调
 changeText(mediaQueryResult) {
  if (mediaQueryResult.matches) { // 若设备为横屏状态,更改相应的页面布局
   this.color = '#FFD700';
   this.text = 'Landscape';
   } else {
   this.color = '#DB7093';
   this.text = 'Portrait';
   }
  }


 aboutToAppear() {
  // 绑定当前应用实例
  portraitFunc = this.changeText.bind(this);
  // 绑定回调函数
  this.listener.on('change', portraitFunc);
  }


 // 改变设备横竖屏状态函数
 private changeOrientation(isLandscape: boolean) {
  // 获取UIAbility实例的上下文信息
  let context = getContext(this) as common.UIAbilityContext;
  // 调用该接口手动改变设备横竖屏状态
  window.getLastWindow(context).then((lastWindow) => {
   lastWindow.setPreferredOrientation(isLandscape ? window.Orientation.LANDSCAPE : window.Orientation.PORTRAIT)
   });
  }


 build() {
  Column({ space: 50 }) {
   Text(this.text).fontSize(50).fontColor(this.color)
   Text('Landscape').fontSize(50).fontColor(this.color).backgroundColor(Color.Orange)
     .onClick(() => {
     this.changeOrientation(true);
     })
   Text('Portrait').fontSize(50).fontColor(this.color).backgroundColor(Color.Orange)
     .onClick(() => {
     this.changeOrientation(false);
     })
   }
   .width('100%').height('100%')
  }
}

媒体查询条件

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

语法规则

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

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

例如

  • screen and (round-screen: true) :表示当设备屏幕是圆形时条件成立。
  • (max-height: 800) :表示当高度小于等于800vp时条件成立。
  • (height <= 800) :表示当高度小于等于800vp时条件成立。
  • screen and (device-type: tv) or (resolution < 2) :表示包含多个媒体特征的多条件复杂语句查询,当设备类型为tv或设备分辨率小于2时条件成立。

media-type)

类型

说明

screen

按屏幕相关参数进行媒体查询。

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

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

类型

说明

and

将多个媒体特征(Media Feature)以“与”的方式连接成一个媒体查询,只有当所有媒体特征都为true,查询条件成立。另外,它还可以将媒体类型和媒体功能结合起来。例如:screen and (device-type: wearable) and (max-height: 600) 表示当设备类型是智能穿戴且应用的最大高度小于等于600个像素单位时成立。

or

将多个媒体特征以“或”的方式连接成一个媒体查询,如果存在结果为true的媒体特征,则查询条件成立。例如:screen and (max-height: 1000) or (round-screen: true) 表示当应用高度小于等于1000个像素单位或者设备屏幕是圆形时,条件成立。

not

取反媒体查询结果,媒体查询结果不成立时返回true,否则返回false。例如:not screen and (min-height: 50) and (max-height: 600) 表示当应用高度小于50个像素单位或者大于600个像素单位时成立。使用not运算符时必须指定媒体类型。

only

当整个表达式都匹配时,才会应用选择的样式,可以应用在防止某些较早的版本的浏览器上产生歧义的场景。一些较早版本的浏览器对于同时包含了媒体类型和媒体特征的语句会产生歧义,比如:screen and (min-height: 50)。老版本浏览器会将这句话理解成screen,从而导致仅仅匹配到媒体类型(screen),就应用了指定样式,使用only可以很好地规避这种情况。使用only时必须指定媒体类型。

comma(, )

将多个媒体特征以“或”的方式连接成一个媒体查询,如果存在结果为true的媒体特征,则查询条件成立。其效果等同于or运算符。例如:screen and (min-height: 1000), (round-screen: true) 表示当应用高度大于等于1000个像素单位或者设备屏幕是圆形时,条件成立。

媒体范围操作符包括<=,>=,<,>

类型

说明

<=

小于等于,例如:screen and (height <= 50)。

>=

大于等于,例如:screen and (height >= 600)。

<

小于,例如:screen and (height < 50)。

>

大于,例如:screen and (height > 600)。

媒体特征(media-feature)

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

类型

说明

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。

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

场景示例

import mediaquery from '@ohos.mediaquery'


@Component
export default struct WidthView{


 // 定义要显示的文本信息
 @State text:string = "小屏幕"


 // 当前设备横屏条件
 xsListener = mediaquery.matchMediaSync('(width<320vp)')
 smListener = mediaquery.matchMediaSync('(320vp<=width<520vp)')
 mdListener = mediaquery.matchMediaSync('(width>520vp)')


 /**
  * 改变布局的回调函数
  */
 xsListenerCallback(mediaQueryResult){
  if(mediaQueryResult.matches){
   this.text = "最小屏幕"
   }
  }
 smListenerCallback(mediaQueryResult){
  if(mediaQueryResult.matches){
   this.text = "小屏幕"
   }
  }
 mdListenerCallback(mediaQueryResult){
  if(mediaQueryResult.matches){
   this.text = "中等屏幕"
   }
  }


 /**
  * 生命周期函数
  */
 aboutToAppear(){
  // 绑定回调函数
  this.xsListener.on("change",this.xsListenerCallback.bind(this))
  this.smListener.on("change",this.smListenerCallback.bind(this))
  this.mdListener.on("change",this.mdListenerCallback.bind(this))
  }




 build() {
  Column(){
   Text(this.text).fontSize(50)
   }.width("100%").height("100%")
  }
}

列表

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

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

温馨提示

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

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

基本示例

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


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


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

设置主轴方向

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

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

List() {
 ...
}
.listDirection(Axis.Horizontal)

设置交叉轴布局

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

List组件的lanes属性通常用于在不同尺寸的设备自适应构建不同行数或列数的列表。lanes属性的取值类型是"number | LengthConstrain",即整数或者LengthConstrain类型。以垂直列表为例,如果将lanes属性设为2,表示构建的是一个两列的垂直列表

List() {
 ...
}
.lanes(2)
List() {
 ...
}
.alignListItem(ListItemAlign.Center)

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

List() {
 ...
}
.lanes({ minLength: 200, maxLength: 300 })

例如,假设在垂直列表中设置了lanes的值为{ minLength: 200, maxLength: 300 }。此时,

  • 当List组件宽度为300vp时,由于minLength为200vp,此时列表为一列。
  • 当List组件宽度变化至400vp时,符合两倍的minLength,则此时列表自适应为两列。

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

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


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


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


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

在列表中显示数据

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

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

@Entry
@Component
struct Index {
  build() {
    List() {
      ListItem() {
        Row() {
          Image('https://credit.yn.gov.cn/pcimages/xczx.jpg')
            .width(40)
            .height(40)
            .margin(10)
            .borderRadius(50)
          Text('西瓜').fontSize(20)
        }
      }
      ListItem() {
        Text('香蕉').fontSize(20)
      }
      ListItem() {
        Text('草莓').fontSize(20)
      }
    }
    .backgroundColor('#FFF1F3F5')
    .alignListItem(ListItemAlign.Center)
  }
}

迭代列表内容

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

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 Index {
  private contacts: Array<object> = [
    new Contact('小明', $r("app.media.app_icon")),
    new Contact('小红', $r("app.media.app_icon")),
  ]

  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的循环渲染详细使用请参见支持分组列表

自定义列表样式

设置内容间距

在初始化列表时,如需在列表项之间添加间距,可以使用space参数。例如,在每个列表项之间沿主轴方向添加10vp的间距

List({ space: 10 }) {
 ...
}

添加分隔线

分隔线用来将界面元素隔开,使单个元素更加容易识别。

List提供了divider属性用于给列表项之间添加分隔线。在设置divider属性时,可以通过strokeWidth和color属性设置分隔线的粗细和颜色。

startMargin和endMargin属性分别用于设置分隔线距离列表侧边起始端的距离和距离列表侧边结束端的距离。

如下图所示,当列表项左边有图标(如蓝牙图标),由于图标本身就能很好的区分,此时分隔线从图标之后开始显示即可。

List() {
 ...
}
.divider({
 strokeWidth: 1,
 startMargin: 60,
 endMargin: 10,
 color: '#ffe9f0f0'
})

示例一

import util from "@ohos.util"


class System{
 // 每一条数据都应该有唯一ID
 key:string = util.generateRandomUUID(true);
 name:string;
 icon:Resource;


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




@Component
export default struct ListDemo {


 private contacts = [
  new System("电话",$r("app.media.call")),
  new System("通讯录",$r("app.media.address")),
  new System("日历",$r("app.media.calendar")),
  new System("相机",$r("app.media.camera")),
  new System("扫一扫",$r("app.media.sweep")),
  new System("邮件",$r("app.media.email"))
  ]


 build() {
  List(){
   ForEach(this.contacts,(item:System) =>{
    ListItem(){
     Row(){
      Flex({ justifyContent: FlexAlign.SpaceBetween,alignItems: ItemAlign.Center }){
       Row(){
        Image(item.icon).width(50).height(50).margin(10)
        Text(item.name).fontSize(20)
        }
       Image($r("app.media.right")).width(20).height(20)
       }.margin({right:10})
      }.width("100%").justifyContent(FlexAlign.Start)
     }
    })
   }.divider({
   strokeWidth: 1,
   startMargin: 60,
   endMargin: 10,
   color: '#ffe9f0f0'
   })
  }
}

示例二

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;
  }
}

class DividerTmp {
  strokeWidth: Length = 1
  startMargin: Length = 60
  endMargin: Length = 10
  color: ResourceColor = '#ffe9f0f0'

  constructor(strokeWidth: Length, startMargin: Length, endMargin: Length, color: ResourceColor) {
    this.strokeWidth = strokeWidth
    this.startMargin = startMargin
    this.endMargin = endMargin
    this.color = color
  }
}

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

  @State egDivider: DividerTmp = new DividerTmp(1, 60, 10, '#ffe9f0f0')

  build() {
    List({ space: 10 }) {
      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%')
    .divider(this.egDivider)
  }
}

此示例表示从距离列表侧边起始端60vp开始到距离结束端10vp的位置,画一条粗细为1vp的分割线,可以实现图9设置列表分隔线的样式。

特别说明

  1. 分隔线的宽度会使ListItem之间存在一定间隔,当List设置的内容间距小于分隔线宽度时,ListItem之间的间隔会使用分隔线的宽度。
  2. 当List存在多列时,分割线的startMargin和endMargin作用于每一列上。
  3. List组件的分隔线画在两个ListItem之间,第一个ListItem上方和最后一个ListItem下方不会绘制分隔线。

添加滚动条

当列表项高度(宽度)超出屏幕高度(宽度)时,列表可以沿垂直(水平)方向滚动。在页面内容很多时,若用户需快速定位,可拖拽滚动条

在使用List组件时,可通过scrollBar属性控制列表滚动条的显示。scrollBar的取值类型为BarState,当取值为BarState.Auto表示按需显示滚动条。此时,当触摸到滚动条区域时显示控件,可上下拖拽滚动条快速浏览内容,拖拽时会变粗。若不进行任何操作,2秒后滚动条自动消失。

scrollBar属性API version 9及以下版本默认值为BarState.Off,从API version 10版本开始默认值为BarState.Auto。

List() {
 ...
}
.scrollBar(BarState.Auto)

支持分组列表

在列表中支持数据的分组展示,可以使列表显示结构清晰,查找方便,从而提高使用效率。分组列表在实际应用中十分常见。如下图所示联系人列表。

在List组件中使用ListItemGroup对项目进行分组,可以构建二维列表

在List组件中可以直接使用一个或者多个ListItemGroup组件,ListItemGroup的宽度默认充满List组件。在初始化ListItemGroup时,可通过header参数设置列表分组的头部组件。

import util from "@ohos.util"


class ContactList{
 // 每一条数据都应该有唯一ID
 key:string = util.generateRandomUUID(true);
 name:string;
 icon:Resource;


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




@Component
export default struct ListDemo {


 private contactsGroup = [
   {
   title:"A",
   contacts:[
    new ContactList("艾美",$r("app.media.iconA")),
    new ContactList("安安",$r("app.media.iconB"))
    ]
   },
   {
   title:"B",
   contacts:[
    new ContactList("彬彬",$r("app.media.iconA"))
    ]
   },
   {
   title:"C",
   contacts:[
    new ContactList("陈伯",$r("app.media.iconB")),
    new ContactList("聪聪",$r("app.media.iconA")),
    new ContactList("崔莺莺",$r("app.media.iconB"))
    ]
   },
   {
   title:"D",
   contacts:[
    new ContactList("大黄",$r("app.media.iconA")),
    ]
   },
   {
   title:"F",
   contacts:[
    new ContactList("房祖名",$r("app.media.iconB")),
    ]
   },
   {
   title:"G",
   contacts:[
    new ContactList("高小小",$r("app.media.iconA")),
    ]
   },
   {
   title:"J",
   contacts:[
    new ContactList("靳东",$r("app.media.iconB")),
    ]
   },
   {
   title:"K",
   contacts:[
    new ContactList("kimi",$r("app.media.iconA")),
    ]
   },
   {
   title:"L",
   contacts:[
    new ContactList("老李",$r("app.media.iconB"))
    ]
   },


  ]


 /**
  * 自定义构建函数
  * 一种更轻量的UI元素复用机制,@Builder所装饰的函数遵循build()函数语法规则,
  * 开发者可以将重复使用的UI元素抽象成一个方法,在build方法里调用
  */
 @Builder itemHead(text: string) {
  // 列表分组的头部组件,对应联系人分组A、B等位置的组件
  Text(text)
    .fontSize(20)
    .backgroundColor('#fff1f3f5')
    .width('100%')
    .padding(5)
  }


 build() {
  List(){
   ForEach(this.contactsGroup,(item) =>{
    ListItemGroup({header:this.itemHead(item.title)}){
     ForEach(item.contacts, contact => {
      ListItem() {
       Row(){
        Image(contact.icon).width(40).height(40).margin(10)
        Text(contact.name).fontSize(20)
        }.width("100%").justifyContent(FlexAlign.Start)
       }
      })
     }
    })
   }
  }
}

如果多个ListItemGroup结构类似,可以将多个分组的数据组成数组,然后使用ForEach对多个分组进行循环渲染。例如在联系人列表中,将每个分组的联系人数据contacts(可参考迭代列表内容章节)和对应分组的标题title数据进行组合,定义为数组contactsGroups。然后在ForEach中对contactsGroups进行循环渲染,即可实现多个分组的联系人列表。可参考添加粘性标题章节示例代码。

添加粘性标题

粘性标题是一种常见的标题模式,常用于定位字母列表的头部元素。如下图所示,在联系人列表中滚动A部分时,B部分开始的头部元素始终处于A的下方。而在开始滚动B部分时,B的头部会固定在屏幕顶部,直到所有B的项均完成滚动后,才被后面的头部替代。

粘性标题不仅有助于阐明列表中数据的表示形式和用途,还可以帮助用户在大量信息中进行数据定位,从而避免用户在标题所在的表的顶部与感兴趣区域之间反复滚动。

List组件的sticky属性配合ListItemGroup组件使用,用于设置ListItemGroup中的头部组件是否呈现吸顶效果或者尾部组件是否呈现吸底效果。

通过给List组件设置sticky属性为StickyStyle.Header,即可实现列表的粘性标题效果。如果需要支持吸底效果,可以通过footer参数初始化ListItemGroup的底部组件,并将sticky属性设置为StickyStyle.Footer。

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;
  }
}
class ContactsGroup {
  title: string = ''
  contacts: Array<object> | null = null
  key: string = ""
}
export let contactsGroups: object[] = [
  {
    title: 'A',
    contacts: [
      new Contact('艾佳', $r('app.media.iconA')),
      new Contact('安安', $r('app.media.iconB')),
      new Contact('Angela', $r('app.media.iconC')),
    ],
    key: util.generateRandomUUID(true)
  } as ContactsGroup,
  {
    title: 'B',
    contacts: [
      new Contact('白叶', $r('app.media.iconD')),
      new Contact('伯明', $r('app.media.iconE')),
    ],
    key: util.generateRandomUUID(true)
  } as ContactsGroup,
  // ...
]
@Entry
@Component
struct ContactsList {
  // 定义分组联系人数据集合contactsGroups数组
  @Builder itemHead(text: string) {
    // 列表分组的头部组件,对应联系人分组A、B等位置的组件
    Text(text)
      .fontSize(20)
      .backgroundColor('#fff1f3f5')
      .width('100%')
      .padding(5)
  }
  build() {
    List() {
      // 循环渲染ListItemGroup,contactsGroups为多个分组联系人contacts和标题title的数据集合
      ForEach(contactsGroups, (itemGroup: ContactsGroup) => {
        ListItemGroup({ header: this.itemHead(itemGroup.title) }) {
          // 循环渲染ListItem
          if (itemGroup.contacts) {
            ForEach(itemGroup.contacts, (item: Contact) => {
              ListItem() {
                // ...
              }
            }, (item: Contact) => JSON.stringify(item))
          }
        }
      }, (itemGroup: ContactsGroup) => JSON.stringify(itemGroup))
    }.sticky(StickyStyle.Header)  // 设置吸顶,实现粘性标题效果
  }
}

控制滚动位置

控制滚动位置在实际应用中十分常见,例如当新闻页列表项数量庞大,用户滚动列表到一定位置时,希望快速滚动到列表底部或返回列表顶部。此时,可以通过控制滚动位置来实现列表的快速定位

List组件初始化时,可以通过scroller参数绑定一个Scroller对象,进行列表的滚动控制。例如,用户在新闻应用中,点击新闻页面底部的返回顶部按钮时,就可以通过Scroller对象的scrollToIndex方法使列表滚动到指定的列表项索引位置。

首先,需要创建一个Scroller的对象listScroller。

private listScroller: Scroller = new Scroller();

然后,通过将listScroller用于初始化List组件的scroller参数,完成listScroller与列表的绑定。在需要跳转的位置指定scrollToIndex的参数为0,表示返回列表顶部。

Stack({ alignContent: Alignment.BottomEnd }) {
 // 将listScroller用于初始化List组件的scroller参数,完成listScroller与列表的绑定。
 List({ scroller: this.listScroller }) {
  ...
  }
 ...


 Button() {
  ...
  }
  .onClick(() => {
  // 点击按钮时,指定跳转位置,返回列表顶部
  this.listScroller.scrollToIndex(0)
  })
 ...
}

响应滚动位置

许多应用需要监听列表的滚动位置变化并作出响应。例如,在联系人列表滚动时,如果跨越了不同字母开头的分组,则侧边字母索引栏也需要更新到对应的字母位置。

除了字母索引之外,滚动列表结合多级分类索引在应用开发过程中也很常见,例如购物应用的商品分类页面,多级分类也需要监听列表的滚动位置。

如上图所示,当联系人列表从A滚动到B时,右侧索引栏也需要同步从选中A状态变成选中B状态。此场景可以通过监听List组件的onScrollIndex事件来实现,右侧索引栏需要使用字母表索引组件AlphabetIndexer

示例一

在列表滚动时,根据列表此时所在的索引值位置firstIndex,重新计算字母索引栏对应字母的位置selectedIndex。由于AlphabetIndexer组件通过selected属性设置了选中项索引值,当selectedIndex变化时会触发AlphabetIndexer组件重新渲染,从而显示为选中对应字母的状态。

const alphabets = ['#', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',
  'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];

@Entry
@Component
struct Index {
  @State selectedIndex: number = 0;
  private listScroller: Scroller = new Scroller();

  build() {
    Stack({ alignContent: Alignment.End }) {
      List({ scroller: this.listScroller }) {}
      .onScrollIndex((firstIndex: number) => {
        // 根据列表滚动到的索引值,重新计算对应联系人索引栏的位置this.selectedIndex
      })

      // 字母表索引组件
      AlphabetIndexer({ arrayValue: alphabets, selected: 0 })
        .selected(this.selectedIndex)
    }
  }
}

示例二

import util from "@ohos.util"


class ContactList{
 // 每一条数据都应该有唯一ID
 key:string = util.generateRandomUUID(true);
 name:string;
 icon:Resource;


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


const alphabets = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',
 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];


@Component
export default struct ListDemo {


 @State selectedIndex: number = 0;
 private listScroller: Scroller = new Scroller();


 private contactsGroup = [
   {
   title:"A",
   contacts:[
    new ContactList("艾美",$r("app.media.iconA")),
    new ContactList("安安",$r("app.media.iconB"))
    ]
   },
   {
   title:"B",
   contacts:[
    new ContactList("彬彬",$r("app.media.iconA")),
    new ContactList("冰灵",$r("app.media.iconA"))
    ]
   },
   {
   title:"C",
   contacts:[
    new ContactList("陈伯",$r("app.media.iconB")),
    new ContactList("聪聪",$r("app.media.iconA"))
    ]
   },
   {
   title:"D",
   contacts:[
    new ContactList("大黄",$r("app.media.iconA")),
    new ContactList("打黑",$r("app.media.iconA"))
    ]
   },
   {
   title:"E",
   contacts:[
    new ContactList("亿万",$r("app.media.iconA")),
    new ContactList("亿3",$r("app.media.iconA"))
    ]
   },
   {
   title:"F",
   contacts:[
    new ContactList("浮云",$r("app.media.iconA")),
    new ContactList("福祥",$r("app.media.iconA"))
    ]
   },
   {
   title:"G",
   contacts:[
    new ContactList("哥哥",$r("app.media.iconA")),
    new ContactList("哥们",$r("app.media.iconA"))
    ]
   },
   {
   title:"H",
   contacts:[
    new ContactList("哈哈1",$r("app.media.iconA")),
    new ContactList("哈哈2",$r("app.media.iconA"))
    ]
   }
  ]
 /**
  * 自定义构建函数
  * 一种更轻量的UI元素复用机制,@Builder所装饰的函数遵循build()函数语法规则,
  * 开发者可以将重复使用的UI元素抽象成一个方法,在build方法里调用
  */
 @Builder itemHead(text:string){
  Text(text)
    .fontSize(20)
    .backgroundColor("#f1f3f5")
    .width("100%")
    .padding(5)
  }


 build() {
  Stack({ alignContent: Alignment.End }){
   List({ scroller:this.listScroller }){
    ForEach(this.contactsGroup,(item) =>{
     ListItemGroup({ header:this.itemHead(item.title) }){
      ForEach(item.contacts,contact =>{
       ListItem(){
        Row(){
         Image(contact.icon).width(40).height(40).margin(10)
         Text(contact.name).fontSize(20)
         }.width("100%").justifyContent(FlexAlign.Start)
        }
       })
      }
     })
    }
    .sticky(StickyStyle.Header)
    .onScrollIndex((firstIndex: number) => {
    this.selectedIndex = firstIndex
    // 根据列表滚动到的索引值,重新计算对应联系人索引栏的位置this.selectedIndex
    console.log("索引:",firstIndex)
    })
   AlphabetIndexer({ arrayValue: alphabets, selected: 0 })
     .selected(this.selectedIndex)
     .onSelect((index:number) =>{
     console.log("数量:",index)
     this.listScroller.scrollTo({xOffset:0,yOffset:index * 153.33})
     })
   }
  }
}

响应列表项侧滑

侧滑菜单在许多应用中都很常见。例如,通讯类应用通常会给消息列表提供侧滑删除功能,即用户可以通过向左侧滑列表的某一项,再点击删除按钮删除消息

如下图所示。其中,列表项头像右上角标记设置参考给列表项添加标记

ListItem的swipeAction属性可用于实现列表项的左右滑动功能。

swipeAction属性方法初始化时有必填参数SwipeActionOptions,其中,start参数表示设置列表项右滑时起始端滑出的组件,end参数表示设置列表项左滑时尾端滑出的组件

在消息列表中,end参数表示设置ListItem左滑时尾端划出自定义组件,即删除按钮。在初始化end方法时,将滑动列表项的索引传入删除按钮组件,当用户点击删除按钮时,可以根据索引值来删除列表项对应的数据,从而实现侧滑删除功能。

  1. 实现尾端滑出组件的构建。
@Builder itemEnd(index: number) {
  // 构建尾端滑出组件
  Button({ type: ButtonType.Circle }) {
    Image($r('app.media.ic_public_delete_filled'))
      .width(20)
      .height(20)
  }
  .onClick(() => {
    // this.messages为列表数据源,可根据实际场景构造。点击后从数据源删除指定数据项。
    this.messages.splice(index, 1);
  })
}
  1. 绑定swipeAction属性到可左滑的ListItem上。
// 构建List时,通过ForEach基于数据源this.messages循环渲染ListItem。
ListItem() {
  // ...
}
.swipeAction({
  end: {
    // index为该ListItem在List中的索引值。
    builder: () => { this.itemEnd(index) },
  }
}) // 设置侧滑属性.

import util from "@ohos.util"


class ContactList{
 // 每一条数据都应该有唯一ID
 key:string = util.generateRandomUUID(true);
 name:string;
 icon:Resource;


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


@Component
export default struct ListDemo {


 @State selectedIndex: number = 0;
 private listScroller: Scroller = new Scroller();


 @State contactsGroup:object[] = [
  new ContactList("艾美",$r("app.media.iconA")),
  new ContactList("安安",$r("app.media.iconB")),
  new ContactList("彬彬",$r("app.media.iconA")),
  new ContactList("冰灵",$r("app.media.iconA")),
  new ContactList("陈伯",$r("app.media.iconB")),
  new ContactList("聪聪",$r("app.media.iconA"))
  ]


 @Builder itemEnd(index: number) {
  // 侧滑后尾端出现的组件
  Button({ type: ButtonType.Circle }) {
   Image($r('app.media.del'))
     .width(20)
     .height(20)
   }
   .onClick(() => {
   console.log("位置:",index)
   this.contactsGroup.splice(index, 1);
   })
  }




 build() {
  Stack({ alignContent: Alignment.End }){
   List({ scroller:this.listScroller }){
    ForEach(this.contactsGroup,(item,index) =>{
     ListItem(){
      Row(){
       Image(item.icon).width(40).height(40).margin(10)
       Text(item.name).fontSize(20)
       }.width("100%").justifyContent(FlexAlign.Start)
      }.swipeAction({ end: this.itemEnd(index) }) // 设置侧滑属性
     })
    }
   }
  }
}

给列表项添加标记

添加标记是一种无干扰性且直观的方法,用于显示通知或将注意力集中到应用内的某个区域。例如,当消息列表接收到新消息时,通常对应的联系人头像的右上方会出现标记,提示有若干条未读消息,如下图所示。

在ListItem中使用Badge组件可实现给列表项添加标记功能。Badge是可以附加在单个组件上用于信息标记的容器组件。

在消息列表中,若希望在联系人头像右上角添加标记,可在实现消息列表项ListItem的联系人头像时,将头像Image组件作为Badge的子组件。

在Badge组件中,count和position参数用于设置需要展示的消息数量和提示点显示位置,还可以通过style参数灵活设置标记的样式。

ListItem() {
  Badge({
    count: 1,
    position: BadgePosition.RightTop,
    style: { badgeSize: 16, badgeColor: '#FA2A2D' }
  }) {
    // Image组件实现消息联系人头像
    // ...
  }
}

下拉刷新与上拉加载

页面的下拉刷新与上拉加载功能在移动应用中十分常见,例如,新闻页面的内容刷新和加载。这两种操作的原理都是通过响应用户的触摸事件,在顶部或者底部显示一个刷新或加载视图,完成后再将此视图隐藏。

以下拉刷新为例,其实现主要分成三步:

  1. 监听手指按下事件,记录其初始位置的值。
  2. 监听手指按压移动事件,记录并计算当前移动的位置与初始值的差值,大于0表示向下移动,同时设置一个允许移动的最大值。
  3. 监听手指抬起事件,若此时移动达到最大值,则触发数据加载并显示刷新视图,加载完成后将此视图隐藏。

下拉刷新与上拉加载的具体实现可参考新闻数据加载
 

编辑列表

列表的编辑模式用途十分广泛,常见于待办事项管理、文件管理、备忘录的记录管理等应用场景。在列表的编辑模式下,新增和删除列表项是最基础的功能,其核心是对列表项对应的数据集合进行数据添加和删除。

下面以待办事项管理为例,介绍如何快速实现新增和删除列表项功能。

新增列表项

如下图所示,当用户点击添加按钮时,提供用户新增列表项内容选择或填写的交互界面,用户点击确定后,列表中新增对应的项目。

添加列表项功能实现主要流程如下:

  1. 定义列表项数据结构,以待办事项管理为例,首先定义待办数据结构。
//ToDo.ets
import { util } from '@kit.ArkTS'


export class ToDo {
  key: string = util.generateRandomUUID(true);
  name: string;


  constructor(name: string) {
    this.name = name;
  }
}
  1. 构建列表整体布局和列表项。
//ToDoListItem.ets
import { ToDo } from './ToDo';
@Component
  export struct ToDoListItem {
    @Link isEditMode: boolean
    @Link selectedItems: ToDo[]
    private toDoItem: ToDo = new ToDo("");


    build() {
      Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
        // ...
      }
      .width('100%')
        .height(80)
        //.padding() 根据具体使用场景设置
        .borderRadius(24)
        //.linearGradient() 根据具体使用场景设置
        .gesture(
          GestureGroup(GestureMode.Exclusive,
                       LongPressGesture()
                       .onAction(() => {
                         // ...
                       })
                      )
        )
    }
  }
  1. 初始化待办列表数据和可选事项,最后,构建列表布局和列表项。
//ToDoList.ets
import { ToDo } from './ToDo';
import { ToDoListItem } from './ToDoListItem';


@Entry
  @Component
  struct ToDoList {
    @State toDoData: ToDo[] = []
    @Watch('onEditModeChange') @State isEditMode: boolean = false
    @State selectedItems: ToDo[] = []
    private availableThings: string[] = ['读书', '运动', '旅游', '听音乐', '看电影', '唱歌']


    onEditModeChange() {
      if (!this.isEditMode) {
        this.selectedItems = []
      }
    }


    build() {
      Column() {
        Row() {
          if (this.isEditMode) {
            Text('X')
              .fontSize(20)
              .onClick(() => {
                this.isEditMode = false;
              })
              .margin({ left: 20, right: 20 })
          } else {
            Text('待办')
              .fontSize(36)
              .margin({ left: 40 })
            Blank()
            Text('+') //提供新增列表项入口,即给新增按钮添加点击事件
              .onClick(() => {
                TextPickerDialog.show({
                  range: this.availableThings,
                  onAccept: (value: TextPickerResult) => {
                    let arr = Array.isArray(value.index) ? value.index : [value.index];
                    for (let i = 0; i < arr.length; i++) {
                      this.toDoData.push(new ToDo(this.availableThings[arr[i]])); // 新增列表项数据toDoData(可选事项)
                    }
                  },
                })
              })
          }
          List({ space: 10 }) {
            ForEach(this.toDoData, (toDoItem: ToDo) => {
              ListItem() {
                // 将toDoData的每个数据放入到以model的形式放进ListItem里
                ToDoListItem({
                  isEditMode: this.isEditMode,
                  toDoItem: toDoItem,
                  selectedItems: this.selectedItems })
              }
            }, (toDoItem: ToDo) => toDoItem.key.toString())
          }
        }
      }
    }
  }
删除列表项

如下图所示,当用户长按列表项进入删除模式时,提供用户删除列表项选择的交互界面,用户勾选完成后点击删除按钮,列表中删除对应的项目。

删除列表项功能实现主要流程如下:

  1. 列表的删除功能一般进入编辑模式后才可使用,所以需要提供编辑模式的入口。

以待办列表为例,通过监听列表项的长按事件,当用户长按列表项时,进入编辑模式。

// 结构参考
export class ToDo {
  key: string = util.generateRandomUUID(true);
  name: string;
  toDoData: ToDo[] = [];


  constructor(name: string) {
    this.name = name;
  }
}
// 实现参考
Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
  // ...
}
.gesture(
  GestureGroup(GestureMode.Exclusive,
               LongPressGesture()
               .onAction(() => {
                 if (!this.isEditMode) {
                   this.isEditMode = true; //进入编辑模式
                 }
               })
              )
)
  1. 需要响应用户的选择交互,记录要删除的列表项数据。

在待办列表中,通过勾选框的勾选或取消勾选,响应用户勾选列表项变化,记录所有选择的列表项。

// 结构参考
import { util } from '@kit.ArkTS'
export class ToDo {
  key: string = util.generateRandomUUID(true);
  name: string;
  toDoData: ToDo[] = [];


  constructor(name: string) {
    this.name = name;
  }
}
// 实现参考
if (this.isEditMode) {
  Checkbox()
    .onChange((isSelected) => {
      if (isSelected) {
        this.selectedItems.push(toDoList.toDoItem) // this.selectedItems为勾选时,记录选中的列表项,可根据实际场景构造
      } else {
        let index = this.selectedItems.indexOf(toDoList.toDoItem)
        if (index !== -1) {
          this.selectedItems.splice(index, 1) // 取消勾选时,则将此项从selectedItems中删除
        }
      }
    })
}
  1. 需要响应用户点击删除按钮事件,删除列表中对应的选项。
// 结构参考
import { util } from '@kit.ArkTS'
export class ToDo {
  key: string = util.generateRandomUUID(true);
  name: string;
  toDoData: ToDo[] = [];


  constructor(name: string) {
    this.name = name;
  }
}
// 实现参考
Button('删除')
  .onClick(() => {
    // this.toDoData为待办的列表项,可根据实际场景构造。点击后删除选中的列表项对应的toDoData数据
    let leftData = this.toDoData.filter((item) => {
      return !this.selectedItems.find((selectedItem) => selectedItem == item);
    })
    this.toDoData = leftData;
    this.isEditMode = false;
  })

长列表的处理

循环渲染适用于短列表,当构建具有大量列表项的长列表时,如果直接采用循环渲染方式,会一次性加载所有的列表元素,会导致页面启动时间过长,影响用户体验。因此,推荐使用数据懒加载(LazyForEach)方式实现按需迭代加载数据,从而提升列表性能。

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

当使用懒加载方式渲染列表时,为了更好的列表滚动体验,减少列表滑动时出现白块,List组件提供了cachedCount参数用于设置列表项缓存数,只在懒加载LazyForEach中生效。

List() {
  // ...
}.cachedCount(3)

以垂直列表为例:

  • 若懒加载是用于ListItem,当列表为单列模式时,会在List显示的ListItem前后各缓存cachedCount个ListItem;若是多列模式下,会在List显示的ListItem前后各缓存cachedCount * 列数个ListItem。
  • 若懒加载是用于ListItemGroup,无论单列模式还是多列模式,都是在List显示的ListItem前后各缓存cachedCount个ListItemGroup。

说明

  1. cachedCount的增加会增大UI的CPU、内存开销。使用时需要根据实际情况,综合性能和用户体验进行调整。
  2. 列表使用数据懒加载时,除了显示区域的列表项和前后缓存的列表项,其他列表项会被销毁。

网格

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

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组件设置了rowsTemplate或columnsTemplate时,Grid的layoutDirection、maxCount、minCount、cellLength属性不生效,属性说明可参考Grid-属性

设置子组件所占行列数

除了大小相同的等比例网格布局,由不同大小的网格组成不均匀分布的网格布局场景在实际应用中十分常见,如下图所示。在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')

build() {
  Row() {
    Grid(){
      GridItem().backgroundColor(Color.Red)
      GridItem().backgroundColor(Color.Black)
      GridItem().backgroundColor(Color.Blue)
      GridItem().backgroundColor(Color.Grey)
      GridItem().backgroundColor(Color.Pink)
      GridItem().backgroundColor(Color.Yellow)
      GridItem().backgroundColor(Color.Green)
      GridItem().backgroundColor(Color.Black)
      GridItem().backgroundColor(Color.Orange)
     }
     .rowsTemplate('1fr 1fr 1fr')
     .columnsTemplate('1fr 2fr 1fr')
   }
   .height(300)
   .width("100%")
}

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

build() {
  Row() {
   Grid(){
    GridItem().backgroundColor(Color.Red)
    GridItem().backgroundColor(Color.Black)
    GridItem().backgroundColor(Color.Blue).columnStart(3).columnEnd(4)
    GridItem().backgroundColor(Color.Pink).rowStart(2).rowEnd(3)
    GridItem().backgroundColor(Color.Green)
    GridItem().backgroundColor(Color.Black)
    GridItem().backgroundColor(Color.Orange)
    GridItem().backgroundColor(Color.Blue).columnStart(2).columnEnd(4)


    }
    .rowsTemplate('1fr 1fr 1fr')
    .columnsTemplate('1fr 1fr 1fr 1fr')
   }
   .height(300)
   .width("100%")
  }

设置主轴方向

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

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

Grid() {
  ...
}
.maxCount(3)
.layoutDirection(GridDirection.Row)

温馨提示

当Grid组件设置了rowsTemplate或columnsTemplate时,Grid的layoutDirection、maxCount、minCount、cellLength属性不生效

 build() {
  Row() {
   Grid(){
    GridItem(){Text("1").fontSize(30)}.backgroundColor(Color.Red).width("33.33%").height("33.33%")
    GridItem(){Text("2").fontSize(30)}.backgroundColor(Color.Black).width("33.33%").height("33.33%")
    GridItem(){Text("3").fontSize(30)}.backgroundColor(Color.Blue).width("33.33%").height("33.33%")
    GridItem(){Text("4").fontSize(30)}.backgroundColor(Color.Pink).width("33.33%").height("33.33%")
    GridItem(){Text("5").fontSize(30)}.backgroundColor(Color.Green).width("33.33%").height("33.33%")
    GridItem(){Text("6").fontSize(30)}.backgroundColor(Color.Black).width("33.33%").height("33.33%")
    GridItem(){Text("7").fontSize(30)}.backgroundColor(Color.Orange).width("33.33%").height("33.33%")
    GridItem(){Text("8").fontSize(30)}.backgroundColor(Color.Blue).width("33.33%").height("33.33%")
    GridItem(){Text("9").fontSize(30)}.backgroundColor(Color.Orange).width("33.33%").height("33.33%")
    }
    .maxCount(3)
    .layoutDirection(GridDirection.Column)
   }
   .height(300)
   .width("100%")
  }

说明

  • layoutDirection属性仅在不设置rowsTemplate和columnsTemplate时生效,此时元素在layoutDirection方向上排列。
  • 仅设置rowsTemplate时,Grid主轴为水平方向,交叉轴为垂直方向。
  • 仅设置columnsTemplate时,Grid主轴为垂直方向,交叉轴为水平方向。

在网格布局中显示数据

网格布局采用二维布局的方式组织其内部元素,如下图所示。

@Entry
@Component
struct Index {

 @State services: Array<string> = ['会议', '投票', '签到', '打印']
 build() {
  Row() {
   Grid(){
    ForEach(this.services, service => {
     GridItem() {
      Text(service).fontSize(30)
      }.backgroundColor(Color.Gray)
     }, service => service)
    }
    .columnsTemplate('1fr 1fr')
    .rowsTemplate('1fr 1fr')
    .columnsGap(10)
    .rowsGap(10)
   }
   .height(300)
   .width("100%")
  }
}

设置行列间距

在两个网格单元之间的网格横向间距称为行间距,网格纵向间距称为列间距,如下图所示。


通过Grid的rowsGap和columnsGap可以设置网格布局的行列间距。在图5所示的计算器中,行间距为15vp,列间距为10vp。

Grid() {
  ...
}
.columnsGap(10)
.rowsGap(15)

构建可滚动的网格布局

可滚动的网格布局常用在文件管理、购物或视频列表等页面中,如下图所示。在设置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组件设置了宽高属性,则其尺寸为设置值。如果没有设置宽高属性,Grid组件的尺寸默认适应其父组件的尺寸

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

  • 行、列数量与占比同时设置:Grid只展示固定行列数的元素,其余元素不展示,且Grid不可滚动。(推荐使用该种布局方式)
  • 只设置行、列数量与占比中的一个:元素按照设置的方向进行排布,超出的元素可通过滚动的方式展示。
  • 行列数量与占比都不设置:元素在布局方向上排布,其行列数由布局方向、单个网格的宽高等多个属性共同决定。超出行列容纳范围的元素不展示,且Grid不可滚动。
build() {
  Column({ space: 5 }) {
   Grid(){
    GridItem(){Text("1").fontSize(30)}.backgroundColor(Color.Red).width("33.33%").height("33.33%")
    GridItem(){Text("2").fontSize(30)}.backgroundColor(Color.Black).width("33.33%").height("33.33%")
    GridItem(){Text("3").fontSize(30)}.backgroundColor(Color.Blue).width("33.33%").height("33.33%")
    GridItem(){Text("4").fontSize(30)}.backgroundColor(Color.Pink).width("33.33%").height("33.33%")
    GridItem(){Text("5").fontSize(30)}.backgroundColor(Color.Green).width("33.33%").height("33.33%")
    GridItem(){Text("6").fontSize(30)}.backgroundColor(Color.Black).width("33.33%").height("33.33%")
    GridItem(){Text("7").fontSize(30)}.backgroundColor(Color.Orange).width("33.33%").height("33.33%")
    GridItem(){Text("8").fontSize(30)}.backgroundColor(Color.Blue).width("33.33%").height("33.33%")
    GridItem(){Text("9").fontSize(30)}.backgroundColor(Color.Orange).width("33.33%").height("33.33%")
    }
    .rowsTemplate('1fr 1fr')
   // .columnsTemplate('1fr 1fr')
   }
   .height(300)
   .width("100%")
  }

控制滚动位置

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

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、内存开销。使用时需要根据实际情况,综合性能和用户体验进行调整。

轮播

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

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

基本示例

private swiperController: SwiperController = new SwiperController()


    build() {
    Row() {
      Swiper(this.swiperController){
        Image($r('app.media.b1')).width("100%")
        Image($r('app.media.b2')).width("100%")
        Image($r('app.media.b3')).width("100%")
       }
       .loop(true)
     }
}

布局与约束

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

循环播放

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

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

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)

自动轮播

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

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

build() {
  Row() {
    Swiper(this.swiperController){
      Image($r('app.media.b1')).width("100%")
      Image($r('app.media.b2')).width("100%")
      Image($r('app.media.b3')).width("100%")
     }
     .loop(true)
       .autoPlay(true)
       .interval(1000)
   }
}

导航点样式

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

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

属性

描述

left:length

设置导航点距离Swiper组件左边的距离

top:length

设置导航点距离Swiper组件顶部的距离

right:length

设置导航点距离Swiper组件右边的距离

bottom:length

设置导航点距离Swiper组件底部的距离

size:length

设置导航点的直径。不支持设置百分比。默认值:6vp。

mask:boolean

设置是否显示导航点蒙层样式。

color

设置导航点的颜色。

selectedColor

设置选中的导航点的颜色。

Swiper(this.swiperController){
  ...
}
.loop(true)
   .autoPlay(true)
   .interval(1000)
   .indicatorStyle({
  size: 30,
  left: 200,
  color: Color.Red
})

页面切换方式

Swiper支持三种页面切换方式:手指滑动、点击导航点和通过控制器。

build() {
  Column() {
    Swiper(this.swiperController){
      Image($r('app.media.b1')).width("100%")
      Image($r('app.media.b2')).width("100%")
      Image($r('app.media.b3')).width("100%")
     }
     .loop(true)
       .autoPlay(true)
       .interval(3000)
       .indicator(true)
       .indicatorStyle({
      size: 30,
      left: 200,
      color: Color.Red
     })


    Row({ space: 20 }) {
      Button('下一页')
         .onClick(() => {
        this.swiperController.showNext(); // 通过controller切换到后一页
       })
      Button('上一页')
         .onClick(() => {
        this.swiperController.showPrevious(); // 通过controller切换到前一页
       })
     }.margin(10)
   }
}

轮播方向

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

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

Swiper(this.swiperController) {
 ...
}
.indicator(true)
.vertical(false)

每页显示多个子页面

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

@Entry
@Component
struct Index {
  build() {
    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 Index {
  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%')
  }
}

选项卡

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

说明

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

基本使用

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

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

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

@Entry
@Component
struct Index {
  build() {
    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。

@Entry
@Component
struct Index {
  build() {
    Tabs({ barPosition: BarPosition.End }) {
      TabContent() {
        Text('首页的内容').fontSize(30)
      }
      .tabBar('首页')

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

      TabContent() {
        Text('发现的内容').fontSize(30)
      }
      .tabBar('发现')

      TabContent() {
        Text('我的内容').fontSize(30)
      }
      .tabBar("我的")
    }
  }
}

侧边导航

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

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

import { router } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';

// 使用组件文件中
import CounterComponent from "../components/CounterComponent";

@Entry
@Component
struct Index {
  @State message: string = 'Hello World OS';

  build() {
    Tabs({ barPosition: BarPosition.Start }) {
      // 关注、视频、游戏、数码、科技、体育、影视
      TabContent() {
        Text('关注的内容').fontSize(30)
      }
      .tabBar('关注')

      TabContent() {
        Text('视频的内容').fontSize(30)
      }
      .tabBar('视频')

      TabContent() {
        Text('游戏的内容').fontSize(30)
      }
      .tabBar('游戏')

      TabContent() {
        Text('数码内容').fontSize(30)
      }
      .tabBar("数码")

      TabContent() {
        Text('科技内容').fontSize(30)
      }
      .tabBar("科技")

      TabContent() {
        Text('体育内容').fontSize(30)
      }
      .tabBar("体育")

      TabContent() {
        Text('影视内容').fontSize(30)
      }
      .tabBar("影视")
    }
    .vertical(true)
    .barWidth(100)
    .barHeight(240)
  }
}

说明

  • 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
})

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部