目录
前言
在 HarmonyOS 应用开发中,优秀的界面布局是用户体验的基础。ArkUI 作为新一代声明式 UI 开发框架,提供了丰富多样的布局组件和容器组件,帮助开发者高效构建复杂界面。本文将系统性地介绍 ArkUI 的核心布局方式,包括线性布局、层叠布局、弹性布局等基础布局,以及列表、轮播、选项卡等高级容器组件。
一、线性布局(Column/Row)
1.先布局后内容
容器组件:布局
Colum {} 内容会竖着排
Row() {} 内容会横着排
内容组件:内容
Text(‘内容’)
@Entry
@Component
struct Index {
build() { //里面要有唯一的容器根组件
Column() {
Column() {
Text('大壮')
Text('中壮');
Text('小壮');
}
Row() {
Text('大壮')
Text('中壮');
Text('小壮');
}
}
}
}
2.元素在主轴上的排列方式
(1) Column:justifyContent(FlexAlign.xxx)
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)
Start
End
(2) Row:justifyContent(FlexAlign.xxx)
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)
3.元素在交叉轴上的排列方式
(1)Column:alignItems(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)') //右对齐
(2)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)')
二、层叠布局(Stack)
1.开发布局
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)
}
}
}
2.对齐方式
Stack组件通过alignContent参数实现位置的相对移动。如图2所示,支持九种对齐方式。
@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 })
}
}
三、弹性布局(Flex)
弹性布局(Flex)提供更加有效的方式对容器中的子元素进行排列、对齐和分配剩余空间。常用于页面头部导航栏的均匀分布、页面框架的搭建、多行数据的排列等。
容器默认存在主轴与交叉轴,子元素默认沿主轴排列,子元素在主轴方向的尺寸称为主轴尺寸,在交叉轴方向的尺寸称为交叉轴尺寸。
@Entry
@Component
struct Index {
build() {
// Flex默认主轴水平往右,交叉轴垂直往下 -> Row
// 1. 主轴方向 通过direction: FlexDirection.Row/Column来调整
// 2. 主轴对齐方式 justifyContent
// 3. 交叉轴对齐方式 alignItems
// 单行或者单列的情况,优先使用线性布局(本质基于Flex设计)
Flex({
direction: FlexDirection.Column,
justifyContent: FlexAlign.SpaceEvenly,
alignItems: ItemAlign.Stretch //拉伸
}) {
Text()
.width(80).height(80)
.backgroundColor(Color.Pink)
.border({ width: 1, color: Color.Blue })
Text()
.width(80).height(80)
.backgroundColor(Color.Pink)
.border({ width: 1, color: Color.Blue })
Text()
.width(80).height(80)
.backgroundColor(Color.Pink)
.border({ width: 1, color: Color.Blue })
}
.width('100%')
.height(600)
.backgroundColor('#5f9a5c')
}
}
@Entry
@Component
struct Index {
build() {
// 4. 换行 wrap
Flex({
wrap: FlexWrap.Wrap// 换行
}) {
Text()
.width(80).height(80)
.backgroundColor(Color.Pink)
.border({ width: 1, color: Color.Blue })
Text()
.width(80).height(80)
.backgroundColor(Color.Pink)
.border({ width: 1, color: Color.Blue })
Text()
.width(80).height(80)
.backgroundColor(Color.Pink)
.border({ width: 1, color: Color.Blue })
Text()
.width(80).height(80)
.backgroundColor(Color.Pink)
.border({ width: 1, color: Color.Blue })
Text()
.width(80).height(80)
.backgroundColor(Color.Pink)
.border({ width: 1, color: Color.Blue })
}
.width(300)
.height(300)
.backgroundColor('#5f9a5c')
}
}
案例:
let names: string[] =['ArkUI', 'ArkTS', '界面开发', '系统能力', '权限控制', '元服务']
@Entry
@Component
struct Index {
build() {
Column() {
Text('阶段选择')
.fontSize(30)
.fontWeight(700)
.padding(15)
.width('100%')
Flex({
wrap: FlexWrap.Wrap
}) {
ForEach(names, (item: string) => {
Text(item)
.padding(10)
.margin(5)
.backgroundColor('#f1f1f1')
})
}
}
}
}
四、创建列表(List)
列表是一种容器,当列表项达到一定数量,超过List容器组件大小,自动滚动
主轴方向(垂直) | .listDirection(Axis.Horizontal) |
交叉轴布局 | lane(列数,间距) |
列对齐方式 | .alignListItem(ListItemAlign.Center) |
滚动条状态 | .scrollBar(Barstate.Auto) |
分割线样式 | divider({…}) |
@Entry
@Component
struct Index {
build() {
Column() {
List() {
ForEach(Array.from({length: 20}),() => {
ListItem() {
Row() {}
.width('100%')
.height(100)
.backgroundColor(Color.Brown)
}.padding(10)
})
}
.width('100%')
.layoutWeight(1)
.backgroundColor(Color.Orange)
.listDirection(Axis.Vertical) //调整主轴方向
.lanes(1, 5) //调整列数和间距
.alignListItem(ListItemAlign.Center)
.scrollBar(BarState.Auto) //按需自动显示滚动条
.divider({
strokeWidth: 3, //线宽
color: Color.Blue, //颜色
startMargin: 10, //左边线距离边缘的间隙
endMargin: 10 //右边线距离边缘的间隙
})
}
.width('100%')
.height('100%')
}
}
lanes(2, 5)
五、创建轮播(Swiper)
1.基本用法
Swiper() {
// 1.轮播内容
}
// 2.设置尺寸(内容会自动拉伸)
.width('100%')
.height(100)
@Entry
@Component
struct Index {
build() {
Column() {
Swiper() {
Text('1').backgroundColor(Color.Orange)
Text('2').backgroundColor(Color.Yellow)
Text('3').backgroundColor(Color.Pink)
}
.loop(true) // 是否开启循环
.autoPlay(true) // 是否自动播放
.interval(2000) // 自动播放的间隔时间
.vertical(false) // 横向
.width('100%')
.height(100)
Swiper() {
Image($r('app.media.syt'))
Image($r('app.media.wave'))
}
.width('100%')
.height(400)
}
}
}
2.常用属性
属性方法 | 传值 | 作用 | 默认值 |
---|---|---|---|
loop | boolean | 是否开启循环 | true |
autoPlay | boolean | 是否自动播放 | false |
interval | number | 自动播放的时间间隔 | 3000 |
vertical | boolean | 纵向滑动轮播 | false |
3.样式自定义
Swiper() {
}
.aspectRatio(2) //宽高比为2
.indicator(
Indicator.dot()// 小圆点
.itemWidth(20)// 默认的宽
.itemHeight(20)//默认的高
.color(Color.Black)// 默认的颜色
.selectedItemWidth(30)//选中的宽
.selectedItemHeight(30)// 选中的高
.selectedColor(Color.White)// 选中的颜色
)
六、选项卡Tabs
1.基本用法
@Entry
@Component
struct Index {
build() {
Tabs() {
TabContent() {
Text('首页内容') //有且一个子组件
}
.tabBar('首页')
TabContent() {
Text('推荐内容')
}
.tabBar('推荐')
TabContent() {
Text('发现内容')
}
.tabBar('发现')
TabContent() {
Text('我的内容')
}
.tabBar('我的')
}
}
}
2.常用属性
名称 | 作用 |
---|---|
barPostion | 调整位置 :开头 或 结尾(参数) |
vertical | 调整导航 :水平 或 垂直 |
scrollable | 调整是否:手势滑动 切换 |
animationDuration | 点击滑动动画时间 |
@Entry
@Component
struct Index {
build() {
Tabs({barPosition: BarPosition.Start}) {
TabContent() {
Text('首页内容') //有且一个子组件
}
.tabBar('首页')
TabContent() {
Text('推荐内容')
}
.tabBar('推荐')
TabContent() {
Text('发现内容')
}
.tabBar('发现')
TabContent() {
Text('我的内容')
}
.tabBar('我的')
}
.vertical(false) //选项横向排布
.scrollable(false) //false为不支持手势滑动
.animationDuration(0) //点击动画时间为0毫秒
}
}
3.滚动导航栏
如果导航栏的内容较多,屏幕无法容纳时,可以将他设置为滚动
可以通过Tabs组件的barMode属性即可调整为固定导航栏或滚动导航栏
@Entry
@Component
struct Index {
titles: string[] = [
'首页', '关注', '热门', '军事', '体育',
'八卦', '数码', '财经', '美食', '旅行'
]
build() {
Tabs() {
ForEach(this.titles, (item: string, index) => {
TabContent() {
Text(`${item}内容`)
}
.tabBar(item)
})
}
.barMode(BarMode.Scrollable)//滚动导航
}
}
4.自定义TabBar
@Entry
@Component
struct Index {
@Builder myBuilder (title: string, img: Resource) {
Column() {
Image(img)
.width(30)
Text(title)
}
}
build() {
Tabs({barPosition: BarPosition.End}){
TabContent() {
Text('购物车')
}
.tabBar(this.myBuilder('购物车', $r('app.media.ic_tabbar_icon_2')))
TabContent() {
Text('我的内容')
}
.tabBar(this.myBuilder('我的', $r('app.media.ic_tabbar_icon_3')))
}
}
}
5.自定义TabBar-高亮切换
核心思路:
1.监听切换事件 -> 得到索引值,记录高亮的索引
2.给每个tabBar起个标记:0,1,2
3.在TabBar内部比较标记 == 记录的索引?高亮:不高亮
名称 | 功能描述 |
---|---|
onChange(event:(index: number) => void) | Tab页签切换后触发的事件;index:当前显示的index索引,索引从0开始;滑动切换、点击切换均会触发 |
onTabBarClick(event: (index: number) => void) | Tab页签点击后触发的事件 |
小米有品案例
@Entry
@Component
struct Index {
@State indexNumber: number = 0;
@Builder myBuilder(index: number, title: string, img1: ResourceStr, img2: ResourceStr) {
Column() {
Image(this.indexNumber == index? img2: img1)
.width(20)
Text(title)
.fontColor(this.indexNumber == index? Color.Black: Color.Gray)
}
}
build() {
Tabs({barPosition: BarPosition.End}) {
TabContent() {
Text('首页')
}
.tabBar(this.myBuilder(0, '首页', $r('app.media.ic_tabbar_icon_0'), $r('app.media.ic_tabbar_icon_0_selected')))
TabContent() {
Text('分类')
}
.tabBar(this.myBuilder(1, '分类', $r('app.media.ic_tabbar_icon_1'), $r('app.media.ic_tabbar_icon_1_selected')))
TabContent() {
Text('购物车')
}
.tabBar(this.myBuilder(2, '购物车', $r('app.media.ic_tabbar_icon_2'), $r('app.media.ic_tabbar_icon_2_selected')))
TabContent() {
Text('我的')
}
.tabBar(this.myBuilder(3, '我的', $r('app.media.ic_tabbar_icon_3'), $r('app.media.ic_tabbar_icon_3_selected')))
}
.onChange((index: number) => {
this.indexNumber = index
})
}
}