Harmony 个人中心(页面交互、跳转、导航、容器组件)(上)https://developer.aliyun.com/article/1407912
三、导航栏
登录后我们进入Index页面,也就是主页面,我们先看看主页面的内容
通过这两张图,我们可以看到,主页面有两部分,选项卡和选项卡内容,通过底部选项卡点击进行切换,那么在写这个页面的时候应该怎么入手呢?首先我们应该先写选项卡,也就是底部导航这一部分内容。
下面我们修改一下Index.ets
中的代码,如下所示:
@Entry @Component struct Index { @State currentIndex: number = 0 private tabsController: TabsController = new TabsController() @Builder TabBuilder(title: string, index: number, selectedImg: Resource, normalImg: Resource) { Column() { Image(this.currentIndex === index ? selectedImg : normalImg) .width(24) .height(24) Text(title) .margin({ top: 4 }) .fontSize(10) .fontColor(this.currentIndex === index ? $r('app.color.mainPage_selected') : $r('app.color.mainPage_normal')) } .justifyContent(FlexAlign.Center) .height(26) .width('100%') .onClick(() => { this.currentIndex = index this.tabsController.changeIndex(this.currentIndex) }) } build() { Tabs({ barPosition: BarPosition.End, controller: this.tabsController }) { TabContent() { // 首页内容 } .padding({ left: 12, right: 12 }) .backgroundColor($r('app.color.mainPage_backgroundColor')) .tabBar(this.TabBuilder('首页', 0, $r('app.media.home_selected'), $r('app.media.home_normal'))) TabContent() { // 我的内容 } .padding({ left: 12, right: 12 }) .backgroundColor($r('app.color.mainPage_backgroundColor')) .tabBar(this.TabBuilder('我的', 1, $r('app.media.mine_selected'), $r('app.media.mine_normal'))) } .width('100%') .backgroundColor(Color.White) .barHeight(56) .barMode(BarMode.Fixed) .onChange((index: number) => { this.currentIndex = index }) } }
下面我们来分析一下这段代码,首先我们定义了currentIndex
变量,用于记录当前选项卡的下标,然后定义了一个tabsController
,用于进行选项卡的控制,接下来使用@Builder
装饰器来构建Tab的内容,使用纵向布局将图标和文字居中摆放,根据currentIndex
和当前Index
的判断来进行Tab的选中、未选中状态。currentIndex
默认为0,则是默认选中第一个Tab,也就是首页Tab,在Tab的点击事件中,我们更新currentIndex
的值,然后再使用this.tabsController.changeIndex(this.currentIndex)
进行切换Tab选项。
然后来看build()
函数中的代码,这里我们使用了Tabs()
组件,通过页签进行内容视图切换的容器组件,每个页签对应一个内容视图。我们看里面传的参数,这里重点是第一个参数,这个的barPosition不是下标的意思,而是设置Tabs的页签位置。默认值:BarPosition.Start
,这里的默认值实际上还要结合Tabs
组件的vertical
属性来结合使用。
vertical
设置为false是为横向Tabs,设置为true时为纵向Tabs。默认值:false,我们没有在代码中设置这个属性,所以默认就是纵向的,那么我们再结合这个BarPosition
的值来看:
Start
,vertical属性方法设置为true时,页签位于容器左侧;vertical属性方法设置为false时,页签位于容器顶部。End
,vertical属性方法设置为true时,页签位于容器右侧;vertical属性方法设置为false时,页签位于容器底部。
那么现在就是Tabs就是在屏幕底部,Tabs可以在屏幕上下左右进行摆放。
在Tabs()
中放置了两个TabContent()
,TabContent,仅在Tabs中使用,对应一个切换页签的内容视图,这个内容视图我们后面来写,这个组件有一个tabBar()
属性,用于装载Tab内容,这里就用到我们之前所构建的TabBuilder()
函数。
最后我们再了解一下Tabs()
组件的其它两个属性:
BarMode
有两个属性,1. Scrollable:每一个TabBar均使用实际布局宽度,超过总长度(横向Tabs的barWidth,纵向Tabs的barHeight)后可滑动。2. Fixed:所有TabBar平均分配barWidth宽度(纵向时平均分配barHeight高度)。onChange
,Tab页签切换后触发的事件。index
:当前显示的index索引,索引从0开始计算。触发该事件的条件:1、TabContent支持滑动时,组件触发滑动时触发。2、通过控制器API接口调用。3、通过状态变量构造的属性值进行修改。4、通过页签处点击触发。
通过这些说明,相信你已经知道Tabs()
的用法了,下面我们保存预览一下Index,默认是Home,点击Mine,如下图所示:
四、首页
在写这个首页的内容之前,我们先看一下整个页面的布局,如图
首页内容呈纵向摆放,同时需要考虑屏幕大小,因此我们可以加一个滑动控件,再看里面的内容,首先是一个标题,标题下面是轮播图,然后是两个网格列表。这样页面内容就介绍完了,那么我么应该怎么来写这个页面的内容呢?
① 轮播图
首先我们完成标题和轮播图,在ets
下创建一个viewmodel
包,该包下创建一个IndexViewModel.ets
文件,代码如下所示:
export class IndexViewModel { /** * 获取轮播图数据 */ getSwiperImages(): Array<Resource> { let swiperImages: Resource[] = [ $r('app.media.fig1'), $r('app.media.fig2'), $r('app.media.fig3'), $r('app.media.fig4') ] return swiperImages } } export default new IndexViewModel()
通过这个getSwiperImages()
来获取轮播图数据,下面我们可以构建主页面的组件了,在在ets
下创建一个view
包,包下新建一个Home.ets文件,里面代码如下所示:
import mainViewModel from '../viewmodel/IndexViewModel'; /** * 首页 */ @Component export default struct Home { private swiperController: SwiperController = new SwiperController(); build() { Scroll() { Column({ space: 12 }) { //首页 Column() { Text('首页') .fontWeight(FontWeight.Medium) .fontSize(24) .margin({ top: 12 }) .padding({ left: 12 }) } .width('100%') .alignItems(HorizontalAlign.Start) //轮播图 Swiper(this.swiperController) { ForEach(mainViewModel.getSwiperImages(), (img: Resource) => { Image(img).borderRadius(16) }, (img: Resource) => JSON.stringify(img.id)) } .margin({ top: 24 }) .autoPlay(true) } } .height('100%') } }
这里的代码就是一个按照我们上面所说的思路来设计的,滚动条里面有标题和轮播图,并设置轮播图自动轮播,在滚动组件中内容未填满页面高度的情况下,内容就会居中显示,我们将Home放在Index中,如下图所示:
然后我们预览Index,看看预览效果图:
② 网格列表
下面我们再来写网格列表,首先要做的就是制造一些数据,先创建一个数据Bean,在ets
下创建一个bean
包,该包下创建一个ItemData.ets
文件,代码如下所示:
export default class ItemData { title: Resource|string; img: Resource; others?: Resource|string; constructor(title: Resource|string, img: Resource, others?: Resource|string) { this.title = title; this.img = img; this.others = others; } }
这个Bean中只有三个数据,标题、图片,其他。下面我们在IndexViewModel
中制造一些假数据,写两个函数,代码如下所示:
/** * 获取第一个网格数据 */ getFirstGridData(): Array<ItemData> { let firstGridData: ItemData[] = [ new ItemData('我的最爱', $r('app.media.love')), new ItemData('历史记录', $r('app.media.record')), new ItemData('消息', $r('app.media.message')), new ItemData('购物车', $r('app.media.shopping')), new ItemData('我的目标', $r('app.media.target')), new ItemData('圈子', $r('app.media.circle')), new ItemData('收藏', $r('app.media.favorite')), new ItemData('回收站', $r('app.media.recycle')) ] return firstGridData } /** * 获取第二个网格数据 */ getSecondGridData(): Array<ItemData> { let secondGridData: ItemData[] = [ new ItemData('排行榜', $r('app.media.top'), '当前热品尽在掌握'), new ItemData('新品首发', $r('app.media.new'), '最新潮牌,马上发布'), new ItemData('大牌闪购', $r('app.media.brand'), '更多大牌敬请期待'), new ItemData('发现好物', $r('app.media.found'), '更多内容等您探索') ] return secondGridData }
这里我们需要导入ItemData,还记得是怎么导入的吗?因为创建others?: Resource|string;
的时候,使用了一个?
,表示可以为空,下面我们在Home中增加这两个网格的UI展示,代码如下所示:
import mainViewModel from '../viewmodel/IndexViewModel'; import ItemData from '../bean/ItemData'; /** * 首页 */ @Component export default struct Home { private swiperController: SwiperController = new SwiperController(); build() { Scroll() { Column({ space: 12 }) { //首页 ... //轮播图 ... //第一个网格布局 Grid() { ForEach(mainViewModel.getFirstGridData(), (item: ItemData) => { GridItem() { Column() { Image(item.img) .width(24) .height(24) Text(item.title) .fontSize(12) .margin({ top: 4 }) } } }, (item: ItemData) => JSON.stringify(item)) } .columnsTemplate('1fr 1fr 1fr 1fr') .rowsTemplate('1fr 1fr') .columnsGap(8) .rowsGap(12) .padding({ top: 12, bottom: 12 }) .height(124) .backgroundColor(Color.White) .borderRadius(24) Text('列表') .fontSize(16) .fontWeight(FontWeight.Medium) .width('100%') .margin({ top: 12 }) //第二个网格布局 Grid() { ForEach(mainViewModel.getSecondGridData(), (secondItem: ItemData) => { GridItem() { Column() { Text(secondItem.title) .fontSize(16) .fontWeight(FontWeight.Medium) Text(secondItem.others) .margin({ top: 4 }) .fontSize(12) .fontColor($r('app.color.home_grid_fontColor')) } .alignItems(HorizontalAlign.Start) } .padding({ top: 8, left: 8 }) .borderRadius(12) .align(Alignment.TopStart) .backgroundImage(secondItem.img) .backgroundImageSize(ImageSize.Cover) .width('100%') .height('100%') }, (secondItem: ItemData) => JSON.stringify(secondItem)) } .width('100%') .height(260) .columnsTemplate('1fr 1fr') .rowsTemplate('1fr 1fr') .columnsGap(8) .rowsGap(12) .margin({ bottom: 55 }) } } .height('100%') } }
这里注意一下我将之前写过的一些代码省略了,所以这里你就不要复制粘贴了,其实网格列表和普通列表在数据渲染的方式上一样,只不过网格列表有一些其他的属性,我们需要了解。
columnsTemplate
:string类型,设置当前网格布局列的数量,不设置时默认1列。例如, ‘1fr 1fr 1fr 1fr’ 是将父组件分4列,将父组件允许的宽分为4等份,第一列占1份,第二列占1份,第三列占1份,第四列占1份。设置为’0fr’时,该列的列宽为0,不显示GridItem。设置为其他非法值时,GridItem显示为固定1列。rowsTemplate
:string类型,设置当前网格布局行的数量,不设置时默认1行。例如,‘1fr 1fr’是将父组件分两行,将父组件允许的高分为2等份,第一行占1份,第二行占1份,设置为’0fr’,则这一行的行宽为0,这一行GridItem不显示。设置为其他非法值,按固定1行处理。columnsGap
:Length类型,设置列与列的间距。默认值:0,设置为小于0的值时,按默认值显示。rowsGap
:Length类型,设置行与行的间距。默认值:0,设置为小于0的值时,按默认值显示。
其余的属性就没有什么好说的,下面我们再预览一下Index,如下图所示:
此时你点击我的,可以看到什么也没有,下面我们来写我的。
五、我的
首先我们看一下我的页面的图
内容同样是呈纵向摆放的,上面是个人信息,中间这里是一个功能列表,最下面是退出按钮,下面我们首先提供列表的数据,在IndexViewModel
中写一个函数,代码如下所示:
/** * 获取设置列表数据 */ getSettingListData(): Array<ItemData> { let settingListData: ItemData[] = [ new ItemData('推送通知', $r('app.media.news'), '开关'), new ItemData('数据管理', $r('app.media.data'), null), new ItemData('菜单设置', $r('app.media.menu'), null), new ItemData('关于', $r('app.media.about'), null), new ItemData('清除缓存', $r('app.media.storage'), null), new ItemData('隐私协议', $r('app.media.privacy'), null) ] return settingListData }
然后我们在view包下先建一个Mine.ets
,代码如下所示:
import router from '@ohos.router'; import promptAction from '@ohos.promptAction'; import ItemData from '../bean/ItemData'; import mainViewModel from '../viewmodel/IndexViewModel'; /** * 我的 */ @Component export default struct Mine { @Builder settingCell(item: ItemData) { Row() { Row({ space: 12 }) { Image(item.img) .width(22) .height(22) Text(item.title) .fontSize(16) } // 设置功能item最右侧的功能项 if (item.others === null) { //可以进入下一级页面 Image($r('app.media.right_grey')) .width(12) .height(24) } else { //开关 Toggle({ type: ToggleType.Switch, isOn: false }) } } .justifyContent(FlexAlign.SpaceBetween) .width('100%') .padding({ left: 8, right: 22 }) } build() { Scroll() { Column({ space: 12 }) { Column() { Text('我的') .fontWeight(FontWeight.Medium) .fontSize(24) .margin({ top: 12 }) .padding({ left: 12 }) } .width('100%') .alignItems(HorizontalAlign.Start) // 个人信息 Row() { Image($r('app.media.account')) .width(48) .height(48) Column() { Text('李先生') .fontSize(20) Text('lonelyxxx@qq.com') .fontSize(12) .margin({ top: 4 }) } .alignItems(HorizontalAlign.Start) .margin({ left: 24 }) } .margin({ top: 24 }) .alignItems(VerticalAlign.Center) .width('100%') .height(96) .backgroundColor(Color.White) .padding({ left: 24 }) .borderRadius(16) // 功能列表 List() { ForEach(mainViewModel.getSettingListData(), (item: ItemData) => { ListItem() { //构建每一个item this.settingCell(item) } .height(48) }, (item: ItemData) => JSON.stringify(item)) } .backgroundColor(Color.White) .width('100%') .height('42%') // 为列表增加分隔线 .divider({ strokeWidth: 1, color: Color.Grey, startMargin: 42, endMargin: 42 }) .borderRadius(16) .padding({ top: 4, bottom: 4 }) Blank() Button('退出登录', { type: ButtonType.Capsule }) .width('90%') .height(40) .fontSize(16) .fontColor($r('app.color.setting_button_fontColor')) .fontWeight(FontWeight.Medium) .backgroundColor($r('app.color.setting_button_backgroundColor')) .margin({ bottom: 55 }) .onClick(() => { promptAction.showToast({ message: '退出登录' }) router.replaceUrl({ url: 'pages/Login' }) }) } .height('100%') } } }
这个代码乍一看很多,下面我们来分析一下,从上往下来,首先是标题和个人信息,这部分就是UI效果,没有什么好说的,然后最关键的功能列表,这里通过@Builder
来装饰settingCell()
函数。通过item的other来判断是否需要显示不同的效果,代码如下所示:
if (item.others === null) { //可以进入下一级页面 Image($r('app.media.right_grey')) .width(12) .height(24) } else { //开关 Toggle({ type: ToggleType.Switch, isOn: false }) }
为null就是一个向右的图标,不为null就是一个开关,默认为false。中间的列表加载就没有什么好说二的,最后的退出登录按钮点击之后就会调用router.replaceUrl({ url: 'pages/Login' })
,返回到登录页面,这里使用的是replaceUrl
,用应用内的某个页面替换当前页面,并销毁被替换的页面。
下面我们通过Index预览一下看看效果:
① 带参数跳转
现在我们登录后的账号并没有其他作用,我们可以把账号替换为李先生,首先我们需要修改登录按钮点击事件的代码,如下所示:
router.replaceUrl({ url: 'pages/Index', params: { account: this.account } });
就是在跳转页面的时候添加一个params属性,然后放入键和值,然后我们在Mine组件中增加一行代码:
//接收传递过来的参数 @State account: string = router.getParams()?.['account'];
这样就能拿到传递的参数值,然后设置到Text中即可。
下面运行一下看看效果
本文就到这里了,鸿蒙提供的一些学习资料是很全面的,通过阅读加上实操过程中的测试可以很快上手应用开发。
六、源码
如果对你有所帮助的话,不妨 Star 或 Fork,山高水长,后会有期~
源码地址:MyCenter