ArkUI
ArkUI
ArkUI(方舟 UI 框架)为应用的 UI 开发提供了完整的基础设施,包括简洁的 UI 语法、丰富的 UI 功能(组件、布局、动画以及交互事件),以及实时界面预览工具等,可以支持开发者进行可视化界面开发。
- 声明式 UI 开发框架:ArkUI 采用声明式 UI 设计语法,使得开发者可以简洁直观地描述用户界面。
- 丰富的 UI 组件:ArkUI 提供了丰富且多功能的 UI 组件和 API,满足广泛的终端应用开发需求,并易于在各类 UI 元素中添加动画,增添美观。
- 实时预览功能:开发者可以无需连接真机,即时在各种设备上预览应用的 UI 效果,加速开发和调试过程。
- 高性能体验:ArkUI 内嵌多核 UI 控件和动画,如图片、列表、网格布局等,并对运行库深度优化,确保在鸿蒙系统上实现原生般的流畅体验。
- 跨平台优势:ArkUI 框架支持在多种 HarmonyOS 设备上提供流畅、一致的用户体验,涵盖智能手机、平板和智能手表等设备。
- ArkTS API:ArkUI 借助 ArkTS API,使得开发者能够使用一套 TS/JS API,在多个 HarmonyOS 设备上提供既丰富又流畅的用户界面体验。
- 多维度的状态管理机制:ArkUI 为开发者提供了跨设备数据绑定功能和多维度的状态管理机制,支持灵活的数据驱动的 UI 变更。
- 支持多设备开发:ArkUI 提供了多维度的解决方案,进一步简化开发,包括基础的分层参数配置、栅格系统,原子化布局能力等。
- 实时预览机制:ArkUI 支持实时界面预览特性,包括一致性渲染、实时性预览和多维度预览。
- 声明式 Canvas 绘制能力:ArkUI 通过 Canvas 组件对外提供高性能2D绘制能力,满足多种场景自定义绘制诉求。
- 高性能混合开发能力:ArkUI 提供了 XComponent 组件,支持 C++/ArkTS 混合开发,满足三方渲染引擎接入,比如游戏、地图等。
- 多样化布局能力:ArkUI 为满足多种多样的布局诉求,提供了相对布局容器、灵活锚点能力、自定义布局能力。
布局
布局是 UI 的必要元素,它定义了组件在界面中的位置。ArkUI 框架提供了多种布局方式,除了基础的线性布局、层叠布局、弹性布局、相对布局、栅格布局外,也提供了相对复杂的列表、宫格、轮播。
布局通常为分层结构,一个常见的页面结构如下所示:
为实现上述效果,开发者需要在页面中声明对应的元素。其中,Page 表示页面的根节点,Column/Row 等元素为系统组件。针对不同的页面结构,ArkUI 提供了不同的布局组件来帮助开发者实现对应布局的效果,例如 Row 用于实现线性布局。
布局相关的容器组件可形成对应的布局效果。例如,List 组件可构成线性布局。
- 组件区域(蓝区方块):组件区域表示组件的大小,width、height 属性用于设置组件区域的大小。
- 组件内容区(黄色方块):组件内容区大小为组件区域大小减去组件的 border 值,组件内容区大小会作为组件内容(或者子组件)进行大小测算时的布局测算限制。
- 组件内容(绿色方块):组件内容本身占用的大小,比如文本内容占用的大小。组件内容和组件内容区不一定匹配,比如设置了固定的 width 和 height,此时组件内容的大小就是设置的 width 和 height 减去 padding 和border值,但文本内容则是通过文本布局引擎测算后得到的大小,可能出现文本真实大小小于设置的组件内容区大小。当组件内容和组件内容区大小不一致时,align 属性生效,定义组件内容在组件内容区的对齐方式,如居中对齐。
- 组件布局边界(虚线部分):组件通过 margin 属性设置外边距时,组件布局边界就是组件区域加上 margin 的大小。
声明式 UI 提供了以下10种常见布局,开发者可根据实际应用场景选择合适的布局进行页面开发。
布局 | 应用场景 |
---|---|
线性布局(Row、Column) | 如果布局内子元素超过 1 个时,且能够以某种方式线性排列时优先考虑此布局。 |
层叠布局(Stack) | 组件需要有堆叠效果时优先考虑此布局。层叠布局的堆叠效果不会占用或影响其他同容器内子组件的布局空间。例如 Panel 作为子组件弹出时将其他组件覆盖更为合理,则优先考虑在外层使用堆叠布局。 |
弹性布局(Flex) | 弹性布局是与线性布局类似的布局方式。区别在于弹性布局默认能够使子组件压缩或拉伸。在子组件需要计算拉伸或压缩比例时优先使用此布局,可使得多个容器内子组件能有更好的视觉上的填充效果。 |
相对布局(RelativeContainer) | 相对布局是在二维空间中的布局方式,不需要遵循线性布局的规则,布局方式更为自由。通过在子组件上设置锚点规则(AlignRules)使子组件能够将自己在横轴、纵轴中的位置与容器或容器内其他子组件的位置对齐。设置的锚点规则可以天然支持子元素压缩、拉伸、堆叠或形成多行效果。在页面元素分布复杂或通过线性布局会使容器嵌套层数过深时推荐使用。 |
栅格布局(GridRow、GridCol) | 栅格是多设备场景下通用的辅助定位工具,可将空间分割为有规律的栅格。栅格不同于网格布局固定的空间划分,可以实现不同设备下不同的布局,空间划分更随心所欲,从而显著降低适配不同屏幕尺寸的设计及开发成本,使得整体设计和开发流程更有秩序和节奏感,同时也保证多设备上应用显示的协调性和一致性,提升用户体验。推荐内容相同但布局不同时使用。 |
媒体查询(@ohos.mediaquery) | 媒体查询可根据不同设备类型或同设备不同状态修改应用的样式。例如根据设备和应用的不同属性信息设计不同的布局,以及屏幕发生动态改变时更新应用的页面布局。 |
列表(List) | 使用列表可以高效地显示结构化、可滚动的信息。在 ArkUI 中,列表具有垂直和水平布局能力和自适应交叉轴方向上排列个数的布局能力,超出屏幕时可以滚动。列表适合用于呈现同类数据类型或数据类型集,例如图片和文本。 |
网格(Grid) | 网格布局具有较强的页面均分能力、子元素占比控制能力。网格布局可以控制元素所占的网格数量、设置子元素横跨几行或者几列,当网格容器尺寸发生变化时,所有子元素以及间距等比例调整。推荐在需要按照固定比例或者均匀分配空间的布局场景下使用,例如计算器、相册、日历等。 |
轮播(Swiper) | 轮播组件通常用于实现广告轮播、图片预览等。 |
选项卡(Tabs) | 选项卡可以在一个页面内快速实现视图内容的切换,一方面提升查找信息的效率,另一方面精简用户单次获取到的信息量。 |
线性布局 (Row/Column)
线性布局(LinearLayout)是开发中最常用的布局,通过线性容器 Row 和 Column 构建。线性布局是其他布局的基础,其子元素在线性方向上(水平方向和垂直方向)依次排列。线性布局的排列方向由所选容器组件决定,Column 容器内子元素按照垂直方向排列,Row 容器内子元素按照水平方向排列。根据不同的排列方向,开发者可选择使用 Row 或 Column 容器创建线性布局。
Column 容器内子元素排列示意图:
Row 容器内子元素排列示意图:
基本概念
- 布局容器:具有布局能力的容器组件,可以承载其他元素作为其子元素,布局容器会对其子元素进行尺寸计算和布局排列。
- 布局子元素:布局容器内部的元素。
- 主轴:线性布局容器在布局方向上的轴线,子元素默认沿主轴排列。Row 容器主轴为水平方向,Column 容器主轴为垂直方向。
- 交叉轴:垂直于主轴方向的轴线。Row 容器交叉轴为垂直方向,Column 容器交叉轴为水平方向。
- 间距:布局子元素的间距。
布局子元素在排列方向上的间距
在布局容器内,可以通过 space 属性设置排列方向上子元素的间距,使各子元素在排列方向上有等间距效果。
Column 容器内排列方向上的间距
Column 容器内排列方向的间距图:
Column({ space: 20 }) {
Text('space: 20').fontSize(15).fontColor(Color.Gray).width('90%')
Row().width('90%').height(50).backgroundColor(0xF5DEB3)
Row().width('90%').height(50).backgroundColor(0xD2B48C)
Row().width('90%').height(50).backgroundColor(0xF5DEB3)
}.width('100%')
Row 容器内排列方向上的间距
Row 容器内排列方向的间距图:
Row({ space: 35 }) {
Text('space: 35').fontSize(15).fontColor(Color.Gray)
Row().width('10%').height(150).backgroundColor(0xF5DEB3)
Row().width('10%').height(150).backgroundColor(0xD2B48C)
Row().width('10%').height(150).backgroundColor(0xF5DEB3)
}.width('90%')
布局子元素在交叉轴上的对齐方式
在布局容器内,可以通过 alignItems 属性设置子元素在交叉轴(排列方向的垂直方向)上的对齐方式。且在各类尺寸屏幕中,表现一致。其中,交叉轴为垂直方向时,取值为 VerticalAlign 类型,水平方向取值为 HorizontalAlign 类型。
alignSelf 属性用于控制单个子元素在容器交叉轴上的对齐方式,其优先级高于 alignItems 属性,如果设置了 alignSelf 属性,则在单个子元素上会覆盖 alignItems 属性。
Column 容器内子元素在水平方向上的排列
Column 容器内子元素在水平方向上的排列图:
- HorizontalAlign.Start:子元素在水平方向左对齐。
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)')
- HorizontalAlign.Center:子元素在水平方向居中对齐。
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.Center).backgroundColor('rgb(242,242,242)')
- HorizontalAlign.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.End).backgroundColor('rgb(242,242,242)')
Row 容器内子元素在垂直方向上的排列
Row 容器内子元素在垂直方向上的排列图:
- VerticalAlign.Top:子元素在垂直方向顶部对齐。
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)')
- VerticalAlign.Center:子元素在垂直方向居中对齐。
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.Center).backgroundColor('rgb(242,242,242)')
- VerticalAlign.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.Bottom).backgroundColor('rgb(242,242,242)')
布局子元素在主轴上的排列方式
在布局容器内,可以通过 justifyContent 属性设置子元素在容器主轴上的排列方式。可以从主轴起始位置开始排布,也可以从主轴结束位置开始排布,或者均匀分割主轴的空间。
Column 容器内子元素在垂直方向上的排列
Column 容器内子元素在垂直方向上的排列图:
- justifyContent(FlexAlign.Start):元素在垂直方向方向首端对齐,第一个元素与行首对齐,同时后续的元素与前一个对齐。
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)
- justifyContent(FlexAlign.Center):元素在垂直方向方向中心对齐,第一个元素与行首的距离与最后一个元素与行尾距离相同。
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.Center)
- justifyContent(FlexAlign.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%').height(300).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.End)
- justifyContent(FlexAlign.SpaceBetween):垂直方向均匀分配元素,相邻元素之间距离相同。第一个元素与行首对齐,最后一个元素与行尾对齐。
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.SpaceBetween)
- justifyContent(FlexAlign.SpaceAround):垂直方向均匀分配元素,相邻元素之间距离相同。第一个元素到行首的距离和最后一个元素到行尾的距离是相邻元素之间距离的一半。
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.SpaceAround)
- justifyContent(FlexAlign.SpaceEvenly):垂直方向均匀分配元素,相邻元素之间的距离、第一个元素与行首的间距、最后一个元素到行尾的间距都完全一样。
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.SpaceEvenly)
Row 容器内子元素在水平方向上的排列
Row容器内子元素在水平方向上的排列图:
- justifyContent(FlexAlign.Start):元素在水平方向首端对齐,第一个元素与行首对齐,同时后续的元素与前一个对齐。
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)
- justifyContent(FlexAlign.Center):元素在水平方向中心对齐,第一个元素与行首的距离与最后一个元素与行尾距离相同。
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.Center)
- justifyContent(FlexAlign.End):元素在水平方向尾部对齐,最后一个元素与行尾对齐,其他元素与后一个对齐。
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.End)
- justifyContent(FlexAlign.SpaceBetween):水平方向均匀分配元素,相邻元素之间距离相同。第一个元素与行首对齐,最后一个元素与行尾对齐。
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.SpaceBetween)
- justifyContent(FlexAlign.SpaceAround):水平方向均匀分配元素,相邻元素之间距离相同。第一个元素到行首的距离和最后一个元素到行尾的距离是相邻元素之间距离的一半。
Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('20%') 的高度 (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.SpaceAround)
- justifyContent(FlexAlign.SpaceEvenly):水平方向均匀分配元素,相邻元素之间的距离、第一个元素与行首的间距、最后一个元素到行尾的间距都完全一样。
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.SpaceEvenly)
自适应拉伸
在线性布局下,常用空白填充组件 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 属性设置子元素和兄弟元素在主轴上的权重,忽略元素本身尺寸设置,使它们在任意尺寸的设备下自适应占满剩余空间。
@Entry
@Component
struct layoutWeightExample {
build() {
Column() {
Text('1:2:3').width('100%')
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%')
}.backgroundColor(0xffd306).height('30%')
Text('2:5:3').width('100%')
Row() {
Column() {
Text('layoutWeight(2)')
.textAlign(TextAlign.Center)
}.layoutWeight(2).backgroundColor(0xF5DEB3).height('100%')
Column() {
Text('layoutWeight(5)')
.textAlign(TextAlign.Center)
}.layoutWeight(5).backgroundColor(0xD2B48C).height('100%')
Column() {
Text('layoutWeight(3)')
.textAlign(TextAlign.Center)
}.layoutWeight(3).backgroundColor(0xF5DEB3).height('100%')
}.backgroundColor(0xffd306).height('30%')
}
}
}
横屏:
竖屏:
- 父容器尺寸确定时,使用百分比设置子元素和兄弟元素的宽度,使他们在任意尺寸的设备下保持固定的自适应占比。
@Entry
@Component
struct WidthExample {
build() {
Column() {
Row() {
Column() {
Text('left width 20%')
.textAlign(TextAlign.Center)
}.width('20%').backgroundColor(0xF5DEB3).height('100%')
Column() {
Text('center width 50%')
.textAlign(TextAlign.Center)
}.width('50%').backgroundColor(0xD2B48C).height('100%')
Column() {
Text('right width 30%')
.textAlign(TextAlign.Center)
}.width('30%').backgroundColor(0xF5DEB3).height('100%')
}.backgroundColor(0xffd306).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?:number|undefined) => {
if(item){
Text(item.toString())
.width('90%')
.height(150)
.backgroundColor(0xFFFFFF)
.borderRadius(15)
.fontSize(16)
.textAlign(TextAlign.Center)
.margin({ top: 10 })
}
}, (item:number) => item.toString())
}.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?:number|undefined) => {
if(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 作为容器,容器内的子元素的顺序为 Item1->Item2->Item3。
开发布局
Stack 组件为容器组件,容器内可包含各种子元素。其中子元素默认进行居中堆叠。子元素被约束在 Stack 下,进行自己的样式定义以及排列。
// xxx.ets
let MTop:Record<string,number> = { 'top': 50 }
@Entry
@Component
struct StackExample {
build() {
Column(){
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)
}
}
}
对齐方式
Stack组件通过 alignContent 实现位置的相对移动。如下图所示,支持九种对齐方式。
// xxx.ets
@Entry
@Component
struct StackExample {
build() {
Stack({ alignContent: Alignment.TopStart }) {
Text('Stack').width('90%').height('100%').backgroundColor('#e1dede').align(Alignment.BottomEnd)
Text('Item 1').width('70%').height('80%').backgroundColor(0xd2cab3).align(Alignment.BottomEnd)
Text('Item 2').width('50%').height('60%').backgroundColor(0xc1cbac).align(Alignment.BottomEnd)
}.width('100%').height(150).margin({ top: 5 })
}
}
Z 序控制
Stack 容器中兄弟组件显示层级关系可以通过 Z序控制 的 zIndex 属性改变。zIndex 值越大,显示层级越高,即 zIndex 值大的组件会覆盖在 zIndex 值小的组件上方。
在层叠布局中,如果后面子元素尺寸大于前面子元素尺寸,则前面子元素完全隐藏。
Stack({ alignContent: Alignment.BottomStart }) {
Column() {
Text('Stack子元素1').textAlign(TextAlign.End).fontSize(20)
}.width(100).height(100).backgroundColor(0xffd306)
Column() {
Text('Stack子元素2').fontSize(20)
}.width(150).height(150).backgroundColor(Color.Pink)
Column() {
Text('Stack子元素3').fontSize(20)
}.width(200).height(200).backgroundColor(Color.Grey)
}.width(350).height(350).backgroundColor(0xe0e0e0)
上图中,最后的子元素 3 的尺寸大于前面的所有子元素,所以,前面两个元素完全隐藏。改变子元素 1,子元素 2的 zIndex 属性后,可以将元素展示出来。
Stack({ alignContent: Alignment.BottomStart }) {
Column() {
Text('Stack子元素1').fontSize(20)
}.width(100).height(100).backgroundColor(0xffd306).zIndex(2)
Column() {
Text('Stack子元素2').fontSize(20)
}.width(150).height(150).backgroundColor(Color.Pink).zIndex(1)
Column() {
Text('Stack子元素3').fontSize(20)
}.width(200).height(200).backgroundColor(Color.Grey)
}.width(350).height(350).backgroundColor(0xe0e0e0)
场景示例
使用层叠布局快速搭建页面。
@Entry
@Component
struct StackSample {
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 容器示意图:
基本概念
- 主轴:Flex 组件布局方向的轴线,子元素默认沿着主轴排列。主轴开始的位置称为主轴起始点,结束位置称为主轴结束点。
- 交叉轴:垂直于主轴方向的轴线。交叉轴开始的位置称为交叉轴起始点,结束位置称为交叉轴结束点。
布局方向
在弹性布局中,容器的子元素可以按照任意方向排列。通过设置参数 direction,可以决定主轴的方向,从而控制子元素的排列方向。
弹性布局方向图:
- FlexDirection.Row(默认值):主轴为水平方向,子元素从起始端沿着水平方向开始排布。
Flex({ direction: FlexDirection.Row }) {
Text('1').width('33%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(50).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.height(70)
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
- FlexDirection.RowReverse:主轴为水平方向,子元素从终点端沿着 FlexDirection. Row 相反的方向开始排布。
Flex({ direction: FlexDirection.RowReverse }) {
Text('1').width('33%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(50).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.height(70)
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
- FlexDirection.Column:主轴为垂直方向,子元素从起始端沿着垂直方向开始排布。
Flex({ direction: FlexDirection.Column }) {
Text('1').width('100%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('100%').height(50).backgroundColor(0xD2B48C)
Text('3').width('100%').height(50).backgroundColor(0xF5DEB3)
}
.height(70)
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
- FlexDirection.ColumnReverse:主轴为垂直方向,子元素从终点端沿着 FlexDirection. Column 相反的方向开始排布。
Flex({ direction: FlexDirection.ColumnReverse }) {
Text('1').width('100%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('100%').height(50).backgroundColor(0xD2B48C)
Text('3').width('100%').height(50).backgroundColor(0xF5DEB3)
}
.height(70)
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
布局换行
弹性布局分为单行布局和多行布局。默认情况下,Flex 容器中的子元素都排在一条线(又称 “轴线”)上。wrap 属性控制当子元素主轴尺寸之和大于容器主轴尺寸时,Flex 是单行布局还是多行布局。在多行布局时,通过交叉轴方向,确认新行排列方向。
- FlexWrap.NoWrap(默认值):不换行。如果子元素的宽度总和大于父元素的宽度,则子元素会被压缩宽度。
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)
- FlexWrap.Wrap:换行,每一行子元素按照主轴方向排列。
Flex({ wrap: FlexWrap.Wrap }) {
Text('1').width('50%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('50%').height(50).backgroundColor(0xD2B48C)
Text('3').width('50%').height(50).backgroundColor(0xD2B48C)
}
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
- FlexWrap.WrapReverse:换行,每一行子元素按照主轴反方向排列。
Flex({ wrap: FlexWrap.WrapReverse}) {
Text('1').width('50%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('50%').height(50).backgroundColor(0xD2B48C)
Text('3').width('50%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
主轴对齐方式
通过 justifyContent 参数设置子元素在主轴方向的对齐方式。
- FlexAlign.Start(默认值):子元素在主轴方向起始端对齐, 第一个子元素与父元素边沿对齐,其他元素与前一个元素对齐。
let PTopBottom:Record<string,number> = { 'top': 10, 'bottom': 10 }
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(PTopBottom)
.backgroundColor(0xAFEEEE)
- FlexAlign.Center:子元素在主轴方向居中对齐。
let PTopBottom:Record<string,number> = { 'top': 10, 'bottom': 10 }
Flex({ justifyContent: FlexAlign.Center }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding(PTopBottom)
.backgroundColor(0xAFEEEE)
- FlexAlign.End:子元素在主轴方向终点端对齐, 最后一个子元素与父元素边沿对齐,其他元素与后一个元素对齐。
let PTopBottom:Record<string,number> = { 'top': 10, 'bottom': 10 }
Flex({ justifyContent: FlexAlign.End }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding(PTopBottom)
.backgroundColor(0xAFEEEE)
- FlexAlign.SpaceBetween:Flex主轴方向均匀分配弹性元素,相邻子元素之间距离相同。第一个子元素和最后一个子元素与父元素边沿对齐。
let PTopBottom1:Record<string,number> = { 'top': 10, 'bottom': 10 }
Flex({ justifyContent: FlexAlign.SpaceBetween }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding(PTopBottom1)
.backgroundColor(0xAFEEEE)
- FlexAlign.SpaceAround:Flex主轴方向均匀分配弹性元素,相邻子元素之间距离相同。第一个子元素到主轴起始端的距离和最后一个子元素到主轴终点端的距离是相邻元素之间距离的一半。
let PTopBottom:Record<string,number> = { 'top': 10, 'bottom': 10 }
Flex({ justifyContent: FlexAlign.SpaceAround }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding(PTopBottom)
.backgroundColor(0xAFEEEE)
- FlexAlign.SpaceEvenly:Flex主轴方向元素等间距布局,相邻子元素之间的间距、第一个子元素与主轴起始端的间距、最后一个子元素到主轴终点端的间距均相等。
let PTopBottom:Record<string,number> = { 'top': 10, 'bottom': 10 }
Flex({ justifyContent: FlexAlign.SpaceEvenly }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding(PTopBottom)
.backgroundColor(0xAFEEEE)
交叉轴对齐方式
容器和子元素都可以设置交叉轴对齐方式,且子元素设置的对齐方式优先级较高。
容器组件设置交叉轴对齐
可以通过 Flex 组件的 alignItems 参数设置子元素在交叉轴的对齐方式。
- ItemAlign.Auto:使用 Flex 容器中默认配置。
let SWh:Record<string,number|string> = { 'width': '90%', 'height': 80 }
Flex({ alignItems: ItemAlign.Auto }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size(SWh)
.padding(10)
.backgroundColor(0xAFEEEE)
- ItemAlign.Start:交叉轴方向首部对齐。
let SWh:Record<string,number|string> = { 'width': '90%', 'height': 80 }
Flex({ alignItems: ItemAlign.Start }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size(SWh)
.padding(10)
.backgroundColor(0xAFEEEE)
- ItemAlign.Center:交叉轴方向居中对齐。
let SWh:Record<string,number|string> = { 'width': '90%', 'height': 80 }
Flex({ alignItems: ItemAlign.Center }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size(SWh)
.padding(10)
.backgroundColor(0xAFEEEE)
- ItemAlign.End:交叉轴方向底部对齐。
let SWh:Record<string,number|string> = { 'width': '90%', 'height': 80 }
Flex({ alignItems: ItemAlign.End }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size(SWh)
.padding(10)
.backgroundColor(0xAFEEEE)
- ItemAlign.Stretch:交叉轴方向拉伸填充,在未设置尺寸时,拉伸到容器尺寸。
let SWh:Record<string,number|string> = { 'width': '90%', 'height': 80 }
Flex({ alignItems: ItemAlign.Stretch }) {
Text('1').width('33%').backgroundColor(0xF5DEB3)
Text('2').width('33%').backgroundColor(0xD2B48C)
Text('3').width('33%').backgroundColor(0xF5DEB3)
}
.size(SWh)
.padding(10)
.backgroundColor(0xAFEEEE)
- ItemAlign. Baseline:交叉轴方向文本基线对齐。
let SWh:Record<string,number|string> = { 'width': '90%', 'height': 80 }
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(SWh)
.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:子元素各行与交叉轴起点对齐。
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.Start }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
- FlexAlign.Center:子元素各行在交叉轴方向居中对齐。
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.Center }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
- FlexAlign.End:子元素各行与交叉轴终点对齐。
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.End }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
- FlexAlign.SpaceBetween:子元素各行与交叉轴两端对齐,各行间垂直间距平均分布。
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.SpaceBetween }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
- FlexAlign.SpaceAround:子元素各行间距相等,是元素首尾行与交叉轴两端距离的两倍。
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.SpaceAround }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
- FlexAlign.SpaceEvenly: 子元素各行间距,子元素首尾行与交叉轴两端距离都相等。
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.SpaceEvenly }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
自适应拉伸
在弹性布局父组件尺寸过小时,通过子元素的以下属性设置其在父容器的占比,达到自适应布局。
- flexBasis:设置子元素在父容器主轴方向上的基准尺寸。如果设置了该属性,则子项占用的空间为该属性所设置的值;如果没设置该属性,那子项的空间为 width/height 的值。
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
.flexBasis(100)
.height(100)
.backgroundColor(0xF5DEB3)
Text('flexBasis(100)')
.flexBasis(100)
.width(200) // flexBasis值为100,覆盖width的设置值,宽度为100vp
.height(100)
.backgroundColor(0xD2B48C)
}.width('90%').height(120).padding(10).backgroundColor(0xAFEEEE)
- flexGrow:设置父容器的剩余空间分配给此属性所在组件的比例。用于分配父组件的剩余空间。
Flex() {
Text('flexGrow(2)')
.flexGrow(2)
.width(100)
.height(100)
.backgroundColor(0xF5DEB3)
Text('flexGrow(3)')
.flexGrow(3)
.width(100)
.height(100)
.backgroundColor(0xD2B48C)
Text('no flexGrow')
.width(100)
.height(100)
.backgroundColor(0xF5DEB3)
}.width(420).height(120).padding(10).backgroundColor(0xAFEEEE)
父容器宽度 420vp,三个子元素原始宽度为 100vp,左右 padding 为 20vp,总和 320vp,剩余空间 100vp 根据 flexGrow 值的占比分配给子元素,未设置 flexGrow 的子元素不参与“瓜分”。
第一个元素以及第二个元素以 2:3 分配剩下的 100vp。第一个元素为 100vp+100vp * 2/5=140vp,第二个元素为 100vp+100vp * 3/5=160vp。
- flexShrink: 当父容器空间不足时,子元素的压缩比例。
Flex({ direction: FlexDirection.Row }) {
Text('flexShrink(3)')
.flexShrink(3)
.width(200)
.height(100)
.backgroundColor(0xF5DEB3)
Text('no flexShrink')
.width(200)
.height(100)
.backgroundColor(0xD2B48C)
Text('flexShrink(2)')
.flexShrink(2)
.width(200)
.height(100)
.backgroundColor(0xF5DEB3)
}.width(400).height(120).padding(10).backgroundColor(0xAFEEEE)
场景示例
使用弹性布局,可以实现子元素沿水平方向排列,两端对齐,子元素间距平分,垂直方向上子元素居中的效果。
@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 为采用相对布局的容器,支持容器内部的子元素设置相对位置关系,适用于界面复杂场景的情况,对多个子组件进行对齐和排列。子元素支持指定兄弟元素作为锚点,也支持指定父容器作为锚点,基于锚点做相对位置布局。下图是一个 RelativeContainer 的概念图,图中的虚线表示位置的依赖关系。
子元素并不完全是上图中的依赖关系。比如,Item4 可以以 Item2 为依赖锚点,也可以以 RelativeContainer 父容器为依赖锚点。
基本概念
- 锚点:通过锚点设置当前元素基于哪个元素确定位置。
- 对齐方式:通过对齐方式,设置当前元素是基于锚点的上中下对齐,还是基于锚点的左中右对齐。
设置依赖关系
锚点设置
锚点设置是指设置子元素相对于父元素或兄弟元素的位置依赖关系。在水平方向上,可以设置 left、middle、right 的锚点。在竖直方向上,可以设置 top、center、bottom 的锚点。
为了明确定义锚点,必须为 RelativeContainer 及其子元素设置 ID,用于指定锚点信息。ID 默认为 __container__
,其余子元素的 ID 通过 id 属性设置。不设置 id 的组件能显示,但是不能被其他子组件作为锚点,相对布局容器会为其拼接 id,此 id 的规律无法被应用感知。互相依赖,环形依赖时容器内子组件全部不绘制。同方向上两个以上位置设置锚点,但锚点位置逆序时此子组件大小为 0,即不绘制。
说明:
在使用锚点时要注意子元素的相对位置关系,避免出现错位或遮挡的情况。
- RelativeContainer 父组件为锚点,
__container__
代表父容器的 ID。
let AlignRus:Record<string,Record<string,string|VerticalAlign|HorizontalAlign>> = {
'top': { 'anchor': '__container__', 'align': VerticalAlign.Top },
'left': { 'anchor': '__container__', 'align': HorizontalAlign.Start }
}
let AlignRue:Record<string,Record<string,string|VerticalAlign|HorizontalAlign>> = {
'top': { 'anchor': '__container__', 'align': VerticalAlign.Top },
'right': { 'anchor': '__container__', 'align': HorizontalAlign.End }
}
let Mleft:Record<string,number> = { 'left': 20 }
let BWC:Record<string,number|string> = { 'width': 2, 'color': '#6699FF' }
@Entry
@Component
struct Index {
build() {
RelativeContainer() {
Row(){Text('row1')}.justifyContent(FlexAlign.Center).width(100).height(100)
.backgroundColor("#FF3333")
.alignRules(AlignRus)
.id("row1")
Row(){Text('row2')}.justifyContent(FlexAlign.Center).width(100).height(100)
.backgroundColor("#FFCC00")
.alignRules(AlignRue)
.id("row2")
}.width(300).height(300)
.margin(Mleft)
.border(BWC)
}
}
- 以兄弟元素为锚点。
let AlignRus:Record<string,Record<string,string|VerticalAlign|HorizontalAlign>> = {
'top': { 'anchor': '__container__', 'align': VerticalAlign.Top },
'left': { 'anchor': '__container__', 'align': HorizontalAlign.Start }
}
let RelConB:Record<string,Record<string,string|VerticalAlign|HorizontalAlign>> = {
'top': { 'anchor': 'row1', 'align': VerticalAlign.Bottom },
'left' : { 'anchor': 'row1', 'align': HorizontalAlign.Start }
}
let Mleft:Record<string,number> = { 'left': 20 }
let BWC:Record<string,number|string> = { 'width': 2, 'color': '#6699FF' }
@Entry
@Component
struct Index {
build() {
RelativeContainer() {
Row(){Text('row1')}.justifyContent(FlexAlign.Center).width(100).height(100)
.backgroundColor("#FF3333")
.alignRules(AlignRus)
.id("row1")
Row(){Text('row2')}.justifyContent(FlexAlign.Center).width(100).height(100)
.backgroundColor("#FFCC00")
.alignRules(RelConB)
.id("row2")
}.width(300).height(300)
.margin(Mleft)
.border(BWC)
}
}
- 子组件锚点可以任意选择,但需注意不要相互依赖。
@Entry
@Component
struct Index {
build() {
Row() {
RelativeContainer() {
Row(){Text('row1')}.justifyContent(FlexAlign.Center).width(100).height(100)
.backgroundColor('#ff3339ff')
.alignRules({
top: {anchor: "__container__", align: VerticalAlign.Top},
left: {anchor: "__container__", align: HorizontalAlign.Start}
})
.id("row1")
Row(){Text('row2')}.justifyContent(FlexAlign.Center).width(100)
.backgroundColor('#ff298e1e')
.alignRules({
top: {anchor: "__container__", align: VerticalAlign.Top},
right: {anchor: "__container__", align: HorizontalAlign.End},
bottom: {anchor: "row1", align: VerticalAlign.Center},
})
.id("row2")
Row(){Text('row3')}.justifyContent(FlexAlign.Center).height(100)
.backgroundColor('#ffff6a33')
.alignRules({
top: {anchor: "row1", align: VerticalAlign.Bottom},
left: {anchor: "row1", align: HorizontalAlign.Start},
right: {anchor: "row2", align: HorizontalAlign.Start}
})
.id("row3")
Row(){Text('row4')}.justifyContent(FlexAlign.Center)
.backgroundColor('#ffff33fd')
.alignRules({
top: {anchor: "row3", align: VerticalAlign.Bottom},
left: {anchor: "row1", align: HorizontalAlign.Center},
right: {anchor: "row2", align: HorizontalAlign.End},
bottom: {anchor: "__container__", align: VerticalAlign.Bottom}
})
.id("row4")
}
.width(300).height(300)
.margin({left: 50})
.border({width:2, color: "#6699FF"})
}
.height('100%')
}
}
设置相对于锚点的对齐位置
设置了锚点之后,可以通过 align 设置相对于锚点的对齐位置。
在水平方向上,对齐位置可以设置为 HorizontalAlign.Start、HorizontalAlign.Center、HorizontalAlign.End。
在竖直方向上,对齐位置可以设置为 VerticalAlign.Top、VerticalAlign.Center、VerticalAlign.Bottom。
子组件位置偏移
子组件经过相对位置对齐后,位置可能还不是目标位置,开发者可根据需要进行额外偏移设置 offset。
@Entry
@Component
struct Index {
build() {
Row() {
RelativeContainer() {
Row(){Text('row1')}.justifyContent(FlexAlign.Center).width(100).height(100)
.backgroundColor("#FF3333")
.alignRules({
top: {anchor: "__container__", align: VerticalAlign.Top},
left: {anchor: "__container__", align: HorizontalAlign.Start}
})
.id("row1")
Row(){Text('row2')}.justifyContent(FlexAlign.Center).width(100)
.backgroundColor("#FFCC00")
.alignRules({
top: {anchor: "__container__", align: VerticalAlign.Top},
right: {anchor: "__container__", align: HorizontalAlign.End},
bottom: {anchor: "row1", align: VerticalAlign.Center},
})
.offset({
x:-40,
y:-20
})
.id("row2")
Row(){Text('row3')}.justifyContent(FlexAlign.Center).height(100)
.backgroundColor("#FF6633")
.alignRules({
top: {anchor: "row1", align: VerticalAlign.Bottom},
left: {anchor: "row1", align: HorizontalAlign.End},
right: {anchor: "row2", align: HorizontalAlign.Start}
})
.offset({
x:-10,
y:-20
})
.id("row3")
Row(){Text('row4')}.justifyContent(FlexAlign.Center)
.backgroundColor("#FF9966")
.alignRules({
top: {anchor: "row3", align: VerticalAlign.Bottom},
bottom: {anchor: "__container__", align: VerticalAlign.Bottom},
left: {anchor: "__container__", align: HorizontalAlign.Start},
right: {anchor: "row1", align: HorizontalAlign.End}
})
.offset({
x:-10,
y:-30
})
.id("row4")
Row(){Text('row5')}.justifyContent(FlexAlign.Center)
.backgroundColor("#FF66FF")
.alignRules({
top: {anchor: "row3", align: VerticalAlign.Bottom},
bottom: {anchor: "__container__", align: VerticalAlign.Bottom},
left: {anchor: "row2", align: HorizontalAlign.Start},
right: {anchor: "row2", align: HorizontalAlign.End}
})
.offset({
x:10,
y:20
})
.id("row5")
Row(){Text('row6')}.justifyContent(FlexAlign.Center)
.backgroundColor('#ff33ffb5')
.alignRules({
top: {anchor: "row3", align: VerticalAlign.Bottom},
bottom: {anchor: "row4", align: VerticalAlign.Bottom},
left: {anchor: "row3", align: HorizontalAlign.Start},
right: {anchor: "row3", align: HorizontalAlign.End}
})
.offset({
x:-15,
y:10
})
.backgroundImagePosition(Alignment.Bottom)
.backgroundImageSize(ImageSize.Cover)
.id("row6")
}
.width(300).height(300)
.margin({left: 50})
.border({width:2, color: "#6699FF"})
}
.height('100%')
}
}
多种组件的对齐布局
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(){Text('row1')}.justifyContent(FlexAlign.Center)
.width(100).height(100)
.backgroundColor("#FF3333")
.alignRules({
top: {anchor: "__container__", align: VerticalAlign.Top},
left: {anchor: "__container__", align: HorizontalAlign.Start}
})
.id("row1")
Row(){Text('row2')}.justifyContent(FlexAlign.Center).width(100)
.backgroundColor("#FFCC00")
.alignRules({
top: {anchor: "__container__", align: VerticalAlign.Top},
right: {anchor: "__container__", align: HorizontalAlign.End},
bottom: {anchor: "row1", align: VerticalAlign.Center},
})
.id("row2")
Row(){Text('row3')}.justifyContent(FlexAlign.Center).height(100)
.backgroundColor("#FF6633")
.alignRules({
top: {anchor: "row1", align: VerticalAlign.Bottom},
left: {anchor: "row1", align: HorizontalAlign.End},
right: {anchor: "row2", align: HorizontalAlign.Start}
})
.id("row3")
Row(){Text('row4')}.justifyContent(FlexAlign.Center)
.backgroundColor("#FF9966")
.alignRules({
top: {anchor: "row3", align: VerticalAlign.Bottom},
bottom: {anchor: "__container__", align: VerticalAlign.Bottom},
left: {anchor: "__container__", align: HorizontalAlign.Start},
right: {anchor: "row1", align: HorizontalAlign.End}
})
.id("row4")
Row(){Text('row5')}.justifyContent(FlexAlign.Center)
.backgroundColor("#FF66FF")
.alignRules({
top: {anchor: "row3", align: VerticalAlign.Bottom},
bottom: {anchor: "__container__", align: VerticalAlign.Bottom},
left: {anchor: "row2", align: HorizontalAlign.Start},
right: {anchor: "row2", align: HorizontalAlign.End}
})
.id("row5")
Row(){Text('row6')}.justifyContent(FlexAlign.Center)
.backgroundColor('#ff33ffb5')
.alignRules({
top: {anchor: "row3", align: VerticalAlign.Bottom},
bottom: {anchor: "row4", align: VerticalAlign.Bottom},
left: {anchor: "row3", align: HorizontalAlign.Start},
right: {anchor: "row3", align: HorizontalAlign.End}
})
.id("row6")
.backgroundImagePosition(Alignment.Bottom)
.backgroundImageSize(ImageSize.Cover)
}
.width(300).height(300)
.margin({left: 50})
.border({width:2, color: "#6699FF"})
}
.height('100%')
}
}
栅格布局 (GridRow/GridCol)
栅格布局是一种通用的辅助定位工具,对移动设备的界面设计有较好的借鉴作用。主要优势包括:
- 提供可循的规律:栅格布局可以为布局提供规律性的结构,解决多尺寸多设备的动态布局问题。通过将页面划分为等宽的列数和行数,可以方便地对页面元素进行定位和排版。
- 统一的定位标注:栅格布局可以为系统提供一种统一的定位标注,保证不同设备上各个模块的布局一致性。这可以减少设计和开发的复杂度,提高工作效率。
- 灵活的间距调整方法:栅格布局可以提供一种灵活的间距调整方法,满足特殊场景布局调整的需求。通过调整列与列之间和行与行之间的间距,可以控制整个页面的排版效果。
- 自动换行和自适应:栅格布局可以完成一对多布局的自动换行和自适应。当页面元素的数量超出了一行或一列的容量时,他们会自动换到下一行或下一列,并且在不同的设备上自适应排版,使得页面布局更加灵活和适应性强。
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 设置断点切换参考物。 考虑到应用可能以非全屏窗口的形式显示,以应用窗口宽度为参照物更为通用。
例如,使用栅格的默认列数 12 列,通过断点设置将应用宽度分成六个区间,在各区间中,每个栅格子元素占用的列数均不同。
@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown];
...
GridRow({
breakpoints: {
value: ['200vp', '300vp', '400vp', '500vp', '600vp'],
reference: BreakpointsReference.WindowSize
}
}) {
ForEach(this.bgColors, (color:Color, index?:number|undefined) => {
GridCol({
span: {
xs: 2, // 在最小宽度类型设备上,栅格子组件占据的栅格容器2列。
sm: 3, // 在小宽度类型设备上,栅格子组件占据的栅格容器3列。
md: 4, // 在中等宽度类型设备上,栅格子组件占据的栅格容器4列。
lg: 6, // 在大宽度类型设备上,栅格子组件占据的栅格容器6列。
xl: 8, // 在特大宽度类型设备上,栅格子组件占据的栅格容器8列。
xxl: 12 // 在超大宽度类型设备上,栅格子组件占据的栅格容器12列。
}
}) {
Row() {
Text(`${index}`)
}.width("100%").height('50vp')
}.backgroundColor(color)
})
}
布局的总列数
GridRow 中通过 columns 设置栅格布局的总列数。
- columns 默认值为 12,即在未设置 columns 时,任何断点下,栅格布局被分成 12 列。
@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown,Color.Red, Color.Orange, Color.Yellow, Color.Green];
...
GridRow() {
ForEach(this.bgColors, (item:Color, index?:number|undefined) => {
GridCol() {
Row() {
Text(`${index}`)
}.width('100%').height('50')
}.backgroundColor(item)
})
}
- 当 columns 为自定义值,栅格布局在任何尺寸设备下都被分为 columns 列。下面分别设置栅格布局列数为 4 和 8,子元素默认占一列,效果如下:
class CurrTmp{
currentBp: string = 'unknown';
set(val:string){
this.currentBp = val
}
}
let BorderWH:Record<string,Color|number> = { 'color': Color.Blue, 'width': 2 }
@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown];
@State currentBp: string = 'unknown';
...
Row() {
GridRow({ columns: 4 }) {
ForEach(this.bgColors, (item:Color, index?:number|undefined) => {
GridCol() {
Row() {
Text(`${index}`)
}.width('100%').height('50')
}.backgroundColor(item)
})
}
.width('100%').height('100%')
.onBreakpointChange((breakpoint:string) => {
let CurrSet:CurrTmp = new CurrTmp()
CurrSet.set(breakpoint)
})
}
.height(160)
.border(BorderWH)
.width('90%')
Row() {
GridRow({ columns: 8 }) {
ForEach(this.bgColors, (item:Color, index?:number|undefined) => {
GridCol() {
Row() {
Text(`${index}`)
}.width('100%').height('50')
}.backgroundColor(item)
})
}
.width('100%').height('100%')
.onBreakpointChange((breakpoint:string) => {
let CurrSet:CurrTmp = new CurrTmp()
CurrSet.set(breakpoint)
})
}
.height(160)
.border(BorderWH)
.width('90%')
- 当 columns 类型为 GridRowColumnOption 时,支持下面六种不同尺寸(xs, sm, md, lg, xl, xxl)设备的总列数设置,各个尺寸下数值可不同。
@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown]
GridRow({ columns: { sm: 4, md: 8 }, breakpoints: { value: ['200vp', '300vp', '400vp', '500vp', '600vp'] } }) {
ForEach(this.bgColors, (item:Color, index?:number|undefined) => {
GridCol() {
Row() {
Text(`${index}`)
}.width('100%').height('50')
}.backgroundColor(item)
})
}
若只设置 sm, md 的栅格总列数,则较小的尺寸使用默认 columns 值 12,较大的尺寸使用前一个尺寸的 columns。这里只设置 sm:4, md:8,则较小尺寸的 xs:12,较大尺寸的参照 md 的设置,lg:8, xl:8, xxl:8
排列方向
栅格布局中,可以通过设置 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(元素序号)的值。
设置 span。
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)
设置 offset。
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)
设置 order。
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:Color, index?:number|undefined) => {
GridCol({ span: 2 }) {
Row() {
Text(`${index}`)
}.width('100%').height('50vp')
}
.backgroundColor(color)
})
}
- 当类型为 GridColColumnOption 时,支持六种不同尺寸(xs, sm, md, lg, xl, xxl)设备中子组件所占列数设置,各个尺寸下数值可不同。
@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown];
...
GridRow({ columns: 8 }) {
ForEach(this.bgColors, (color:Color, index?:number|undefined) => {
GridCol({ span: { xs: 1, sm: 2, md: 3, lg: 4 } }) {
Row() {
Text(`${index}`)
}.width('100%').height('50vp')
}
.backgroundColor(color)
})
}
offset
栅格子组件相对于前一个子组件的偏移列数,默认为 0。
- 当类型为number时,子组件偏移相同列数。
@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown];
...
GridRow() {
ForEach(this.bgColors, (color:Color, index?:number|undefined) => {
GridCol({ offset: 2 }) {
Row() {
Text('' + index)
}.width('100%').height('50vp')
}
.backgroundColor(color)
})
}
栅格默认分成 12 列,每一个子组件默认占 1 列,偏移 2 列,每个子组件及间距共占 3 列,一行放四个子组件。
- 当类型为 GridColColumnOption 时,支持六种不同尺寸(xs, sm, md, lg, xl, xxl)设备中子组件所占列数设置,各个尺寸下数值可不同。
@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown];
...
GridRow() {
ForEach(this.bgColors, (color:Color, index?:number|undefined) => {
GridCol({ offset: { xs: 1, sm: 2, md: 3, lg: 4 } }) {
Row() {
Text('' + index)
}.width('100%').height('50vp')
}
.backgroundColor(color)
})
}
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')
}
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 等参数,即可确定最终布局,无需关心具体的设备类型及设备状态(如横竖屏)等。
媒体查询 (@ohos.mediaquery)
媒体查询作为响应式设计的核心,在移动设备上应用十分广泛。媒体查询可根据不同设备类型或同设备不同状态修改应用的样式。媒体查询常用于下面两种场景:
- 针对设备和应用的属性信息(比如显示区域、深浅色、分辨率),设计出相匹配的布局。
- 当屏幕发生动态改变时(比如分屏、横竖屏切换),同步更新应用的页面布局。
引入与使用流程
媒体查询通过 mediaquery 模块接口,设置查询条件并绑定回调函数,任一媒体特征改变时,均会触发回调函数,返回匹配结果,根据返回值更改页面布局或者实现业务逻辑,实现页面的响应式设计。具体步骤如下:
首先导入媒体查询模块。
import { mediaquery } from '@kit.ArkUI';
通过 matchMediaSync 接口设置媒体查询条件,保存返回的条件监听句柄 listener。例如监听横屏事件:
let listener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync('(orientation: landscape)');
给条件监听句柄 listener 绑定回调函数 onPortrait,当 listener 检测设备状态变化时执行回调函数。在回调函数内,根据不同设备状态更改页面布局或者实现业务逻辑。
onPortrait(mediaQueryResult: mediaquery.MediaQueryResult) {
if (mediaQueryResult.matches as boolean) {
// do something here
} else {
// do something here
}
}
listener.on('change', onPortrait);
媒体查询条件
媒体查询条件由媒体类型、逻辑操作符、媒体特征组成,其中媒体类型可省略,逻辑操作符用于连接不同媒体类型与媒体特征,其中,媒体特征要使用 “()” 包裹且可以有多个。
语法规则
语法规则包括媒体类型(media-type)、媒体逻辑操作(media-logic-operations)和媒体特征(media-feature)。
[media-type] [media-logic-operations] [(media-feature)]
例如:
screen and (round-screen: true)
:表示当设备屏幕是圆形时条件成立。(max-height: 800px)
:表示当高度小于等于 800px 时条件成立。(height <= 800px)
:表示当高度小于等于 800px 时条件成立。screen and (device-type: tv) or (resolution < 2)
:表示包含多个媒体特征的多条件复杂语句查询,当设备类型为 tv 或设备分辨率小于 2 时条件成立。(dark-mode: true)
:表示当系统为深色模式时成立。
媒体类型(media-type)
查询条件未写媒体类型时,默认为 screen。媒体类型必须写在查询条件开头。
类型 | 说明 |
---|---|
screen | 按屏幕相关参数进行媒体查询。 |
媒体逻辑操作(media-logic-operations)
媒体逻辑操作符:and、or、not、only 用于构成复杂媒体查询,也可以通过 comma(, )将其组合起来,详细解释说明如下表。
类型 | 说明 |
---|---|
and | 将多个媒体特征(Media Feature)以“与”的方式连接成一个媒体查询,只有当所有媒体特征都为 true,查询条件成立。另外,它还可以将媒体类型和媒体功能结合起来。例如:screen and (device-type: wearable) and (max-height: 600px) 表示当设备类型是智能穿戴且应用的最大高度小于等于 600 个像素单位时成立。 |
or | 将多个媒体特征以“或”的方式连接成一个媒体查询,如果存在结果为 true 的媒体特征,则查询条件成立。例如:screen and (max-height: 1000px) or (round-screen: true) 表示当应用高度小于等于 1000 个像素单位或者设备屏幕是圆形时,条件成立。 |
not | not 操作符必须搭配 screen 使用,取反媒体查询结果,媒体查询结果不成立时返回 true,否则返回 false。例如:not screen and (min-height: 50px) and (max-height: 600px) 表示当应用高度小于 50 个像素单位或者大于 600 个像素单位时成立。 |
only | only 操作符必须搭配 screen 使用, 当前效果与单独使用 screen 相同。例如:only screen and (height <= 50) |
comma(, ) | 将多个媒体特征以“或”的方式连接成一个媒体查询,如果存在结果为 true 的媒体特征,则查询条件成立。其效果等同于 or 运算符。例如:screen and (min-height: 1000px), (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。深色模式为 true,浅色模式为 false。 |
比较 height、width 等宽高尺寸时,支持 vp 和 px 单位,无单位默认为 px。
说明
目前在卡片中使用媒体查询,只支持 height、width。
场景示例
下例中使用媒体查询,实现屏幕横竖屏切换时,给页面文本应用添加不同的内容和样式。
Stage 模型下的示例:
import { mediaquery, window } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
@Entry
@Component
struct MediaQueryExample {
@State color: string = '#DB7093';
@State text: string = 'Portrait';
// 当设备横屏时条件成立
listener:mediaquery.MediaQueryListener = this.getUIContext().getMediaQuery().matchMediaSync('(orientation: landscape)');
// 当满足媒体查询条件时,触发回调
onPortrait(mediaQueryResult:mediaquery.MediaQueryResult) {
if (mediaQueryResult.matches as boolean) { // 若设备为横屏状态,更改相应的页面布局
this.color = '#FFD700';
this.text = 'Landscape';
} else {
this.color = '#DB7093';
this.text = 'Portrait';
}
}
aboutToAppear() {
// 绑定当前应用实例
// 绑定回调函数
this.listener.on('change', (mediaQueryResult: mediaquery.MediaQueryResult) => {
this.onPortrait(mediaQueryResult)
});
}
aboutToDisappear() {
// 解绑listener中注册的回调函数
this.listener.off('change');
}
// 改变设备横竖屏状态函数
private changeOrientation(isLandscape: boolean) {
// 获取UIAbility实例的上下文信息
let context:common.UIAbilityContext = this.getUIContext().getHostContext() 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%')
}
}
竖屏:
横屏:
创建列表 (List)
列表是一种复杂的容器,当列表项达到一定数量,内容超过屏幕大小时,可以自动提供滚动功能。它适合用于呈现同类数据类型或数据类型集,例如图片和文本。在列表中显示数据集合是许多应用程序中的常见要求(如通讯录、音乐列表、购物清单等)。
使用列表可以轻松高效地显示结构化、可滚动的信息。通过在 List 组件中按垂直或者水平方向线性排列子组件 ListItemGroup 或 ListItem,为列表中的行或列提供单个视图,或使用 循环渲染 迭代一组行或列,或混合任意数量的单个视图和 ForEach 结构,构建一个列表。List 组件支持使用条件渲染、循环渲染、懒加载等渲染控制方式生成子组件。
布局与约束
列表作为一种容器,会自动按其滚动方向排列子组件,向列表中添加组件或从列表中移除组件会重新排列子组件。
如下图所示,在垂直列表中,List 按垂直方向自动排列 ListItemGroup 或 ListItem。
ListItemGroup 用于列表数据的分组展示,其子组件也是 ListItem。ListItem 表示单个列表项,可以包含单个子组件。
List、ListItemGroup和ListItem组件关系:
说明
List 的子组件必须是 ListItemGroup 或 ListItem,ListItem 和 ListItemGroup 必须配合 List 来使用。
布局
List 除了提供垂直和水平布局能力、超出屏幕时可以滚动的自适应延伸能力之外,还提供了自适应交叉轴方向上排列个数的布局能力。
利用垂直布局能力可以构建单列或者多列垂直滚动列表,如下图所示,垂直滚动列表(左:单列;右:多列):
利用水平布局能力可以是构建单行或多行水平滚动列表,如下图所示,水平滚动列表(左:单行;右:多行):
Grid 和 WaterFlow 也可以实现单列、多列布局,如果布局每列等宽,且不需要跨行跨列布局,相比 Gird 和 WaterFlow,则更推荐使用 List。
约束
列表的主轴方向是指子组件列的排列方向,也是列表的滚动方向。垂直于主轴的轴称为交叉轴,其方向与主轴方向相互垂直。
如下图所示,垂直列表的主轴是垂直方向,交叉轴是水平方向;水平列表的主轴是水平方向,交叉轴是垂直方向。
如果 List 组件主轴或交叉轴方向设置了尺寸,则其对应方向上的尺寸为设置值。
如果 List 组件主轴方向没有设置尺寸,当 List 子组件主轴方向总尺寸小于 List 的父组件尺寸时,List 主轴方向尺寸自动适应子组件的总尺寸。
如下图所示,一个垂直列表 B 没有设置高度时,其父组件 A 高度为 200vp,若其所有子组件 C 的高度总和为 150vp,则此时列表 B 的高度为 150vp。
列表主轴高度约束示例1(A: List 的父组件; B: List 组件; C: List 的所有子组件)
如果子组件主轴方向总尺寸超过 List 父组件尺寸时,List 主轴方向尺寸适应 List 的父组件尺寸。
如下图所示,同样是没有设置高度的垂直列表 B,其父组件 A 高度为 200vp,若其所有子组件 C 的高度总和为 300vp,则此时列表 B 的高度为 200vp。
列表主轴高度约束示例2(A: List 的父组件; B: List 组件; C: List 的所有子组件)
List 组件交叉轴方向在没有设置尺寸时,其尺寸默认自适应父组件尺寸。
开发布局
设置主轴方向
List 组件主轴默认是垂直方向,即默认情况下不需要手动设置 List 方向,就可以构建一个垂直滚动列表。
若是水平滚动列表场景,将 List 的 listDirection 属性设置为 Axis.Horizontal 即可实现。listDirection 默认为 Axis.Vertical,即主轴默认是垂直方向。
List() {
// ...
}
.listDirection(Axis.Horizontal)
设置交叉轴布局
List 组件的交叉轴布局可以通过 lanes 和 alignListItem 属性进行设置,lanes 属性用于确定交叉轴排列的列表项数量,alignListItem 用于设置子组件在交叉轴方向的对齐方式。
List 组件的 lanes 属性通常用于在不同尺寸的设备自适应构建不同行数或列数的列表,即一次开发、多端部署的场景,例如歌单列表。lanes 属性的取值类型是 "number | LengthConstrain",即整数或者 LengthConstrain 类型。以垂直列表为例,如果将 lanes 属性设为 2,表示构建的是一个两列的垂直列表。lanes 的默认值为 1,即默认情况下,垂直列表的列数是 1。
List() {
// ...
}
.lanes(2)
当其取值为 LengthConstrain 类型时,表示会根据 LengthConstrain 与 List 组件的尺寸自适应决定行或列数。
@Entry
@Component
struct EgLanes {
@State egLanes: LengthConstrain = { minLength: 200, maxLength: 300 }
build() {
List() {
// ...
}
.lanes(this.egLanes)
}
}
例如,假设在垂直列表中设置了 lanes 的值为 { minLength: 200, maxLength: 300 }。此时,
- 当 List 组件宽度为 300vp 时,由于 minLength 为 200vp,此时列表为一列。
- 当 List 组件宽度变化至 400vp 时,符合两倍的 minLength,则此时列表自适应为两列。
同样以垂直列表为例,当 alignListItem 属性设置为 ListItemAlign.Center 表示列表项在水平方向上居中对齐。alignListItem 的默认值是 ListItemAlign.Start,即列表项在列表交叉轴方向上默认按首部对齐。
List() {
// ...
}
.alignListItem(ListItemAlign.Center)
在列表中显示数据
列表视图垂直或水平显示项目集合,在行或列超出屏幕时提供滚动功能,使其适合显示大型数据集合。在最简单的列表形式中,List 静态地创建其列表项 ListItem 的内容。
@Entry
@Component
struct CityList {
build() {
List() {
ListItem() {
Text('北京').fontSize(24)
}
ListItem() {
Text('杭州').fontSize(24)
}
ListItem() {
Text('上海').fontSize(24)
}
}
.backgroundColor('#FFF1F3F5')
.alignListItem(ListItemAlign.Center)
}
}
由于在 ListItem 中只能有一个根节点组件,不支持以平铺形式使用多个组件。因此,若列表项是由多个组件元素组成的,则需要将这多个元素组合到一个容器组件内或组成一个自定义组件。
联系人列表项示例:
如上图所示,联系人列表的列表项中,每个联系人都有头像和名称。此时,需要将Image和Text封装到一个Row容器内。
List() {
ListItem() {
Row() {
Image($r('app.media.iconE'))
.width(40)
.height(40)
.margin(10)
Text('小明')
.fontSize(20)
}
}
ListItem() {
Row() {
Image($r('app.media.iconF'))
.width(40)
.height(40)
.margin(10)
Text('小红')
.fontSize(20)
}
}
}
迭代列表内容
通常,应用通过数据集合动态地创建列表。使用循环渲染可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件,降低代码复杂度。
ArkTS 通过 ForEach 提供了组件的循环渲染能力。以简单形式的联系人列表为例,将联系人名称和头像数据以 Contact 类结构存储到 contacts 数组,使用 ForEach 中嵌套 ListItem 的形式来代替多个平铺的、内容相似的 ListItem,从而减少重复代码。
import { util } from '@kit.ArkTS'
class Contact {
key: string = util.generateRandomUUID(true);
name: string;
icon: Resource;
constructor(name: string, icon: Resource) {
this.name = name;
this.icon = icon;
}
}
@Entry
@Component
struct SimpleContacts {
private contacts: Array<object> = [
new Contact('小明', $r("app.media.iconA")),
new Contact('小红', $r("app.media.iconB")),
]
build() {
List() {
ForEach(this.contacts, (item: Contact) => {
ListItem() {
Row() {
Image(item.icon)
.width(40)
.height(40)
.margin(10)
Text(item.name).fontSize(20)
}
.width('100%')
.justifyContent(FlexAlign.Start)
}
}, (item: Contact) => JSON.stringify(item))
}
.width('100%')
}
}
在 List 组件中,ForEach 除了可以用来循环渲染 ListItem,也可以用来循环渲染 ListItemGroup。ListItemGroup 的循环渲染详细使用请参见支持分组列表。
自定义列表样式
设置内容间距
在初始化列表时,如需在列表项之间添加间距,可以使用 space 参数。例如,在每个列表项之间沿主轴方向添加 10vp 的间距:
List({ space: 10 }) {
// ...
}
添加分隔线
分隔线用来将界面元素隔开,使单个元素更加容易识别。如下图所示,当列表项左边有图标(如蓝牙图标),由于图标本身就能很好的区分,此时分隔线从图标之后开始显示即可。
List 提供了 divider 属性用于给列表项之间添加分隔线。在设置 divider 属性时,可以通过 strokeWidth 和 color 属性设置分隔线的粗细和颜色。
startMargin 和 endMargin 属性分别用于设置分隔线距离列表侧边起始端的距离和距离列表侧边结束端的距离。
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 EgDivider {
@State egDivider: DividerTmp = new DividerTmp(1, 60, 10, '#ffe9f0f0')
build() {
List() {
// ...
}
.divider(this.egDivider)
}
}
此示例表示从距离列表侧边起始端 60vp 开始到距离结束端 10vp 的位置,画一条粗细为 1vp 的分割线,可以实现图9设置列表分隔线的样式。
说明
- 分隔线的宽度会使 ListItem 之间存在一定间隔,当 List 设置的内容间距小于分隔线宽度时,ListItem 之间的间隔会使用分隔线的宽度。
- 当 List 存在多列时,分割线的 startMargin 和 endMargin 作用于每一列上。
- 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 参数设置列表分组的头部组件。
@Entry
@Component
struct ContactsList {
@Builder itemHead(text: string) {
// 列表分组的头部组件,对应联系人分组A、B等位置的组件
Text(text)
.fontSize(20)
.backgroundColor('#fff1f3f5')
.width('100%')
.padding(5)
}
build() {
List() {
ListItemGroup({ header: this.itemHead('A') }) {
// 循环渲染分组A的ListItem
}
ListItemGroup({ header: this.itemHead('B') }) {
// 循环渲染分组B的ListItem
}
}
}
}
如果多个 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.Bottom }) {
// 将listScroller用于初始化List组件的scroller参数,完成listScroller与列表的绑定。
List({ space: 20, 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 ContactsList {
@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)
}
}
}
说明
计算索引值时,ListItemGroup 作为一个整体占一个索引值,不计算 ListItemGroup 内部 ListItem 的索引值。
响应列表项侧滑
侧滑菜单在许多应用中都很常见。例如,通讯类应用通常会给消息列表提供侧滑删除功能,即用户可以通过向左侧滑列表的某一项,再点击删除按钮删除消息,如下图所示。其中,列表项头像右上角标记设置参考给列表项添加标记。
ListItem 的 swipeAction 属性可用于实现列表项的左右滑动功能。swipeAction 属性方法初始化时有必填参数 SwipeActionOptions,其中,start 参数表示设置列表项右滑时起始端滑出的组件,end 参数表示设置列表项左滑时尾端滑出的组件。
在消息列表中,end 参数表示设置 ListItem 左滑时尾端划出自定义组件,即删除按钮。在初始化 end 方法时,将滑动列表项的索引传入删除按钮组件,当用户点击删除按钮时,可以根据索引值来删除列表项对应的数据,从而实现侧滑删除功能。
- 实现尾端滑出组件的构建。
@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);
})
}
- 绑定 swipeAction 属性到可左滑的 ListItem 上。
// 构建List时,通过ForEach基于数据源this.messages循环渲染ListItem。
ListItem() {
// ...
}
.swipeAction({
end: {
// index为该ListItem在List中的索引值。
builder: () => { 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组件实现消息联系人头像
// ...
}
}
下拉刷新与上拉加载
页面的下拉刷新与上拉加载功能在移动应用中十分常见,例如,新闻页面的内容刷新和加载。这两种操作的原理都是通过响应用户的触摸事件,在顶部或者底部显示一个刷新或加载视图,完成后再将此视图隐藏。
以下拉刷新为例,其实现主要分成三步:
- 监听手指按下事件,记录其初始位置的值。
- 监听手指按压移动事件,记录并计算当前移动的位置与初始值的差值,大于 0 表示向下移动,同时设置一个允许移动的最大值。
- 监听手指抬起事件,若此时移动达到最大值,则触发数据加载并显示刷新视图,加载完成后将此视图隐藏。
下拉刷新与上拉加载的具体实现可参考新闻数据加载。
编辑列表
列表的编辑模式用途十分广泛,常见于待办事项管理、文件管理、备忘录的记录管理等应用场景。在列表的编辑模式下,新增和删除列表项是最基础的功能,其核心是对列表项对应的数据集合进行数据添加和删除。
下面以待办事项管理为例,介绍如何快速实现新增和删除列表项功能。
新增列表项
如下图所示,当用户点击添加按钮时,提供用户新增列表项内容选择或填写的交互界面,用户点击确定后,列表中新增对应的项目。
添加列表项功能实现主要流程如下:
- 定义列表项数据结构,以待办事项管理为例,首先定义待办数据结构。
//ToDo.ets
import { util } from '@kit.ArkTS'
export class ToDo {
key: string = util.generateRandomUUID(true);
name: string;
constructor(name: string) {
this.name = name;
}
}
- 构建列表整体布局和列表项。
//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(() => {
// ...
})
)
)
}
}
- 初始化待办列表数据和可选事项,最后,构建列表布局和列表项。
//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(() => {
this.getUIContext().showTextPickerDialog({
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())
}
}
}
}
}
删除列表项
如下图所示,当用户长按列表项进入删除模式时,提供用户删除列表项选择的交互界面,用户勾选完成后点击删除按钮,列表中删除对应的项目。
删除列表项功能实现主要流程如下:
- 列表的删除功能一般进入编辑模式后才可使用,所以需要提供编辑模式的入口。以待办列表为例,通过监听列表项的长按事件,当用户长按列表项时,进入编辑模式。
// 结构参考
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; //进入编辑模式
}
})
)
)
- 需要响应用户的选择交互,记录要删除的列表项数据。在待办列表中,通过勾选框的勾选或取消勾选,响应用户勾选列表项变化,记录所有选择的列表项。
// 结构参考
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中删除
}
}
})
}
- 需要响应用户点击删除按钮事件,删除列表中对应的选项。
// 结构参考
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。
说明
- cachedCount 的增加会增大 UI 的 CPU、内存开销。使用时需要根据实际情况,综合性能和用户体验进行调整。
- 列表使用数据懒加载时,除了显示区域的列表项和前后缓存的列表项,其他列表项会被销毁。
创建网格 (Grid/GridItem)
网格布局是由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局。网格布局具有较强的页面均分能力,子组件占比控制能力,是一种重要自适应布局,其使用场景有九宫格图片展示、日历、计算器等。
ArkUI提供了 Grid 容器组件和子组件 GridItem,用于构建网格布局。Grid 用于设置网格布局相关参数,GridItem 定义子组件相关特征。Grid 组件支持使用条件渲染、循环渲染、懒加载等方式生成子组件。
布局与约束
Grid 组件为网格容器,其中容器内各条目对应一个 GridItem 组件,如下图所示。
说明
Grid 的子组件必须是 GridItem 组件。
网格布局是一种二维布局。Grid 组件支持自定义行列数和每行每列尺寸占比、设置子组件横跨几行或者几列,同时提供了垂直和水平布局能力。当网格容器组件尺寸发生变化时,所有子组件以及间距会等比例调整,从而实现网格布局的自适应能力。根据 Grid 的这些布局能力,可以构建出不同样式的网格布局,如下图所示。
如果 Grid 组件设置了宽高属性,则其尺寸为设置值。如果没有设置宽高属性,Grid 组件的尺寸默认适应其父组件的尺寸。
Grid 组件根据行列数量与占比属性的设置,可以分为三种布局情况:
- 行、列数量与占比同时设置:Grid 只展示固定行列数的元素,其余元素不展示,且 Grid 不可滚动。(推荐使用该种布局方式)
- 只设置行、列数量与占比中的一个:元素按照设置的方向进行排布,超出的元素可通过滚动的方式展示。
- 行列数量与占比都不设置:元素在布局方向上排布,其行列数由布局方向、单个网格的宽高等多个属性共同决定。超出行列容纳范围的元素不展示,且 Grid 不可滚动。
设置排列方式
设置行列数量与占比
通过设置行列数量与尺寸占比可以确定网格布局的整体排列方式。Grid 组件提供了 rowsTemplate 和 columnsTemplate 属性用于设置网格布局行列数量与尺寸占比。
rowsTemplate 和 columnsTemplate 属性值是一个由多个空格和 '数字+fr' 间隔拼接的字符串,fr 的个数即网格布局的行或列数,fr 前面的数值大小,用于计算该行或列在网格布局宽度上的占比,最终决定该行或列宽度。
如上图所示,构建的是一个三行三列的网格布局,其在垂直方向上分为三等份,每行占一份;在水平方向上分为四等份,第一列占一份,第二列占两份,第三列占一份。
只要将 rowsTemplate 的值为 '1fr 1fr 1fr',同时将 columnsTemplate 的值为'1fr 2fr 1fr',即可实现上述网格布局。
Grid() {
...
}
.rowsTemplate('1fr 1fr 1fr')
.columnsTemplate('1fr 2fr 1fr')
说明
当 Grid 组件设置了 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')
设置主轴方向
使用 Grid 构建网格布局时,若没有设置行列数量与占比,可以通过 layoutDirection 设置网格布局的主轴方向,决定子组件的排列方式。此时可以结合 minCount 和 maxCount 属性来约束主轴方向上的网格数量。
当前 layoutDirection 设置为 Row 时,先从左到右排列,排满一行再排下一行。当前 layoutDirection 设置为 Column 时,先从上到下排列,排满一列再排下一列,如上图所示。此时,将 maxCount 属性设为 3,表示主轴方向上最大显示的网格单元数量为 3。
Grid() {
...
}
.maxCount(3)
.layoutDirection(GridDirection.Row)
说明
- layoutDirection 属性仅在不设置 rowsTemplate 和 columnsTemplate 时生效,此时元素在 layoutDirection 方向上排列。
- 仅设置 rowsTemplate 时,Grid 主轴为水平方向,交叉轴为垂直方向。
- 仅设置 columnsTemplate 时,Grid 主轴为垂直方向,交叉轴为水平方向。
在网格布局中显示数据
网格布局采用二维布局的方式组织其内部元素,如下图所示。
Grid 组件可以通过二维布局的方式显示一组 GridItem 子组件。
Grid() {
GridItem() {
Text('会议')
...
}
GridItem() {
Text('签到')
...
}
GridItem() {
Text('投票')
...
}
GridItem() {
Text('打印')
...
}
}
.rowsTemplate('1fr 1fr')
.columnsTemplate('1fr 1fr')
对于内容结构相似的多个 GridItem,通常更推荐使用 ForEach 语句中嵌套 GridItem 的形式,来减少重复代码。
@Entry
@Component
struct OfficeService {
@State services: Array<string> = ['会议', '投票', '签到', '打印']
build() {
Column() {
Grid() {
ForEach(this.services, (service:string) => {
GridItem() {
Text(service)
}
}, (service:string):string => service)
}
.rowsTemplate(('1fr 1fr') as string)
.columnsTemplate(('1fr 1fr') as string)
}
}
}
设置行列间距
在两个网格单元之间的网格横向间距称为行间距,网格纵向间距称为列间距,如下图所示。
通过 Grid 的 rowsGap 和 columnsGap 可以设置网格布局的行列间距。在上图所示的计算器中,行间距为 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 组件初始化时,可以绑定一个 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 本身是一个容器组件,当设置了多个子组件后,可以对这些子组件进行轮播显示。通常,在一些应用首页显示推荐的内容时,需要用到轮播显示的能力。
针对复杂页面场景,可以使用 Swiper 组件的预加载机制,利用主线程的空闲时间来提前构建和布局绘制组件,优化滑动体验。
布局与约束
Swiper 作为一个容器组件,如果设置了自身尺寸属性,则在轮播显示过程中均以该尺寸生效。如果自身尺寸属性未被设置,则分两种情况:如果设置了 prevMargin 或者 nextMargin 属性,则 Swiper 自身尺寸会跟随其父组件;如果未设置 prevMargin 或者 nextMargin 属性,则会自动根据子组件的大小设置自身的尺寸。
循环播放
通过 loop 属性控制是否循环播放,该属性默认值为 true。
当 loop 为 true 时,在显示第一页或最后一页时,可以继续往前切换到前一页或者往后切换到后一页。如果 loop 为 false,则在第一页或最后一页时,无法继续向前或者向后切换页面。
- loop 为 true
Swiper() {
Text('0')
.width('90%')
.height('100%')
.backgroundColor(Color.Gray)
.textAlign(TextAlign.Center)
.fontSize(30)
Text('1')
.width('90%')
.height('100%')
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.fontSize(30)
Text('2')
.width('90%')
.height('100%')
.backgroundColor(Color.Pink)
.textAlign(TextAlign.Center)
.fontSize(30)
}
.loop(true)
- loop 为 false
Swiper() {
// ...
}
.loop(false)
自动轮播
Swiper 通过设置 autoPlay 属性,控制是否自动轮播子组件。该属性默认值为 false。
autoPlay 为 true 时,会自动切换播放子组件,子组件与子组件之间的播放间隔通过 interval 属性设置。interval 属性默认值为 3000,单位毫秒。
Swiper() {
// ...
}
.loop(true)
.autoPlay(true)
.interval(1000)
导航点样式
Swiper 提供了默认的导航点样式和导航点箭头样式,导航点默认显示在 Swiper 下方居中位置,开发者也可以通过 indicator 属性自定义导航点的位置和样式,导航点箭头默认不显示。
通过 indicator 属性,开发者可以设置导航点相对于 Swiper 组件上下左右四个方位的位置,同时也可以设置每个导航点的尺寸、颜色、蒙层和被选中导航点的颜色。
- 导航点使用默认样式
Swiper() {
Text('0')
.width('90%')
.height('100%')
.backgroundColor(Color.Gray)
.textAlign(TextAlign.Center)
.fontSize(30)
Text('1')
.width('90%')
.height('100%')
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.fontSize(30)
Text('2')
.width('90%')
.height('100%')
.backgroundColor(Color.Pink)
.textAlign(TextAlign.Center)
.fontSize(30)
}
- 自定义导航点样式
导航点直径设为 30vp,左边距为 0,导航点颜色设为红色。
Swiper() {
// ...
}
.indicator(
Indicator.dot()
.left(0)
.itemWidth(15)
.itemHeight(15)
.selectedItemWidth(30)
.selectedItemHeight(15)
.color(Color.Red)
.selectedColor(Color.Blue)
)
Swiper 通过设置 displayArrow 属性,可以控制导航点箭头的大小、位置、颜色,底板的大小及颜色,以及鼠标悬停时是否显示箭头。
- 箭头使用默认样式
Swiper() {
// ...
}
.displayArrow(true, false)
- 自定义箭头样式
箭头显示在组件两侧,大小为 18vp,导航点箭头颜色设为蓝色。
Swiper() {
// ...
}
.displayArrow({
showBackground: true,
isSidebarMiddle: true,
backgroundSize: 24,
backgroundColor: Color.White,
arrowSize: 18,
arrowColor: Color.Blue
}, false)
页面切换方式
Swiper 支持手指滑动、点击导航点和通过控制器三种方式切换页面,以下示例展示通过控制器切换页面的方法。
@Entry
@Component
struct SwiperDemo {
private swiperController: SwiperController = new SwiperController();
build() {
Column({ space: 5 }) {
Swiper(this.swiperController) {
Text('0')
.width(250)
.height(250)
.backgroundColor(Color.Gray)
.textAlign(TextAlign.Center)
.fontSize(30)
Text('1')
.width(250)
.height(250)
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.fontSize(30)
Text('2')
.width(250)
.height(250)
.backgroundColor(Color.Pink)
.textAlign(TextAlign.Center)
.fontSize(30)
}
.indicator(true)
Row({ space: 12 }) {
Button('showNext')
.onClick(() => {
this.swiperController.showNext(); // 通过controller切换到后一页
})
Button('showPrevious')
.onClick(() => {
this.swiperController.showPrevious(); // 通过controller切换到前一页
})
}.margin(5)
}.width('100%')
.margin({ top: 5 })
}
}
轮播方向
Swiper 支持水平和垂直方向上进行轮播,主要通过 vertical 属性控制。
当 vertical 为 true 时,表示在垂直方向上进行轮播;为 false 时,表示在水平方向上进行轮播。vertical 默认值为 false。
- 设置水平方向上轮播。
Swiper() {
// ...
}
.indicator(true)
.vertical(false)
- 设置垂直方向轮播。
Swiper() {
// ...
}
.indicator(true)
.vertical(true)
每页显示多个子页面
Swiper 支持在一个页面内同时显示多个子组件,通过 displayCount 属性设置。
Swiper() {
Text('0')
.width(250)
.height(250)
.backgroundColor(Color.Gray)
.textAlign(TextAlign.Center)
.fontSize(30)
Text('1')
.width(250)
.height(250)
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.fontSize(30)
Text('2')
.width(250)
.height(250)
.backgroundColor(Color.Pink)
.textAlign(TextAlign.Center)
.fontSize(30)
Text('3')
.width(250)
.height(250)
.backgroundColor(Color.Blue)
.textAlign(TextAlign.Center)
.fontSize(30)
}
.indicator(true)
.displayCount(2)
自定义切换动画
Swiper 支持通过 customContentTransition 设置自定义切换动画,可以在回调中对视窗内所有页面逐帧设置透明度、缩放比例、位移、渲染层级等属性实现自定义切换动画。
@Entry
@Component
struct SwiperCustomAnimationExample {
private DISPLAY_COUNT: number = 2
private MIN_SCALE: number = 0.75
@State backgroundColors: Color[] = [Color.Green, Color.Blue, Color.Yellow, Color.Pink, Color.Gray, Color.Orange]
@State opacityList: number[] = []
@State scaleList: number[] = []
@State translateList: number[] = []
@State zIndexList: number[] = []
aboutToAppear(): void {
for (let i = 0; i < this.backgroundColors.length; i++) {
this.opacityList.push(1.0)
this.scaleList.push(1.0)
this.translateList.push(0.0)
this.zIndexList.push(0)
}
}
build() {
Column() {
Swiper() {
ForEach(this.backgroundColors, (backgroundColor: Color, index: number) => {
Text(index.toString()).width('100%').height('100%').fontSize(50).textAlign(TextAlign.Center)
.backgroundColor(backgroundColor)
.opacity(this.opacityList[index])
.scale({ x: this.scaleList[index], y: this.scaleList[index] })
.translate({ x: this.translateList[index] })
.zIndex(this.zIndexList[index])
})
}
.height(300)
.indicator(false)
.displayCount(this.DISPLAY_COUNT, true)
.customContentTransition({
timeout: 1000,
transition: (proxy: SwiperContentTransitionProxy) => {
if (proxy.position <= proxy.index % this.DISPLAY_COUNT || proxy.position >= this.DISPLAY_COUNT + proxy.index % this.DISPLAY_COUNT) {
// 同组页面完全滑出视窗外时,重置属性值
this.opacityList[proxy.index] = 1.0
this.scaleList[proxy.index] = 1.0
this.translateList[proxy.index] = 0.0
this.zIndexList[proxy.index] = 0
} else {
// 同组页面未滑出视窗外时,对同组中左右两个页面,逐帧根据position修改属性值
if (proxy.index % this.DISPLAY_COUNT === 0) {
this.opacityList[proxy.index] = 1 - proxy.position / this.DISPLAY_COUNT
this.scaleList[proxy.index] = this.MIN_SCALE + (1 - this.MIN_SCALE) * (1 - proxy.position / this.DISPLAY_COUNT)
this.translateList[proxy.index] = - proxy.position * proxy.mainAxisLength + (1 - this.scaleList[proxy.index]) * proxy.mainAxisLength / 2.0
} else {
this.opacityList[proxy.index] = 1 - (proxy.position - 1) / this.DISPLAY_COUNT
this.scaleList[proxy.index] = this.MIN_SCALE + (1 - this.MIN_SCALE) * (1 - (proxy.position - 1) / this.DISPLAY_COUNT)
this.translateList[proxy.index] = - (proxy.position - 1) * proxy.mainAxisLength - (1 - this.scaleList[proxy.index]) * proxy.mainAxisLength / 2.0
}
this.zIndexList[proxy.index] = -1
}
}
})
}.width('100%')
}
}
选项卡 (Tabs)
当页面信息较多时,为了让用户能够聚焦于当前显示的内容,需要对页面内容进行分类,提高页面空间利用率。Tabs 组件可以在一个页面内快速实现视图内容的切换,一方面提升查找信息的效率,另一方面精简用户单次获取到的信息量。
基本布局
Tabs 组件的页面组成包含两个部分,分别是 TabContent 和 TabBar。TabContent 是内容页,TabBar 是导航页签栏,页面结构如下图所示,根据不同的导航类型,布局会有区别,可以分为底部导航、顶部导航、侧边导航,其导航栏分别位于底部、顶部和侧边。
说明
- TabContent 组件不支持设置通用宽度属性,其宽度默认撑满 Tabs 父组件。
- TabContent 组件不支持设置通用高度属性,其高度由 Tabs 父组件高度与 TabBar 组件高度决定。
Tabs 使用花括号包裹 TabContent,如下图,其中 TabContent 显示相应的内容页。
每一个 TabContent 对应的内容需要有一个页签,可以通过 TabContent 的 tabBar 属性进行配置。在如下 TabContent 组件上设置 tabBar 属性,可以设置其对应页签中的内容,tabBar 作为内容的页签。
TabContent() {
Text('首页的内容').fontSize(30)
}
.tabBar('首页')
设置多个内容时,需在 Tabs 内按照顺序放置。
Tabs() {
TabContent() {
Text('首页的内容').fontSize(30)
}
.tabBar('首页')
TabContent() {
Text('推荐的内容').fontSize(30)
}
.tabBar('推荐')
TabContent() {
Text('发现的内容').fontSize(30)
}
.tabBar('发现')
TabContent() {
Text('我的内容').fontSize(30)
}
.tabBar("我的")
}
底部导航
底部导航是应用中最常见的一种导航方式。底部导航位于应用一级页面的底部,用户打开应用,能够分清整个应用的功能分类,以及页签对应的内容,并且其位于底部更加方便用户单手操作。底部导航一般作为应用的主导航形式存在,其作用是将用户关心的内容按照功能进行分类,迎合用户使用习惯,方便在不同模块间的内容切换。
导航栏位置使用 Tabs 的 barPosition 参数进行设置。默认情况下,导航栏位于顶部,此时,barPosition 为 BarPosition.Start。设置为底部导航时,需要将 barPosition 设置为 BarPosition.End。
Tabs({ barPosition: BarPosition.End }) {
// TabContent的内容:首页、发现、推荐、我的
...
}
顶部导航
当内容分类较多,用户对不同内容的浏览概率相差不大,需要经常快速切换时,一般采用顶部导航模式进行设计,作为对底部导航内容的进一步划分,常见一些资讯类应用对内容的分类为关注、视频、数码,或者主题应用中对主题进行进一步划分为图片、视频、字体等。
Tabs({ barPosition: BarPosition.Start }) {
// TabContent的内容:关注、视频、游戏、数码、科技、体育、影视
...
}
侧边导航
侧边导航是应用较为少见的一种导航模式,更多适用于横屏界面,用于对应用进行导航操作,由于用户的视觉习惯是从左到右,侧边导航栏默认为左侧侧边栏。
实现侧边导航栏需要将 Tabs 的 vertical 属性设置为 true,vertical 默认值为 false,表明内容页和导航栏垂直方向排列。
Tabs({ barPosition: BarPosition.Start }) {
// TabContent的内容:首页、发现、推荐、我的
...
}
.vertical(true)
.barWidth(100)
.barHeight(200)
说明
- vertical 为 false时,tabbar 的宽度默认为撑满屏幕的宽度,需要设置 barWidth 为合适值。
- vertical 为 true 时,tabbar 的高度默认为实际内容的高度,需要设置 barHeight 为合适值。
限制导航栏的滑动切换
默认情况下,导航栏都支持滑动切换,在一些内容信息量需要进行多级分类的页面,如支持底部导航 + 顶部导航组合的情况下,底部导航栏的滑动效果与顶部导航出现冲突,此时需要限制底部导航的滑动,避免引起不好的用户体验。
控制滑动切换的属性为 scrollable,默认值为 true,表示可以滑动,若要限制滑动切换页签则需要设置为 false。
Tabs({ barPosition: BarPosition.End }) {
TabContent(){
Column(){
Tabs(){
// 顶部导航栏内容
...
}
}
.backgroundColor('#ff08a8f1')
.width('100%')
}
.tabBar('首页')
// 其他TabContent内容:发现、推荐、我的
...
}
.scrollable(false)
固定导航栏
当内容分类较为固定且不具有拓展性时,例如底部导航内容分类一般固定,分类数量一般在 3-5 个,此时使用固定导航栏。固定导航栏不可滚动,无法被拖拽滚动,内容均分 tabBar 的宽度。
Tabs 的 barMode 属性用于控制导航栏是否可以滚动,默认值为 BarMode.Fixed。
Tabs({ barPosition: BarPosition.End }) {
// TabContent的内容:首页、发现、推荐、我的
...
}
.barMode(BarMode.Fixed)
滚动导航栏
滚动导航栏可以用于顶部导航栏或者侧边导航栏的设置,内容分类较多,屏幕宽度无法容纳所有分类页签的情况下,需要使用可滚动的导航栏,支持用户点击和滑动来加载隐藏的页签内容。
滚动导航栏需要设置 Tabs 组件的 barMode 属性,默认值为 BarMode.Fixed 表示为固定导航栏,BarMode.Scrollable 表示可滚动导航栏。
Tabs({ barPosition: BarPosition.Start }) {
// TabContent的内容:关注、视频、游戏、数码、科技、体育、影视、人文、艺术、自然、军事
...
}
.barMode(BarMode.Scrollable)
自定义导航栏
对于底部导航栏,一般作为应用主页面功能区分,为了更好的用户体验,会组合文字以及对应语义图标表示页签内容,这种情况下,需要自定义导航页签的样式。
系统默认情况下采用了下划线标志当前活跃的页签,而自定义导航栏需要自行实现相应的样式,用于区分当前活跃页签和未活跃页签。
设置自定义导航栏需要使用 tabBar 的参数,以其支持的 CustomBuilder 的方式传入自定义的函数组件样式。例如这里声明 tabBuilder 的自定义函数组件,传入参数包括页签文字 title,对应位置 index,以及选中状态和未选中状态的图片资源。通过当前活跃的 currentIndex 和页签对应的 targetIndex 匹配与否,决定 UI 显示的样式。
@Builder tabBuilder(title: string, targetIndex: number, selectedImg: Resource, normalImg: Resource) {
Column() {
Image(this.currentIndex === targetIndex ? selectedImg : normalImg)
.size({ width: 25, height: 25 })
Text(title)
.fontColor(this.currentIndex === targetIndex ? '#1698CE' : '#6B6B6B')
}
.width('100%')
.height(50)
.justifyContent(FlexAlign.Center)
}
在 TabContent 对应 tabBar 属性中传入自定义函数组件,并传递相应的参数。
TabContent() {
Column(){
Text('我的内容')
}
.width('100%')
.height('100%')
.backgroundColor('#007DFF')
}
.tabBar(this.tabBuilder('我的', 0, $r('app.media.mine_selected'), $r('app.media.mine_normal')))
切换至指定页签
在不使用自定义导航栏时,默认的 Tabs 会实现切换逻辑。在使用了自定义导航栏后,默认的 Tabs 仅实现滑动内容页和点击页签时内容页的切换逻辑,页签切换逻辑需要自行实现。即用户滑动内容页和点击页签时,页签栏需要同步切换至内容页对应的页签。
内容页和页签不联动:
此时需要使用 Tabs 提供的 onChange 事件方法,监听索引 index 的变化,并将当前活跃的 index 值传递给 currentIndex,实现页签的切换。
@Entry
@Component
struct TabsExample1 {
@State currentIndex: number = 2
@Builder tabBuilder(title: string, targetIndex: number) {
Column() {
Text(title)
.fontColor(this.currentIndex === targetIndex ? '#1698CE' : '#6B6B6B')
}
}
build() {
Column() {
Tabs({ barPosition: BarPosition.End }) {
TabContent() {
...
}.tabBar(this.tabBuilder('首页', 0))
TabContent() {
...
}.tabBar(this.tabBuilder('发现', 1))
TabContent() {
...
}.tabBar(this.tabBuilder('推荐', 2))
TabContent() {
...
}.tabBar(this.tabBuilder('我的', 3))
}
.animationDuration(0)
.backgroundColor('#F1F3F5')
.onChange((index: number) => {
this.currentIndex = index
})
}.width('100%')
}
}
内容页和页签联动:
若希望不滑动内容页和点击页签也能实现内容页和页签的切换,可以将 currentIndex 传给 Tabs 的 index 参数,通过改变 currentIndex 来实现跳转至指定索引值对应的 TabContent 内容。也可以使用 TabsController,TabsController 是 Tabs 组件的控制器,用于控制 Tabs 组件进行内容页切换。通过 TabsController 的changeIndex 方法来实现跳转至指定索引值对应的 TabContent 内容。
@State currentIndex: number = 2
private controller: TabsController = new TabsController()
Tabs({ barPosition: BarPosition.End, index: this.currentIndex, controller: this.controller }) {
...
}
.height(600)
.onChange((index: number) => {
this.currentIndex = index
})
Button('动态修改index').width('50%').margin({ top: 20 })
.onClick(()=>{
this.currentIndex = (this.currentIndex + 1) % 4
})
Button('changeIndex').width('50%').margin({ top: 20 })
.onClick(()=>{
let index = (this.currentIndex + 1) % 4
this.controller.changeIndex(index)
})
切换指定页签:
开发者可以通过 Tabs 组件的 onContentWillChange 接口,设置自定义拦截回调函数。拦截回调函数在下一个页面即将展示时被调用,如果回调返回 true,新页面可以展示;如果回调返回 false,新页面不会展示,仍显示原来页面。
Tabs({ barPosition: BarPosition.End, controller: this.controller, index: this.currentIndex }) {...}
.onContentWillChange((currentIndex, comingIndex) => {
if (comingIndex == 2) {
return false
}
return true
})
支持开发者自定义页面切换拦截事件:
沉浸式效果
典型应用全屏窗口 UI 元素包括状态栏、应用界面和底部导航条,其中状态栏和导航条,通常在沉浸式布局下称为避让区;避让区之外的区域称为安全区。开发应用沉浸式效果主要指通过调整状态栏、应用界面和导航条的显示效果来减少状态栏导航条等系统界面的突兀感,从而使用户获得最佳的UI体验。
界面元素示意图:
开发应用沉浸式效果主要要考虑如下几个设计要素:
- UI 元素避让处理:导航条底部区域可以响应点击事件,除此之外的可交互 UI 元素和应用关键信息不建议放到导航条区域。状态栏显示系统信息,如果与界面元素有冲突,需要考虑避让状态栏。
- 沉浸式效果处理:将状态栏和导航条颜色与界面元素颜色相匹配,不出现明显的突兀感。
针对上面的设计要求,可以通过如下两种方式实现应用沉浸式效果:
- 窗口全屏布局方案:调整布局系统为全屏布局,界面元素延伸到状态栏和导航条区域实现沉浸式效果。当不隐藏避让区时,可通过接口查询状态栏和导航条区域进行可交互元素避让处理,并设置状态栏或导航条的颜色等属性与界面元素匹配。当隐藏避让区时,通过对应接口设置全屏布局即可。
- 组件安全区方案:布局系统保持安全区内布局,然后通过接口延伸绘制内容(如背景色,背景图)到状态栏和导航条区域实现沉浸式效果。该方案下,界面元素仅做绘制延伸,无法单独布局到状态栏和导航条区域,针对需要单独布局 UI 元素到状态栏和导航条区域的场景建议使用窗口全屏布局方案处理。
窗口全屏布局方案
窗口全屏布局方案主要涉及以下应用扩展布局,全屏显示,不隐藏避让区和应用扩展布局,隐藏避让区两个应用场景。
应用扩展布局,全屏显示,不隐藏避让区
可以通过调用窗口强制全屏布局接口 setWindowLayoutFullScreen() 实现界面元素延伸到状态栏和导航条;然后通过接口 getWindowAvoidArea() 和 on('avoidAreaChange') 获取并动态监听避让区域的变更信息,页面布局根据避让区域信息进行动态调整;设置状态栏或导航条的颜色等属性与界面元素进行匹配。
调用
setWindowLayoutFullScreen()
接口设置窗口全屏。// EntryAbility.ets import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'; import { window } from '@kit.ArkUI'; import { BusinessError } from '@kit.BasicServicesKit'; export default class EntryAbility extends UIAbility { // ... onWindowStageCreate(windowStage: window.WindowStage): void { windowStage.loadContent('pages/Index', (err, data) => { if (err.code) { return; } let windowClass: window.Window = windowStage.getMainWindowSync(); // 获取应用主窗口 // 1. 设置窗口全屏 let isLayoutFullScreen = true; windowClass.setWindowLayoutFullScreen(isLayoutFullScreen).then(() => { console.info('Succeeded in setting the window layout to full-screen mode.'); }).catch((err: BusinessError) => { console.error('Failed to set the window layout to full-screen mode. Cause:' + JSON.stringify(err)); }); // 进行后续步骤2-3中的操作 }); } }
使用
getWindowAvoidArea()
接口获取当前布局遮挡区域(例如状态栏、导航条)。// EntryAbility.ets // 2. 获取布局避让遮挡的区域 let type = window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR; // 以导航条避让为例 let avoidArea = windowClass.getWindowAvoidArea(type); let bottomRectHeight = avoidArea.bottomRect.height; // 获取到导航条区域的高度 AppStorage.setOrCreate('bottomRectHeight', bottomRectHeight); type = window.AvoidAreaType.TYPE_SYSTEM; // 以状态栏避让为例 avoidArea = windowClass.getWindowAvoidArea(type); let topRectHeight = avoidArea.topRect.height; // 获取状态栏区域高度 AppStorage.setOrCreate('topRectHeight', topRectHeight);
注册监听函数,动态获取避让区域的实时数据。
// EntryAbility.ets // 3. 注册监听函数,动态获取避让区域数据 windowClass.on('avoidAreaChange', (data) => { if (data.type === window.AvoidAreaType.TYPE_SYSTEM) { let topRectHeight = data.area.topRect.height; AppStorage.setOrCreate('topRectHeight', topRectHeight); } else if (data.type == window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR) { let bottomRectHeight = data.area.bottomRect.height; AppStorage.setOrCreate('bottomRectHeight', bottomRectHeight); } });
布局中的 UI 元素需要避让状态栏和导航条,否则可能产生 UI 元素重叠等情况。如下例子中,对控件顶部设置 padding(具体数值与状态栏高度一致),实现对状态栏的避让;对底部设置 padding(具体数值与底部导航条区域高度一致),实现对底部导航条的避让。如果去掉顶部和底部的 padding 设置,即不避让状态栏和导航条,UI 元素就会发生重叠。具体可见下面的对比图。
// Index.ets @Entry @Component struct Index { @StorageProp('bottomRectHeight') bottomRectHeight: number = 0; @StorageProp('topRectHeight') topRectHeight: number = 0; build() { Row() { Column() { Row() { Text('DEMO-ROW1').fontSize(40) }.backgroundColor(Color.Orange).padding(20) Row() { Text('DEMO-ROW2').fontSize(40) }.backgroundColor(Color.Orange).padding(20) Row() { Text('DEMO-ROW3').fontSize(40) }.backgroundColor(Color.Orange).padding(20) Row() { Text('DEMO-ROW4').fontSize(40) }.backgroundColor(Color.Orange).padding(20) Row() { Text('DEMO-ROW5').fontSize(40) }.backgroundColor(Color.Orange).padding(20) Row() { Text('DEMO-ROW6').fontSize(40) }.backgroundColor(Color.Orange).padding(20) } .width('100%') .height('100%') .alignItems(HorizontalAlign.Center) .justifyContent(FlexAlign.SpaceBetween) .backgroundColor('#008000') // top数值与状态栏区域高度保持一致;bottom数值与导航条区域高度保持一致 .padding({ top: px2vp(this.topRectHeight), bottom: px2vp(this.bottomRectHeight) }) } } }
根据实际的 UI 界面显示或相关 UI 元素背景颜色等,还可以按需设置状态栏的文字颜色、背景色或设置导航条的显示或隐藏,以使 UI 界面效果呈现和谐。状态栏默认是透明的,透传的是应用界面的背景色。
此例中 UI 颜色主要有两种,比较简单,故未对状态栏文字颜色、背景色进行设置。
布局避让状态栏和导航条:
布局未避让状态栏和导航条,UI 元素重叠:
应用扩展布局,隐藏避让区
此场景下导航条会自动隐藏,适用于游戏、电影等应用场景。可以通过从底部上滑唤出导航条。
调用
setWindowLayoutFullScreen()
接口设置窗口全屏。// EntryAbility.ets import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'; import { window } from '@kit.ArkUI'; import { BusinessError } from '@kit.BasicServicesKit'; export default class EntryAbility extends UIAbility { // ... onWindowStageCreate(windowStage: window.WindowStage): void { windowStage.loadContent('pages/Index', (err, data) => { if (err.code) { return; } let windowClass: window.Window = windowStage.getMainWindowSync(); // 获取应用主窗口 // 1. 设置窗口全屏 let isLayoutFullScreen = true; windowClass.setWindowLayoutFullScreen(isLayoutFullScreen).then(() => { console.info('Succeeded in setting the window layout to full-screen mode.'); }).catch((err: BusinessError) => { console.error(`Failed to set the window layout to full-screen mode. Code is ${err.code}, message is ${err.message}`); }); // 进行后续步骤2中的状态栏和导航条的隐藏操作 }); } }
调用
setSpecificSystemBarEnabled()
接口设置状态栏和导航条的具体显示/隐藏状态,此场景下将其设置为隐藏。// EntryAbility.ets // 2. 设置状态栏隐藏 windowClass.setSpecificSystemBarEnabled('status', false).then(() => { console.info('Succeeded in setting the status bar to be invisible.'); }).catch((err: BusinessError) => { console.error(`Failed to set the status bar to be invisible. Code is ${err.code}, message is ${err.message}`); }); // 2. 设置导航条隐藏 windowClass.setSpecificSystemBarEnabled('navigationIndicator', false).then(() => { console.info('Succeeded in setting the navigation indicator to be invisible.'); }).catch((err: BusinessError) => { console.error(`Failed to set the navigation indicator to be invisible. Code is ${err.code}, message is ${err.message}`); });
在界面中无需进行导航条避让操作。
// Index.ets @Entry() @Component struct Index { build() { Row() { Column() { Row() { Text('ROW1').fontSize(40) }.backgroundColor(Color.Orange).padding(20) Row() { Text('ROW2').fontSize(40) }.backgroundColor(Color.Orange).padding(20) Row() { Text('ROW3').fontSize(40) }.backgroundColor(Color.Orange).padding(20) Row() { Text('ROW4').fontSize(40) }.backgroundColor(Color.Orange).padding(20) Row() { Text('ROW5').fontSize(40) }.backgroundColor(Color.Orange).padding(20) Row() { Text('ROW6').fontSize(40) }.backgroundColor(Color.Orange).padding(20) } .width('100%') .height('100%') .alignItems(HorizontalAlign.Center) .justifyContent(FlexAlign.SpaceBetween) .backgroundColor('#008000') } } }
组件安全区方案
应用未使用 setWindowLayoutFullScreen()
接口设置窗口全屏布局时,默认使能组件安全区布局。
应用在默认情况下窗口背景绘制范围是全屏,但 UI 元素被限制在安全区内(自动排除状态栏和导航条)进行布局,来避免界面元素被状态栏和导航条遮盖。
界面元素自动避让状态栏和导航条示意图:
针对状态栏和导航条颜色与界面元素颜色不匹配问题,可以通过如下两种方式实现沉浸式效果:
状态栏和导航条颜色相同场景,可以通过设置窗口的背景色来实现沉浸式效果。窗口背景色可通过 setWindowBackgroundColor() 进行设置。
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'; import { window } from '@kit.ArkUI'; export default class EntryAbility extends UIAbility { // ... onWindowStageCreate(windowStage: window.WindowStage): void { windowStage.loadContent('pages/Index', (err, data) => { if (err.code) { return; } // 设置全窗颜色和应用元素颜色一致 windowStage.getMainWindowSync().setWindowBackgroundColor('#008000'); }); } }
界面状态栏和导航条颜色相同场景。
// xxx.ets @Entry @Component struct Example { build() { Column() { Row() { Text('ROW1').fontSize(40) }.backgroundColor(Color.Orange).padding(20) Row() { Text('ROW2').fontSize(40) }.backgroundColor(Color.Orange).padding(20) Row() { Text('ROW3').fontSize(40) }.backgroundColor(Color.Orange).padding(20) Row() { Text('ROW4').fontSize(40) }.backgroundColor(Color.Orange).padding(20) Row() { Text('ROW5').fontSize(40) }.backgroundColor(Color.Orange).padding(20) Row() { Text('ROW6').fontSize(40) }.backgroundColor(Color.Orange).padding(20) } .width('100%') .height('100%') .alignItems(HorizontalAlign.Center) .justifyContent(FlexAlign.SpaceBetween) .backgroundColor('#008000') } }
状态栏和导航条颜色不同时,可以使用 expandSafeArea 属性扩展安全区域属性进行调整。
// xxx.ets @Entry @Component struct Example { build() { Column() { Row() { Text('Top Row').fontSize(40).textAlign(TextAlign.Center).width('100%') } .backgroundColor('#F08080') // 设置顶部绘制延伸到状态栏 .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP]) Row() { Text('ROW2').fontSize(40) }.backgroundColor(Color.Orange).padding(20) Row() { Text('ROW3').fontSize(40) }.backgroundColor(Color.Orange).padding(20) Row() { Text('ROW4').fontSize(40) }.backgroundColor(Color.Orange).padding(20) Row() { Text('ROW5').fontSize(40) }.backgroundColor(Color.Orange).padding(20) Row() { Text('Bottom Row').fontSize(40).textAlign(TextAlign.Center).width('100%') } .backgroundColor(Color.Orange) // 设置底部绘制延伸到导航条 .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]) } .width('100%').height('100%').alignItems(HorizontalAlign.Center) .backgroundColor('#008000') .justifyContent(FlexAlign.SpaceBetween) } }
扩展安全区域属性原理
- 布局阶段按照安全区范围大小进行 UI 元素布局。
- 布局完成后查看设置了 expandSafeArea 的组件边界(不包括 margin)是否和安全区边界相交。
- 如果设置了 expandSafeArea 的组件和安全区边界相交,根据 expandSafeArea 传递的属性则进一步扩大组件绘制区域大小覆盖状态栏、导航条这些非安全区域。
- 上述过程仅改变组件自身绘制大小,不进行二次布局,不影响子节点和兄弟节点的大小和位置。
- 子节点可以单独设置该属性,只需要自身边界和安全区域重合就可以延伸自身大小至非安全区域内,需要确保父组件未设置 clip 等裁切属性。
- 配置 expandSafeArea 属性组件进行绘制扩展时,需要关注组件不能配置固定宽高尺寸,百分比除外。
背景图和视频场景
设置背景图、视频控件大小为安全区域大小并配置 expandSafeArea 属性。
// xxx.ets
@Entry
@Component
struct SafeAreaExample1 {
build() {
Stack() {
Image($r('app.media.bg'))
.height('100%').width('100%')
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) // 图片组件的绘制区域扩展至状态栏和导航条。
}.height('100%').width('100%')
}
}
滚动类场景
要求需要 List 滚动类组件滚动过程中元素可以和导航条重合,滚动至底部时,元素在导航条上面需要避让。
由于 expandSafeArea 不改变子节点布局,因此,List 等滚动类组件可以调用 expandSafeArea,延伸 List 组件视图窗口大小而不改变 ListItem 内在布局。实现 ListItem 在滑动过程中显示在导航条下,但滚动至最后一个时显示在导航条上,示意图如下:
未适配时列表下方被导航条遮盖:
List 配置 expandSafeArea 属性后的效果:
仅扩展底部导航条。
配置窗口整体底色。
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'; import { window } from '@kit.ArkUI'; export default class EntryAbility extends UIAbility { // ... onWindowStageCreate(windowStage: window.WindowStage): void { windowStage.loadContent('pages/Index', (err, data) => { if (err.code) { return; } windowStage.getMainWindowSync().setWindowBackgroundColor('#DCDCDC'); // 配置窗口整体底色 }); } }
界面代码展示。
// xxx.ets @Entry @Component struct ListExample { private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] build() { Column() { List({ space: 20, initialIndex: 0 }) { ForEach(this.arr, (item: number) => { ListItem() { Text('' + item) .width('100%') .height(100) .fontSize(16) .textAlign(TextAlign.Center) .borderRadius(10) .backgroundColor(0xFFFFFF) } }, (item:number) => item.toString()) } .listDirection(Axis.Vertical) // 排列方向 .scrollBar(BarState.Off) .friction(0.6) .divider({ strokeWidth: 2, color: 0xFFFFFF, startMargin: 20, endMargin: 20 }) // 每行之间的分界线 .edgeEffect(EdgeEffect.Spring) // 边缘效果设置为Spring .width('90%') // List组件的视窗范围扩展至导航条。 .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) } .width('100%') .height('100%') .padding({ top: 15 }) } }
底部页签场景
要求页签背景色能够延伸到导航条区域,但页签内部可操作元素需要在导航条之上。
针对底部的页签部分,Navigation 组件和 Tabs 组件默认实现了页签的延伸处理,开发者只需要保证 Navigation 和 Tabs 组件的底部边界和底部导航条重合即可。若开发者显式调用 expandSafeArea 接口,则安全区效果由 expandSafeArea 参数指定。
如果未使用上述组件而是采用自定义方式实现页签的场景,可以针对底部元素设置 expandSafeArea 属性实现底部元素的背景扩展。
顶部和底部 UI 元素未设置和设置 expandSafeArea 属性效果对比:
// xxx.ets
@Entry
@Component
struct VideoCreateComponent {
build() {
Column() {
Row() {
Text('Top Row').fontSize(40).textAlign(TextAlign.Center).width('100%')
}
.backgroundColor('#F08080')
// 设置顶部绘制延伸到状态栏
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
Row() {
Text('ROW2').fontSize(40)
}.backgroundColor(Color.Orange).padding(20)
Row() {
Text('ROW3').fontSize(40)
}.backgroundColor(Color.Orange).padding(20)
Row() {
Text('ROW4').fontSize(40)
}.backgroundColor(Color.Orange).padding(20)
Row() {
Text('ROW5').fontSize(40)
}.backgroundColor(Color.Orange).padding(20)
Row() {
Text('Bottom Row').fontSize(40).textAlign(TextAlign.Center).width('100%')
}
.backgroundColor(Color.Orange)
// 设置底部绘制延伸到导航条
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
}
.width('100%').height('100%').alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.SpaceBetween)
.backgroundColor(Color.Green)
}
}
图文场景
当状态栏元素和底部导航条元素不同时,无法单纯通过窗口背景色或者背景图组件延伸实现,此时需要对顶部元素和底部元素分别配置 expandSafeArea 属性,顶部元素配置 expandSafeArea([SafeAreaType.SYSTEM],[SafeAreaEdge.TOP])
,底部元素配置 expandSafeArea([SafeAreaType.SYSTEM],[SafeAreaEdge.BOTTOM])
@Entry
@Component
struct Index {
build() {
Swiper() {
Column() {
Image($r('app.media.start'))
.height('50%').width('100%')
// 设置图片延伸到状态栏
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
Column() {
Text('HarmonyOS 第一课')
.fontSize(32)
.margin(30)
Text('通过循序渐进的学习路径,无经验和有经验的开发者都可以掌握ArkTS语言声明式开发范式,体验更简洁、更友好的HarmonyOS应用开发旅程。')
.fontSize(20).margin(20)
}.height('50%').width('100%')
.backgroundColor(Color.White)
// 设置文本内容区背景延伸到导航栏
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
}
}
.width('100%')
.height('100%')
// 关闭Swiper组件默认的裁切效果以便子节点可以绘制在Swiper外。
.clip(false)
}
}