线性布局 (Row/Column)
概述:
线性容器Row和Column构建。线性布局是其他布局的基础,其子元素在线性方向上(水平方向和垂直方向)依次排列。线性布局的排列方向由所选容器组件决定,Column容器内子元素按照垂直方向排列,Row容器内子元素按照水平方向排列。根据不同的排列方向,开发者可选择使用Row或Column容器创建线性布局。
1.Column容器排列示图
2. Row容器排列示图
基本概念
1.布局容器:具有布局能力的容器组件,可以承载其他元素作为其子元素,布局容器会对其子元素进行尺寸计算和布局排列.
2.布局子元素:布局容器内部的元素.
3.主轴:线性布局容器在布局方向上的轴线,子元素默认沿主轴排列。Row容器主轴为水平方向,Column容器主轴为垂直方向.
4.交叉轴:垂直于主轴方向的轴线。Row容器交叉轴为垂直方向,Column容器交叉轴为水平方向.
5.间距:布局子元素的间距.
布局子元素在排列方向上的间距
可以通过space属性设置排列方向上子元素的间距,使各子元素在排列方向上有等间距效果。
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({ 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属性。
.justifyContent(FlexAlign.Start)//主流对齐方式.alignItems(HorizontalAlign.Start)//侧轴对其
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() {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容器内子元素在垂直方向上的排列示例图
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容器内子元素在水平方向上的排列示例图
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)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 ScrollPage {@State message: string = 'Hello World';@State nums:number[]=[1,2,3,4,5,6,7,8,9,10]build() {Scroll() {Column({space:10}){ForEach(this.nums,(n:number,i)=>{Row(){Text(n.toString())}.height(100).width('90%').backgroundColor('gray').justifyContent(FlexAlign.Center).borderRadius(10)})}.width('100%')}.height('100%').width('100%').scrollable(ScrollDirection.Vertical).scrollBar(BarState.Auto)//开关滚动条.scrollBarColor('red')//滚动条的颜色.scrollBarWidth(5)}
}
层叠布局 (Stack)
概述:
层叠布局(StackLayout)用于在屏幕上预留一块区域来显示组件中的元素,提供元素可以重叠的布局。层叠布局通过Stack容器组件实现位置的固定定位与层叠,容器中的子元素依次入栈,后一个子元素覆盖前一个子元素,子元素可以叠加,也可以设置位置.
层叠布局具有较强的页面层叠、位置定位能力,其使用场景有广告、卡片层叠效果等。
Stack作为容器,容器内的子元素的顺序为Item1->Item2->Item3。
示例代码:
Stack(){Text('第一个子元素').width('80%').height(260).backgroundColor('#faf')Text('第2个子元素').width('60%').height(230).backgroundColor('#fcf')Text('第3个子元素').width('40%').height(200).backgroundColor('#fef')}.height(300).width('90%').backgroundColor('gray')
示例图:
开发布局
Stack组件为容器组件,容器内可包含各种子元素。其中子元素默认进行居中堆叠。子元素被约束在Stack下,进行自己的样式定义以及排列。
对齐方式
Stack组件通过alignContent参数实现位置的相对移动。如图2所示,支持九种对齐方式。
Stack容器内元素的对齐方式示例图
示例代码:
Stack({alignContent:Alignment.End}){Text('第一个子元素').width('80%').height(260).backgroundColor('#faf').zIndex(101)Text('第2个子元素').width('60%').height(230).backgroundColor('#fcf').zIndex(102)Text('第3个子元素').width('40%').height(200).backgroundColor('#fef').zIndex(3)}.height(300).width('90%').backgroundColor('gray')
用stack写页面示例代码/示例图
@Entry
@Component
struct StackTestPage {@State message: string ='广告';arr:number[]=[1,2,3,4,5,6,7,8,9]@State isflag:boolean=truebuild() {Stack({alignContent:Alignment.End}){Column(){List({space:10}){ForEach(this.arr,(n:number)=>{ListItem(){Text(`商品编号${n}`).height(200).width('90%').backgroundColor('gray').textAlign(TextAlign.Center)}})}.alignListItem(ListItemAlign.Center)}.height('100%').width('100%')if (this.isflag){Column(){SymbolGlyph($r('sys.symbol.xmark')).fontSize(30).onClick(()=>{this.isflag=false})Text('这是广告').fontSize(30)}.width(50).height(200).backgroundColor('#f0f')}}.height('100%').width('100%')}
}
弹性布局 (Flex)
概述
弹性布局(Flex)提供更加有效的方式对容器中的子元素进行排列、对齐和分配剩余空间。常用于页面头部导航栏的均匀分布、页面框架的搭建、多行数据的排列等。
容器默认存在主轴与交叉轴,子元素默认沿主轴排列,子元素在主轴方向的尺寸称为主轴尺寸,在交叉轴方向的尺寸称为交叉轴尺寸。
主轴为水平方向的Flex容器示例图
基本概念
主轴:Flex组件布局方向的轴线,子元素默认沿着主轴排列。主轴开始的位置称为主轴起始点,结束位置称为主轴结束点。
交叉轴:垂直于主轴方向的轴线。交叉轴开始的位置称为交叉轴起始点,结束位置称为交叉轴结束点。
布局方向
在弹性布局中,容器的子元素可以按照任意方向排列。通过设置参数direction,可以决定主轴的方向,从而控制子元素的排列方向。
主轴对齐方式
通过justifyContent参数设置子元素在主轴方向的对齐方式。
代码示例:
@Entry
@Component
struct FlexPage {@State message: string = 'Hello World';build() {Scroll(){Column(){Flex({direction:FlexDirection.Row,justifyContent:FlexAlign.Center,alignItems:ItemAlign.Stretch//侧轴对齐}){//从左到右Text('子元素1').width(100).border({color:'red',width:1,style:BorderStyle.Solid}).direction(Direction.Rtl)Text('子元素2').height(50).width(210).border({color:'red',width:1,style:BorderStyle.Solid})Text('子元素3').height(50).flexBasis(100).border({color:'red',width:1,style:BorderStyle.Solid})}.height(100).width('100%').backgroundColor('gray')Flex({direction:FlexDirection.RowReverse,justifyContent:FlexAlign.End}){//从右到左排列Text('子元素1').height(50).width(50).alignSelf(ItemAlign.Start).border({color:'red',width:1,style:BorderStyle.Solid})Text('子元素2').height(50).width(50).alignSelf(ItemAlign.End).border({color:'red',width:1,style:BorderStyle.Solid})Text('子元素3').height(50).width(50).alignSelf(ItemAlign.Center).border({color:'red',width:1,style:BorderStyle.Solid})Text('子元素4').height(50).width(50).alignSelf(ItemAlign.Baseline).border({color:'red',width:1,style:BorderStyle.Solid})Text('子元素5').height(50).width(50).alignSelf(ItemAlign.Stretch).border({color:'red',width:1,style:BorderStyle.Solid})}.height(100).width('100%').backgroundColor('#0ff')Flex({direction:FlexDirection.Column,justifyContent:FlexAlign.SpaceBetween}){//从左到右Text('子元素1').height(50).width(50).border({color:'red',width:1,style:BorderStyle.Solid})Text('子元素2').height(50).width(50).border({color:'red',width:1,style:BorderStyle.Solid})Text('子元素3').height(50).width(50).border({color:'red',width:1,style:BorderStyle.Solid})}.height(200).width('100%').backgroundColor('#cff')Flex({direction:FlexDirection.ColumnReverse,justifyContent:FlexAlign.SpaceAround}){//从左到右Text('子元素1').height(50).width(50).border({color:'red',width:1,style:BorderStyle.Solid})Text('子元素2').height(50).width(50).border({color:'red',width:1,style:BorderStyle.Solid})Text('子元素3').height(50).width(50).border({color:'red',width:1,style:BorderStyle.Solid})}.height(200).width('100%').backgroundColor('#aff')Text("换行").fontSize(30).fontColor('red')Flex({direction:FlexDirection.Row,wrap:FlexWrap.Wrap,justifyContent:FlexAlign.SpaceBetween,alignItems:ItemAlign.Center,alignContent:FlexAlign.SpaceAround}){//从左到右Text('子元素1').height(100).width(100).border({color:'red',width:1,style:BorderStyle.Solid})Text('子元素2').height(100).width(100).border({color:'red',width:1,style:BorderStyle.Solid})Text('子元素3').height(100).width(100).border({color:'red',width:1,style:BorderStyle.Solid})Text('子元素4').height(100).width(100).border({color:'red',width:1,style:BorderStyle.Solid})Text('子元素5').height(100).width(100).border({color:'red',width:1,style:BorderStyle.Solid})Text('子元素6').height(100).width(100).border({color:'red',width:1,style:BorderStyle.Solid})}.height(210).width('100%').backgroundColor('gray')Flex({direction:FlexDirection.Column,wrap:FlexWrap.Wrap,justifyContent:FlexAlign.SpaceEvenly}){//从左到右Text('子元素1').height(100).width('45%').border({color:'red',width:1,style:BorderStyle.Solid})Text('子元素2').height(110).width('45%').border({color:'red',width:1,style:BorderStyle.Solid})Text('子元素3').height(90).width('45%').border({color:'red',width:1,style:BorderStyle.Solid})Text('子元素4').height(130).width('45%').border({color:'red',width:1,style:BorderStyle.Solid})Text('子元素5').height(100).width('45%').border({color:'red',width:1,style:BorderStyle.Solid})}.height(330).width('100%').backgroundColor('#0cf')}}.height('100%').width('100%')}
}
相对布局 (RelativeContainer)
概述:
RelativeContainer为采用相对布局的容器,支持容器内部的子元素设置相对位置关系,适用于界面复杂场景的情况,对多个子组件进行对齐和排列。子元素支持指定兄弟元素作为锚点,也支持指定父容器作为锚点,基于锚点做相对位置布局。下图是一个RelativeContainer的概念图,图中的虚线表示位置的依赖关系。
相对布局示例图
基本概念
锚点:通过锚点设置当前元素基于哪个元素确定位置。
对齐方式:通过对齐方式,设置当前元素是基于锚点的上中下对齐,还是基于锚点的左中右对齐。
设置依赖关系
水平方向上,可以设置left、middle、right的锚点。在竖直方向上,可以设置top、center、bottom的锚点。
为了明确定义锚点,必须为RelativeContainer及其子id,用于指定锚id默认为“__container__”,其余子元素的ID通过id属性设置。不设置id的组件能显示,但是不能被其他子组件作为锚点,相对布局容器会为其拼接id,此id的规律无法被应用感知。互相依赖,环形依赖时容器内子组件全部不绘制。同方向上两个以上位置设置锚点,但锚点位置逆序时此子组件大小为0,即不绘制。
设置相对于锚点的对齐位置
设置了锚点之后,可以通过align设置相对于锚点的对齐位置。
在水平方向上,对齐位置可以设置为HorizontalAlign.Start、HorizontalAlign.Center、HorizontalAlign.End。
在竖直方向上,对齐位置可以设置为VerticalAlign.Top、VerticalAlign.Center、VerticalAlign.Bottom。
子组件位置偏移
子组件经过相对位置对齐后,位置可能还不是目标位置,开发者可根据需要进行额外偏移设置offset。
示例代码:
@Entry
@Component
struct RelativePage {@State message: string = 'Hello World';build() {
Scroll(){Column(){RelativeContainer(){Row(){}.height(100).width(100).backgroundColor('blue').alignRules({//anchor锚点'bottom':{'anchor':'__container__',align:VerticalAlign.Bottom},'right':{'anchor':'__container__',align:HorizontalAlign.End}})Row(){}.height(100).width(100).backgroundColor('red').alignRules({//anchor锚点'top':{'anchor':'__container__',align:VerticalAlign.Top},'left':{'anchor':'__container__',align:HorizontalAlign.Start}})Row(){}.height(100).width(100).backgroundColor('#FFAEC9').alignRules({//anchor锚点'center':{'anchor':'__container__',align:VerticalAlign.Center},'middle':{'anchor':'__container__',align:HorizontalAlign.Center}})}.width('100%') .height(300) .backgroundColor('gray')// .id('__container__')//系统默认生成RelativeContainer(){Row().height(100).width(100).backgroundColor('red').id('r1').alignRules({'middle':{'anchor':'__container__',align:HorizontalAlign.Center}})Row().height(100).width(100).backgroundColor('blue').id('r2').alignRules({'left':{'anchor':'r1',align:HorizontalAlign.End}})Row(){}.height(100).width(100).backgroundColor('blue').alignRules({'top':{'anchor':'r1',align:VerticalAlign.Bottom},left:{'anchor':'r1',align:HorizontalAlign.Start}})Row().height(100).width(100).backgroundColor('green').alignRules({'top':{'anchor':'r2',align:VerticalAlign.Bottom},'left':{'anchor':'r2',align:HorizontalAlign.Start}})}.width('100%').height(300).backgroundColor('#faf')RelativeContainer(){Row().height(100).width(100).backgroundColor('red').id('r2').alignRules({'middle':{'anchor':'__container__',align:HorizontalAlign.Center}})Row().height(100).width(100).backgroundColor('blue').id('r3').alignRules({'left':{'anchor':'r1',align:HorizontalAlign.End}})Row(){}.height(100).width(100).backgroundColor('blue').id('r3').alignRules({'top':{'anchor':'r1',align:VerticalAlign.Bottom},left:{'anchor':'r1',align:HorizontalAlign.Start}})Row().height(100).width(100).backgroundColor('green').alignRules({'top':{'anchor':'r2',align:VerticalAlign.Bottom},'left':{'anchor':'r2',align:HorizontalAlign.Start}})}.width('100%').height(300).backgroundColor('#faf')RelativeContainer(){Row().height(100).width(100).backgroundColor('red').id('r1').alignRules({'middle':{'anchor':'__container__',align:HorizontalAlign.Center}})Row().height(100).width(100).backgroundColor('blue').id('r2').alignRules({'left':{'anchor':'r1',align:HorizontalAlign.End}})Row(){}.height(100).width(100).backgroundColor('blue').alignRules({'top':{'anchor':'r1',align:VerticalAlign.Bottom},left:{'anchor':'r1',align:HorizontalAlign.Start}})Row().height(100).width(100).backgroundColor('green').alignRules({'top':{'anchor':'r2',align:VerticalAlign.Bottom},'left':{'anchor':'r2',align:HorizontalAlign.Start}})// .offset({// x:-40,// y:-40// })Row().height(100).width(100).backgroundColor('green').alignRules({'top':{'anchor':'r2',align:VerticalAlign.Bottom},'left':{'anchor':'r2',align:HorizontalAlign.Start}})}.width('100%').height(300).backgroundColor('#faf')}}.height('100%').width('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最多支持六个断点,单调递增数组长度最大为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设置断点切换参考物。 考虑到应用可能以非全屏窗口的形式显示,以应用窗口宽度为参照物更为通用。
示例代码:
let gr:Record<string,number>={'xs':12,'sm':6,'md':4,'lg':2}@Entry
@Component
struct GridRowPage {@State message: string = 'Hello World';@State color:Color[]=[Color.Black,Color.Blue,Color.Brown,Color.Gray,Color.Green,Color.Grey,Color.Orange,Color.Pink,Color.Red,Color.Yellow,Color.Transparent]build() {Scroll(){Column(){GridRow() {GridCol({span:{xs:12,sm:6,md:3,lg:1}}){Text('1')}.backgroundColor('red')GridCol({span:{xs:12,sm:6,md:3,lg:1}}){Text('2')} .backgroundColor('blue')GridCol(){Text('3')} .backgroundColor('green')GridCol(){Text('4')}.backgroundColor('gray')}.height('30%').width('100%')GridRow(){ForEach(this.color,(c:Color,index:number)=>{GridCol({span:{xs:12,sm:6,md:4,lg:2}}){Text(`${index}`).fontColor(Color.White)}.backgroundColor(c)})}.width('100%').border({style:BorderStyle.Solid,width:1,color:'red'})GridRow({columns:8,direction:GridRowDirection.RowReverse,gutter:{x:10,y:20}}){ForEach(this.color,(c:Color,index:number)=>{GridCol({span:{xs:8,sm:4,md:2,lg:1}}){Text(`${index}`).fontColor(Color.White)}.backgroundColor(c)})}.width('100%').border({style:BorderStyle.Solid,width:1,color:'red'})GridRow(){ForEach(this.color,(c:Color,index:number)=>{GridCol({offset:{'xs':2,'sm':2,'md':2,'lg':2}}){Text(`${index}`).fontColor(Color.White)}.backgroundColor(c).span(gr).order(parseInt(`${Math.random()*10}`))})}.width('100%').border({style:BorderStyle.Solid,width:1,color:'red'})}}.height('100%').width('100%')}
}
媒体查询 (@ohos.mediaquery)
概述
媒体查询作为响应式设计的核心,在移动设备上应用十分广泛。媒体查询可根据不同设备类型或同设备不同状态修改应用的样式。媒体查询常用于下面两种场景:
-
针对设备和应用的属性信息(比如显示区域、深浅色、分辨率),设计出相匹配的布局。
-
当屏幕发生动态改变时(比如分屏、横竖屏切换),同步更新应用的页面布局。
引入与使用流程
媒体查询通过mediaquery模块接口,设置查询条件并绑定回调函数,任一媒体特征改变时,均会触发回调函数,返回匹配结果,根据返回值更改页面布局或者实现业务逻辑,实现页面的响应式设计.
首先导入媒体查询模块。import { mediaquery } from '@kit.ArkUI';
通过matchMediaSync接口设置媒体查询条件,保存返回的条件监听句柄listener。例如监听横屏事件:let listener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync('(orientation: landscape)');
给条件监听句柄listener绑定回调函数onPortrait,当listener检测设备状态变化时执行回调函数。在回调函数内,根据不同设备状态更改页面布局或者实现业务逻辑。
媒体查询条件
媒体查询条件由媒体类型、逻辑操作符、媒体特征组成,其中媒体类型可省略,逻辑操作符用于连接不同媒体类型与媒体特征,其中,媒体特征要使用“()”包裹且可以有多个。
语法规则
语法规则包括媒体类型(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 | 按屏幕相关参数进行媒体查询。 |
示例代码:
@Entry
@Component
struct Media1page {@State message: string = 'Hello World';@State c:Color=Color.Black//产生一个监听句柄listener:mediaquery.MediaQueryListener = mediaquery.matchMediaSync('screen and(orientation: landscape)');//回调函数:当屏幕发生变化时,触发的函数onPortrait(mr:mediaquery.MediaQueryResult){if(mr.matches){//如果满足条件this.c=Color.Red}else {//不满足this.c=Color.Blue}}aboutToAppear(): void {this.listener.on('change',(mr:mediaquery.MediaQueryResult)=>{this.onPortrait(mr)})}build() {RelativeContainer() {Text(this.message).id('Media1pageHelloWorld').fontSize(50).fontWeight(FontWeight.Bold).alignRules({center: { anchor: '__container__', align: VerticalAlign.Center },middle: { anchor: '__container__', align: HorizontalAlign.Center }})}.height('100%').width('100%')}
}
import { mediaquery } from '@kit.ArkUI'@Entry
@Component
struct Media2Page {@State str:string='普通'@State img:ResourceStr=$r('app.media.99')l1:mediaquery.MediaQueryListener=this.getUIContext().getMediaQuery().matchMediaSync('(2224px>=width>1080px)')onp1(mr:mediaquery.MediaQueryResult) {if (mr.matches) {this.str = '屏幕折叠屏'this.img = $r('app.media.88')}}l2:mediaquery.MediaQueryListener=this.getUIContext().getMediaQuery().matchMediaSync('(width<=1080px)')onp2(mr:mediaquery.MediaQueryResult) {if (mr.matches) {this.str = '普通'this.img = $r('app.media.25')}}l3:mediaquery.MediaQueryListener=this.getUIContext().getMediaQuery().matchMediaSync('(width>2224px)')onp3(mr:mediaquery.MediaQueryResult) {if (mr.matches) {this.str = '平板'this.img = $r('app.media.26')}}aboutToAppear(): void {this.l1.on('change',(mr)=>{this.onp1(mr)})this.l2.on('change',(mr)=>{this.onp2(mr)})this.l3.on('change',(mr)=>{this.onp3(mr)})}build() {Column() {Text().fontSize(50).fontWeight(FontWeight.Bold)Image(this.img)}.height('100%').width('100%')}
}
import { mediaquery } from '@kit.ArkUI';@Entry
@Component
struct Media3Page {@State message: string = 'Hello World';@State isSm:boolean=true//小屏@State nrWidth:string='100%'l1:mediaquery.MediaQueryListener=this.getUIContext().getMediaQuery().matchMediaSync('(width>1088px)')aboutToAppear(): void {this.l1.on('change',(mr)=>{if (mr.matches) {//>1000this.isSm=falsethis.nrWidth='80%'}else {this.isSm=truethis.nrWidth='100%'}})}build() {Column() {if (this.isSm){Row(){Text('横向的导航')}.width('100%').backgroundColor('#ccc').height(60)}Row(){if (!this.isSm){Column(){Text('左侧导航')}.width('20%').backgroundColor('#ddd').height('100%')}Column(){Text('具体内容')}.backgroundColor('#eee').height('100%').width(this.nrWidth)}.width('100%')}.height('100%').width('100%')}
}
创建列表 (List)
概述
列表是一种复杂的容器,当列表项达到一定数量,内容超过屏幕大小时,可以自动提供滚动功能。它适合用于呈现同类数据类型或数据类型集,例如图片和文本。在列表中显示数据集合是许多应用程序中的常见要求(如通讯录、音乐列表、购物清单等)。
使用列表可以轻松高效地显示结构化、可滚动的信息。通过在List组件中按垂直或者水平方向线性排列子组件ListItemGroup或ListItem,为列表中的行或列提供单个视图,或使用循环渲染迭代一组行或列,或混合任意数量的单个视图和ForEach结构,构建一个列表。List组件支持使用条件渲染、循环渲染、懒加载等渲染控制方式生成子组件。
布局与约束
列表作为一种容器,会自动按其滚动方向排列子组件,向列表中添加组件或从列表中移除组件会重新排列子组件。
如下图所示,在垂直列表中,List按垂直方向自动排列ListItemGroup或ListItem。
ListItemGroup用于列表数据的分组展示,其子组件也是ListItem。ListItem表示单个列表项,可以包含单个子组件。
list、ListItemGroup和ListItem组件关系
布局
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的listDirection属性设置为Axis.Horizontal即可实现。listDirection默认为Axis.Vertical,即主轴默认是垂直方向。
设置交叉轴布局
List组件的交叉轴布局可以通过lanes和alignListItem属性进行设置,lanes属性用于确定交叉轴排列的列表项数量,alignListItem用于设置子组件在交叉轴方向的对齐方式。
List组件的lanes属性通常用于在不同尺寸的设备自适应构建不同行数或列数的列表,即一次开发、多端部署的场景,例如歌单列表。lanes属性的取值类型是"number | LengthConstrain",即整数或者LengthConstrain类型。以垂直列表为例,如果将lanes属性设为2,表示构建的是一个两列的垂直列表,如图2中右图所示。lanes的默认值为1,即默认情况下,垂直列表的列数是1。
当其取值为LengthConstrain类型时,表示会根据LengthConstrain与List组件的尺寸自适应决定行或列数。
在列表中显示数据
列表视图垂直或水平显示项目集合,在行或列超出屏幕时提供滚动功能,使其适合显示大型数据集合。在最简单的列表形式中,List静态地创建其列表项ListItem的内容。
迭代列表内容
通常,应用通过数据集合动态地创建列表。使用循环渲染可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件,降低代码复杂度。
ArkTS通过ForEach提供了组件的循环渲染能力。以简单形式的联系人列表为例,将联系人名称和头像数据以Contact类结构存储到contacts数组,使用ForEach中嵌套ListItem的形式来代替多个平铺的、内容相似的ListItem,从而减少重复代码。
自定义列表样式
设置内容间距
在初始化列表时,如需在列表项之间添加间距,可以使用space参数。例如,在每个列表项之间沿主轴方向添加10vp的间距:
示例代码:
class Lxr{touimg:ResourceStrname:stringconstructor(touimg: ResourceStr, name: string) {this.touimg = touimg;this.name = name;}
}
class DividerTem{color:ResourceColor|stringstrokeWidth:numberstartMargin:numberendMargin:numberconstructor(color: ResourceColor | string, strokeWidth: number, startMargin: number, endMargin: number) {this.color = color;this.strokeWidth = strokeWidth;this.startMargin = startMargin;this.endMargin = endMargin;}}@Entry
@Component
struct ListPage {@State message: string = 'Hello World';@State nums:number[]=[1,2,3,4,5,6,7,8,9]@State zm:string[]=['a','b','c','d','e','f','g']//最小值应是一个元素的大小,最大值应是所有元素占得位置,主要用最小分行数// autoList:LengthConstrain={maxLength:300,minLength:100}@State lxr:Lxr[]=[new Lxr($r('app.media.a2'),'小明'),new Lxr($r('app.media.a2'),'小红'),new Lxr($r('app.media.a2'),'小白'),new Lxr($r('app.media.a2'),'小黑'),new Lxr($r('app.media.a2'),'小黄'),new Lxr($r('app.media.a2'),'小绿'),new Lxr($r('app.media.a2'),'小紫'),new Lxr($r('app.media.a2'),'小蓝'),new Lxr($r('app.media.a2'),'小青'),new Lxr($r('app.media.a2'),'小明'),new Lxr($r('app.media.a2'),'小黑'),new Lxr($r('app.media.a2'),'小黄'),new Lxr($r('app.media.a2'),'小绿'),new Lxr($r('app.media.a2'),'小紫'),new Lxr($r('app.media.a2'),'小蓝'),new Lxr($r('app.media.a2'),'小青'),new Lxr($r('app.media.a2'),'小明'),]@State lxrA:Lxr[]=[new Lxr($r('app.media.startIcon'),'阿明'),new Lxr($r('app.media.startIcon'),'阿红'),new Lxr($r('app.media.startIcon'),'阿白'),new Lxr($r('app.media.startIcon'),'a黑'),new Lxr($r('app.media.startIcon'),'amy'),new Lxr($r('app.media.startIcon'),'爱雅'),new Lxr($r('app.media.startIcon'),'阿一'),]@State lxrB:Lxr[]=[new Lxr($r('app.media.startIcon'),'白青'),new Lxr($r('app.media.startIcon'),'白黄'),new Lxr($r('app.media.startIcon'),'布尔'),new Lxr($r('app.media.startIcon'),'布鲁'),new Lxr($r('app.media.startIcon'),'不坏'),new Lxr($r('app.media.startIcon'),'不绿'),new Lxr($r('app.media.startIcon'),'不紫'),]@State lxrC:Lxr[]=[new Lxr($r('app.media.startIcon'),'聪聪'),new Lxr($r('app.media.startIcon'),'匆匆'),new Lxr($r('app.media.startIcon'),'丛丛'),new Lxr($r('app.media.startIcon'),'擦擦'),new Lxr($r('app.media.startIcon'),'长春'),new Lxr($r('app.media.startIcon'),'超超'),new Lxr($r('app.media.startIcon'),'陈陈'),new Lxr($r('app.media.startIcon'),'纯纯'),new Lxr($r('app.media.startIcon'),'聪聪'),new Lxr($r('app.media.startIcon'),'匆匆'),new Lxr($r('app.media.startIcon'),'丛丛'),new Lxr($r('app.media.startIcon'),'擦擦'),new Lxr($r('app.media.startIcon'),'长春'),new Lxr($r('app.media.startIcon'),'超超'),new Lxr($r('app.media.startIcon'),'陈陈'),new Lxr($r('app.media.startIcon'),'纯纯'),new Lxr($r('app.media.startIcon'),'聪聪'),new Lxr($r('app.media.startIcon'),'匆匆'),new Lxr($r('app.media.startIcon'),'丛丛'),new Lxr($r('app.media.startIcon'),'擦擦'),new Lxr($r('app.media.startIcon'),'长春'),new Lxr($r('app.media.startIcon'),'超超'),new Lxr($r('app.media.startIcon'),'陈陈'),new Lxr($r('app.media.startIcon'),'纯纯'),]build() {Column() {// this.test1()// this.test2()// this.test3()// this.test4()// this.txl5()// this.txl6()this.txl7()}.height('100%').width('100%')}//主轴方向@Builder test1(){List({space:5}){ForEach(this.nums,(n:number,index)=>{ListItem(){Text(n.toString()).width(150).height(150).backgroundColor('#abc')}})}//主轴方向,默认垂直.listDirection(Axis.Horizontal)}//交叉轴@Builder test2(){List({space:5}){ForEach(this.nums,(n:number,index)=>{ListItem(){Text(n.toString()).width(150).height(150).backgroundColor('#abc')}})}//主轴方向,默认垂直.listDirection(Axis.Vertical)//列数.lanes(2)//对齐方向// .alignListItem(ListItemAlign.End)}//自适应行数@Builder test3(){List({space:5}){ForEach(this.nums,(n:number,index)=>{ListItem(){Text(n.toString()).width(150).height(150).backgroundColor('#abc')}})}//主轴方向,默认垂直.listDirection(Axis.Horizontal)//列数// .lanes(this.autoList)//对齐方向.alignListItem(ListItemAlign.Center)//竖着设宽度,横着设高度// .height(400)}private dTem:DividerTem=new DividerTem('blue',2,10,10)//右滑的组件@Builder shanchu(index:number){Button({type:ButtonType.Circle}){SymbolGlyph($r('sys.symbol.trash')).fontSize(50)}.onClick(()=>{this.lxr.splice(index,1)})}@Builder tj(index:number){Button({type:ButtonType.Circle}){SymbolGlyph($r('sys.symbol.rays')).fontSize(50)}}//通讯录@Builder test4(){List({space:10,scroller:this.sc}){ForEach(this.lxr,(l:Lxr,index)=>{ListItem(){Row({space:10}){Image(l.touimg).width(50).height(50).borderRadius(100)Text(l.name).fontSize(30)}.width('100%').backgroundColor(index/2==0?'#ccc':'#aaa')}.swipeAction({start:this.tj(index),end:this.shanchu(index)})})}// .divider({// color:'red',strokeWidth:2,startMargin:10,endMargin:50// })//分割线样式.divider(this.dTem)//滚动条.scrollBar(BarState.Auto)Button('^').onClick(()=>{this.sc.scrollToIndex(6)})}//通讯录2.0 分组private dTem1:DividerTem=new DividerTem('#ccc',2,50,10)@Builder group(str:string){Text(str).fontSize(30).width('100%').backgroundColor('gray').height(50)}@Builder txl5(){List(){ListItemGroup({space:20,header:this.group('A')}){ForEach(this.lxrA,(l:Lxr,i)=>{ListItem(){Row({space:10}){Image(l.touimg).width(50).height(50).borderRadius(100)Text(l.name).fontSize(30)}.width('100%')}})}.divider(this.dTem1)ListItemGroup({space:20,header:this.group('B')}){ForEach(this.lxrB,(l:Lxr,i)=>{ListItem(){Row({space:10}){Image(l.touimg).width(50).height(50).borderRadius(100)Text(l.name).fontSize(30)}.width('100%')}})}.divider(this.dTem1)ListItemGroup({space:20,header:this.group('C')}){ForEach(this.lxrC,(l:Lxr,i)=>{ListItem(){Row({space:10}){Image(l.touimg).width(50).height(50).borderRadius(100)Text(l.name).fontSize(30)}.width('100%')}})}.divider(this.dTem1)}}@State lxrA1:Lxr[]=[new Lxr($r('app.media.startIcon'),'阿明'),new Lxr($r('app.media.startIcon'),'阿红'),new Lxr($r('app.media.startIcon'),'阿白'),new Lxr($r('app.media.startIcon'),'a黑'),new Lxr($r('app.media.startIcon'),'amy'),new Lxr($r('app.media.startIcon'),'爱雅'),new Lxr($r('app.media.startIcon'),'阿一'),]@State lxrB2:Lxr[]=[new Lxr($r('app.media.startIcon'),'白青'),new Lxr($r('app.media.startIcon'),'白黄'),new Lxr($r('app.media.startIcon'),'布尔'),new Lxr($r('app.media.startIcon'),'布鲁'),new Lxr($r('app.media.startIcon'),'不坏'),new Lxr($r('app.media.startIcon'),'不绿'),new Lxr($r('app.media.startIcon'),'不紫'),]@State lxrC3:Lxr[]=[new Lxr($r('app.media.startIcon'),'聪聪'),new Lxr($r('app.media.startIcon'),'匆匆'),new Lxr($r('app.media.startIcon'),'丛丛'),new Lxr($r('app.media.startIcon'),'擦擦'),new Lxr($r('app.media.startIcon'),'长春'),new Lxr($r('app.media.startIcon'),'超超'),new Lxr($r('app.media.startIcon'),'陈陈'),new Lxr($r('app.media.startIcon'),'纯纯'),]@State txls:Txl[]=[new Txl('A',[new Lxr($r('app.media.startIcon'),'阿明'),new Lxr($r('app.media.startIcon'),'阿红'),new Lxr($r('app.media.startIcon'),'阿白'),new Lxr($r('app.media.startIcon'),'a黑'),new Lxr($r('app.media.startIcon'),'amy'),new Lxr($r('app.media.startIcon'),'爱雅'),new Lxr($r('app.media.startIcon'),'阿一'),]),new Txl('B',this.lxrB),new Txl('C',this.lxrC),]@Builder txl6(){List(){ForEach(this.txls,(txl:Txl,index)=>{ListItemGroup({header:this.group(txl.group)}){ForEach(txl.lxr,(lxr:Lxr,i)=>{ListItem(){Row({space:10}){Image(lxr.touimg).width(50).height(50).borderRadius(100)Text(lxr.name).fontSize(30)}.width('100%')}})}.divider(this.dTem1)})}//吸顶,粘性标题.sticky(StickyStyle.Header)}@State xh:string[]=['A','B','C']sc:Scroller=new Scroller()@State selected:number=0@Builder txl7(){Stack({alignContent:Alignment.End}){List({scroller:this.sc}){ForEach(this.txls,(txl:Txl,index)=>{ListItemGroup({header:this.group(txl.group)}){ForEach(txl.lxr,(lxr:Lxr,i)=>{ListItem(){Row({space:10}){Image(lxr.touimg).width(50).height(50).borderRadius(100)Text(lxr.name).fontSize(30)}.width('100%')}})}.divider(this.dTem1)})}//吸顶,粘性标题.sticky(StickyStyle.Header)// Button('^')// .onClick(()=>{// this.sc.scrollToIndex(1)// }).onScrollIndex((firstIndex:number)=>{this.selected=firstIndex})AlphabetIndexer({arrayValue:this.xh,selected:0}).selected(this.selected).selectedFont({size:40}).font({size:30}).itemSize(60)}}}
class Txl{group:stringlxr:Lxr[]constructor(group: string, lxr: Lxr[]) {this.group = group;this.lxr = lxr;}}
创建网格 (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组件中,可以通过创建Grid时传入合适的GridLayoutOptions实现如图所示的单个网格横跨多行或多列的场景,其中,irregularIndexes和onGetIrregularSizeByIndex可对仅设置rowsTemplate或columnsTemplate的Grid使用;onGetRectByIndex可对同时设置rowsTemplate和columnsTemplate的Grid使用。
设置行列间距
在两个网格单元之间的网格横向间距称为行间距,网格纵向间距称为列间距,如下图所示。
构建可滚动的网格布局
可滚动的网格布局常用在文件管理、购物或视频列表等页面中,如下图所示。在设置Grid的行列数量与占比时,如果仅设置行、列数量与占比中的一个,即仅设置rowsTemplate或仅设置columnsTemplate属性,网格单元按照设置的方向排列,超出Grid显示区域后,Grid拥有可滚动能力。
性能优化
与长列表的处理类似,循环渲染适用于数据量较小的布局场景,当构建具有大量网格项的可滚动网格布局时,推荐使用数据懒加载方式实现按需迭代加载数据,从而提升列表性能。
关于按需加载优化的具体实现可参考数据懒加载章节中的示例。
当使用懒加载方式渲染网格时,为了更好的滚动体验,减少滑动时出现白块,Grid组件中也可通过cachedCount属性设置GridItem的预加载数量,只在懒加载LazyForEach中生效。
设置预加载数量后,会在Grid显示区域前后各缓存cachedCount*列数个GridItem,超出显示和缓存范围的GridItem会被释放。
示例代码:
@Entry
@Component
struct GridPage {@State message: string = 'Hello World';build() {Column({space:10}) {// this.test1()// this.test2()// this.test2()// this.test3()// this.test4()// this.test5()// this.test6()}.height('100%').width('100%')}@Builder test1(){Grid(){ForEach([1,2,3,4,5,6,7,8,9],(n:number)=>{GridItem(){Text(n.toString())}.backgroundColor('blue')})}.width('100%').height(300).rowsTemplate('1fr 1fr 1fr')//行的占比.columnsTemplate('1fr 2fr 1fr')//列的占比.rowsGap(5) //行间距.columnsGap(5)}@Builder test6(){Grid(){ForEach([1,2,3,4,5,6,7,8,9],(n:number)=>{GridItem(){Text(n.toString())}.backgroundColor('blue')})}.width('100%').height(300).rowsTemplate('1fr 1fr 1fr')//行的占比.columnsTemplate('1fr 1fr 2fr')//列的占比.rowsGap(5) //行间距.columnsGap(5)//列间距}ly:GridLayoutOptions={regularSize:[1,1],//单元格所占大小onGetRectByIndex:(index)=>{if (index==2){return[0,2,1,2]}else if (index==3){return[1,0,2,1]}else if (index==7){return[1,1,1,3]}return[0,0,1,0]}}@Builder test2(){Grid(undefined,this.ly){ForEach([0,1,2,3,4,5,6,7],(n:number)=>{GridItem(){Text(n.toString())}.backgroundColor('blue')})}.width('100%').height(300).rowsTemplate('1fr 1fr 1fr')//行的占比.columnsTemplate('1fr 1fr 1fr 1fr')//列的占比.rowsGap(5) //行间距.columnsGap(5)//列间距}ly1:GridLayoutOptions={regularSize:[1,1],//单元格所占大小onGetRectByIndex:(index)=>{if (index==0){return[0,0,1,6]}else if (index==16) {return [4, 3, 2, 1]}else if (index==17) {return [5, 0, 1, 2]}return[0,0,1,0]}}@Builder test3(){Grid(undefined,this.ly1){ForEach([0,'CE','c',3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18],(n:number)=>{GridItem(){Text(n.toString())}.backgroundColor('gray')})}.width('100%').height(300).rowsTemplate('2fr 1fr 1fr 1fr 1fr 1fr')//行的占比.columnsTemplate('1fr 1fr 1fr 1fr')//列的占比.rowsGap(5) //行间距.columnsGap(5)//列间距}@Builder test4(){Grid(){ForEach([1,2,3,4,5,6,7,8,9],(n:number)=>{GridItem(){Text(n.toString())}.backgroundColor('#DE7D7A').height(200).width(200)})}// .rowsTemplate('1fr 1fr 1fr')//行的占比.columnsTemplate('1fr 1fr 1fr ')//列的占比.width('100%').height(300).rowsGap(5).columnsGap(5).maxCount(3)// .layoutDirection(GridDirection.Row)}private S1:Scroller=new Scroller()@State dates:number[]=[25,26,27,28,29,30,1,2,3,4,5,6,7,8,9,10,11, 12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,1,2,3,4,5,30,31, 1,2,3,4,5,6,7,8,9,10,11, 12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,1,2,3,4,5,6,7,8,9]@Builder test5(){Grid(this.S1){ForEach(this.dates,(day:number)=>{GridItem(){Text(day.toString())}.backgroundColor('#ccc').height(50)})}.columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr ')//列的占比.width('100%').height(330).rowsGap(5).columnsGap(5).maxCount(3)Row(){Button("上一页").onClick(()=>{this.S1.scrollPage({next:true})})Button("下一页").onClick(()=>{this.S1.scrollPage({next:false})})}}
}
创建轮播 (Swiper)
布局与约束
Swiper作为一个容器组件,如果设置了自身尺寸属性,则在轮播显示过程中均以该尺寸生效。如果自身尺寸属性未被设置,则分两种情况:如果设置了prevMargin或者nextMargin属性,则Swiper自身尺寸会跟随其父组件;如果未设置prevMargin或者nextMargin属性,则会自动根据子组件的大小设置自身的尺寸。
循环播放
通过loop属性控制是否循环播放,该属性默认值为true。
当loop为true时,在显示第一页或最后一页时,可以继续往前切换到前一页或者往后切换到后一页。如果loop为false,则在第一页或最后一页时,无法继续向前或者向后切换页面。
自动轮播
Swiper通过设置autoPlay属性,控制是否自动轮播子组件。该属性默认值为false。
autoPlay为true时,会自动切换播放子组件,子组件与子组件之间的播放间隔通过interval属性设置。interval属性默认值为3000,单位毫秒。
导航点样式
Swiper提供了默认的导航点样式和导航点箭头样式,导航点默认显示在Swiper下方居中位置,开发者也可以通过indicator属性自定义导航点的位置和样式,导航点箭头默认不显示。
通过indicator属性,开发者可以设置导航点相对于Swiper组件上下左右四个方位的位置,同时也可以设置每个导航点的尺寸、颜色、蒙层和被选中导航点的颜色。
轮播方向
Swiper支持水平和垂直方向上进行轮播,主要通过vertical属性控制。
当vertical为true时,表示在垂直方向上进行轮播;为false时,表示在水平方向上进行轮播。vertical默认值为false。
示例代码:
@Entry
@Component
struct SwiperPage {@State message: string = 'Hello World';sc:SwiperController=new SwiperController()build() {Column() {Swiper(this.sc){//轮播Text('1').height(300).width('50%').backgroundColor('red')Text('2').height(300).width('50%').backgroundColor('blue')Text('3').height(300).width('50%').backgroundColor('green')Text('4').height(300).width('50%').backgroundColor('gray')Text('5').height(300).width('50%').backgroundColor('#faf')}// .autoPlay(true).interval(1000).indicator(Indicator.dot().left(10).itemWidth(10).selectedItemWidth(20).itemHeight(10).selectedItemHeight(20).color('black').selectedColor('white'))// .displayArrow(true,false).displayArrow({showBackground:true,isSidebarMiddle:true,backgroundSize:70,backgroundColor:'#abcdef',arrowSize:30,arrowColor:Color.White},false).displayCount(2)Row(){Button('上一页').onClick(()=>{this.sc.showPrevious()})Button('下一页').onClick(()=>{this.sc.showNext()})}}.height('100%').width('100%')}
}
选项卡 (Tabs)
基本布局
Tabs组件的页面组成包含两个部分,分别是TabContent和TabBar。TabContent是内容页,TabBar是导航页签栏,页面结构如下图所示,根据不同的导航类型,布局会有区别,可以分为底部导航、顶部导航、侧边导航,其导航栏分别位于底部、顶部和侧边。
Tabs组件布局示意图
Tabs使用花括号包裹TabContent,如图2,其中TabContent显示相应的内容页。
每一个TabContent对应的内容需要有一个页签,可以通过TabContent的tabBar属性进行配置。在如下TabContent组件上设置tabBar属性,可以设置其对应页签中的内容,tabBar作为内容的页签。
底部导航
底部导航是应用中最常见的一种导航方式。底部导航位于应用一级页面的底部,用户打开应用,能够分清整个应用的功能分类,以及页签对应的内容,并且其位于底部更加方便用户单手操作。底部导航一般作为应用的主导航形式存在,其作用是将用户关心的内容按照功能进行分类,迎合用户使用习惯,方便在不同模块间的内容切换。
导航栏位置使用Tabs的barPosition参数进行设置。默认情况下,导航栏位于顶部,此时,barPosition为BarPosition.Start。设置为底部导航时,需要将barPosition设置为BarPosition.End。
顶部导航
当内容分类较多,用户对不同内容的浏览概率相差不大,需要经常快速切换时,一般采用顶部导航模式进行设计,作为对底部导航内容的进一步划分,常见一些资讯类应用对内容的分类为关注、视频、数码,或者主题应用中对主题进行进一步划分为图片、视频、字体等。
侧边导航
侧边导航是应用较为少见的一种导航模式,更多适用于横屏界面,用于对应用进行导航操作,由于用户的视觉习惯是从左到右,侧边导航栏默认为左侧侧边栏。
限制导航栏的滑动切换
默认情况下,导航栏都支持滑动切换,在一些内容信息量需要进行多级分类的页面,如支持底部导航+顶部导航组合的情况下,底部导航栏的滑动效果与顶部导航出现冲突,此时需要限制底部导航的滑动,避免引起不好的用户体验。
固定导航栏
当内容分类较为固定且不具有拓展性时,例如底部导航内容分类一般固定,分类数量一般在3-5个,此时使用固定导航栏。固定导航栏不可滚动,无法被拖拽滚动,内容均分tabBar的宽度。
滚动导航栏
滚动导航栏可以用于顶部导航栏或者侧边导航栏的设置,内容分类较多,屏幕宽度无法容纳所有分类页签的情况下,需要使用可滚动的导航栏,支持用户点击和滑动来加载隐藏的页签内容。
自定义导航栏
对于底部导航栏,一般作为应用主页面功能区分,为了更好的用户体验,会组合文字以及对应语义图标表示页签内容,这种情况下,需要自定义导航页签的样式。
切换至指定页签
在不使用自定义导航栏时,默认的Tabs会实现切换逻辑。在使用了自定义导航栏后,默认的Tabs仅实现滑动内容页和点击页签时内容页的切换逻辑,页签切换逻辑需要自行实现。即用户滑动内容页和点击页签时,页签栏需要同步切换至内容页对应的页签。
示例代码:
@Entry
@Component
struct TabsPage {@State message: string = 'Hello World';build() {Column(){// this.test1()this.test2()// this.test3()
}.height('100%').width('100%')}@Builder test1(){Tabs({barPosition:BarPosition.End}) {TabContent(){//每一个选项卡里的内容Row(){Text('首页文字')}.height('100%').width('100%').backgroundColor('red')}.tabBar('首页')TabContent() { //每一个选项卡里的内容Row() {Text('推荐的文字')}.height('100%').width('100%').backgroundColor('blue')}.tabBar('推荐')TabContent() { //每一个选项卡里的内容Row() {Text('发现的文字')}.height('100%').width('100%').backgroundColor('#EBA0B9')}.tabBar('发现')TabContent() { //每一个选项卡里的内容Row() {Text('我的文字')}.height('100%').width('100%').backgroundColor('#F0E300')}.tabBar('我的')}.height('100%').width('100%')}@Builder test2(){Tabs({barPosition:BarPosition.End}) {TabContent(){//每一个选项卡里的内容Tabs(){TabContent(){Image($r('app.media.a9')).width('100%').height('100%')}.tabBar('关注')TabContent(){Text('推荐').width('100%').height('100%').backgroundColor('#C8BFE7')}.tabBar('推荐')TabContent(){Text('视频').width('100%').height('100%').backgroundColor('#93D0E0')}.tabBar('视频')TabContent(){Text('音频').width('100%').height('100%').backgroundColor('#ADDB1C')}.tabBar('音频')TabContent(){Text('商城').width('100%').height('100%').backgroundColor('#EFE4B0')}.tabBar('商城')TabContent(){Text('热点').width('100%').height('100%').backgroundColor('#7092BE')}.tabBar('热点')TabContent(){Text('科目').width('100%').height('100%').backgroundColor('#009BDE')}.tabBar('科目')TabContent(){Text('类型').width('100%').height('100%').backgroundColor('#AD7252')}.tabBar('类型')}// .vertical(true).barMode(BarMode.Scrollable).animationDuration(3000).animationMode(AnimationMode.CONTENT_FIRST).barOverlap(true).backgroundBlurStyle(BlurStyle.NONE).barBackgroundColor('#00000000')}.tabBar('首页')TabContent() { //每一个选项卡里的内容Row() {Text('推荐的文字')}.height('100%').width('100%').backgroundColor('blue')}.tabBar('推荐')TabContent() { //每一个选项卡里的内容Row() {Text('发现的文字')}.height('100%').width('100%').backgroundColor('#EBA0B9')}.tabBar('发现')TabContent() { //每一个选项卡里的内容Row() {Text('我的文字')}.height('100%').width('100%').backgroundColor('#F0E300')}.tabBar('我的')}.height('100%').width('100%')}@State currentIndex:number=0//被选中的tab下标@Builder navStyle(img:Resource,title:string,index:number){Column(){SymbolGlyph(img).fontColor([index==this.currentIndex?'#1698ce':'#6b6b6b'])Text(title).fontColor(index==this.currentIndex?'#1698ce':'#6b6b6b')}}@Builder test3(){Tabs({barPosition:BarPosition.End,index:this.currentIndex}) {TabContent(){//每一个选项卡里的内容Row(){Text('首页文字')}.height('100%').width('100%').backgroundColor('red')}.tabBar(this.navStyle($r('sys.symbol.house'),'首页',0))TabContent() { //每一个选项卡里的内容Row() {Text('推荐的文字')}.height('100%').width('100%').backgroundColor('blue')}.tabBar(this.navStyle($r('sys.symbol.chevron_left_2'),'推荐',0))TabContent() { //每一个选项卡里的内容Row() {Text('发现的文字')}.height('100%').width('100%').backgroundColor('#EBA0B9')}.tabBar('发现')TabContent() { //每一个选项卡里的内容Row() {Text('我的文字')}.height('100%').width('100%').backgroundColor('#F0E300')}.tabBar('我的')}.height('100%').width('100%').onChange((index)=>{this.currentIndex=index})}
}