游戏详情制作(Navigation组件)
1.1 需求
使用Navigation实现游戏主详情视图,从瀑布流容器中的游戏项(游戏中心首页-游戏瀑布流列表)点击游戏后进入游戏详情页,从游戏详情页可以返回游戏列表主页。
1.2 界面原型
从瀑布流组件进入:
游戏详情:
2 预备知识
2.1 Navigation组件
2.1.1 Navigation路由导航组件
- 实现页面间及组件内部的页面跳转,也可以实现跨包跳转
- 支持传递跳转参数
- 包含导航页和子页
- 导航页根容器是Navigation
- 子页根容器是NavDestination,用于显示Navigation的内容区
- 导航页不存在与页面栈中,与子页,甚至是子页之间通过路由操作进行切换
- Navigation:导航页包含标题栏(包含菜单栏)、内容区和工具栏,hideToolBar(value: boolean)属性用于显隐工具栏
- NavDestination子页包含标题栏,标题栏包含主副标题和返回键,如未设置主副标题并没有返回键时则不显示标题栏,hideTitleBar属性用于显隐标题栏
2.1.2 子页面
NavDestination是Navigation子页面的根容器
页面显示类型
- 标准类型
- NavDestination组件默认为标准类型
- mode属性为NavDestinationMode.STANDARD
- 弹窗类型
- NavDestination设置mode为NavDestinationMode.DIALOG弹窗类型
- 整个NavDestination默认透明显示
2.1.3 NavPathStack路由操作
- NavPathStack路由栈:
- Navigation路由相关的操作都是基于页面栈NavPathStack提供的方法进行
- 每个Navigation都需要创建并传入一个NavPathStack对象
- 页面管理涉及页面跳转、页面返回、页面替换、页面删除、参数获取、路由拦截等功能
- 页面跳转:
NavPathStack通过Push相关的接口去实现页面跳转的功能
- 普通跳转,通过页面的name去跳转,并可以携带param
this.pageStack.pushPath({ name: "PageOne", param: "PageOne Param" })
this.pageStack.pushPathByName("PageOne", "PageOne Param")
- 带返回回调的跳转,跳转时添加onPop回调,能在页面出栈时获取返回信息,并进行处理
this.pageStack.pushPathByName('PageOne', "PageOne Param", (popInfo) => {console.log('Pop page name is: ' + popInfo.info.name + ', result: ' + JSON.stringify(popInfo.result))
});
- 页面返回:
NavPathStack通过Pop相关接口去实现页面返回功能
// 返回到上一页
this.pageStack.pop()
// 返回到上一个PageOne页面
this.pageStack.popToName("PageOne")
// 返回到索引为1的页面
this.pageStack.popToIndex(1)
// 返回到根首页(清除栈中所有页面)
this.pageStack.clear()
- 页面替换:
NavPathStack通过Replace相关接口去实现页面替换功能
// 将栈顶页面替换为PageOne
this.pageStack.replacePath({ name: "PageOne", param: "PageOne Param" })
this.pageStack.replacePathByName("PageOne", "PageOne Param")
- 页面删除:
NavPathStack通过Remove相关接口去实现删除页面栈中特定页面的功能
// 删除栈中name为PageOne的所有页面
this.pageStack.removeByName("PageOne")
// 删除指定索引的页面
this.pageStack.removeByIndexes([1,3,5])
// 删除指定id的页面
this.pageStack.removeByNavDestinationId("1");
- 移动页面:
NavPathStack通过Move相关接口去实现移动页面栈中特定页面到栈顶的功能
// 移动栈中name为PageOne的页面到栈顶
this.pageStack.moveToTop("PageOne");
// 移动栈中索引为1的页面到栈顶
this.pageStack.moveIndexToTop(1);
- 参数获取
NavPathStack通过Get相关接口去获取页面的一些参数
// 获取栈中所有页面name集合
this.pageStack.getAllPathName()
// 获取索引为1的页面参数
this.pageStack.getParamByIndex(1)
// 获取PageOne页面的参数
this.pageStack.getParamByName("PageOne")
// 获取PageOne页面的索引集合
this.pageStack.getIndexByName("PageOne")
2.2 @Provide 和@Consume装饰器
2.2.1 概述
-
@Provide和@Consume,用于与后代组件的双向数据同步,状态数据实现跨层级传递。(不限于父子,可以是孙辈,穿越能力)
-
通过相同的变量名或者相同的变量别名绑定(建议类型相同,否则会发生类型隐式转换,从而导致应用行为异常)。
-
@Provide装饰的变量,在祖先组件中【提供】信息,@Consume在后代组件中【消费】信息
-
跨组件双向同步
-
@State和@Link组合仅限于父子组件间双向数据同步
-
框架会使用map的形式处理@Provide和@Consume变量,通过map形式传递给当前@Provide所属的所有子组件,子组件在使用@Consume变量时,会从map中查找变量名和别名对应的@Provide变量,并向@Provide注册,所有别名相当于key,必须为string类型
-
更多指导:
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-provide-and-consume
2.2.2 @Provide装饰器
- 可以使用参数指定别名,指定别名则通过别名绑定变量,未指定别名则通过变量名绑定变量
- 支持类型包括:string、number、boolean、Date、enum、Object、class、Map、Set
- 必须赋初值
- 私有属性,仅可在组件内访问
2.2.3 @Consume装饰器
- 可以使用参数指定别名,指定别名则通过别名匹配变量,未指定别名则通过变量名匹配变量
- 类型需和@Provide保持一致
- 不可赋初值
- 私有属性,仅可在组件内访问
2.3 Flex布局
Flex:弹性布局,以弹性方式布局子组件的容器组件
- direction: 主轴方向,默认:FlexDirection.Row
- Row、RowReverse(从右到左)
- Column、ColumnReverse(从下向上)
- wrap:FlexWrap换行
- NoWrap:默认不换行,超过尺寸会压缩
- Wrap:换行
- WrapReverse:反向换行
- justifyContent、alignItems同线性布局
- alignContent:FlexAlign:多行内容时交叉轴内容对齐
内容参考:
https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-flex
2.4 自定义组件生命周期&页面生命周期
2.4.1 自定义组件VS页面
- 自定义组件:由@Component装饰的UI单元
- 页面:应用的UI页面,由一个或多个自定义组件构成,@Entry装饰的自定义组件是页面的入口组件,即页面根节点。
2.4.3 自定义组件生命周期
提供以下接口:
- aboutToAppear:组件即将出现时回调,具体时机:创建自定义组件的新实例后,在执行build函数之前执行
- onDidBuild: 组件build函数执行完成之后进行的回调。可用于埋点数据上报等不影响实际UI的功能。
- aboutToDisappear: 自定义组件析构销毁之前执行。
2.4.4 页面生命周期
被@Entry装饰的组件生命周期,提供以下生命周期接口:
- onPageShow:页面每次显示时触发,包括路由过程、应用进入前台等场景
- onPageHide:页面每次隐藏时触发,包括路由过程、应用进入后台等场景
- onBackPress:当用户点击返回按钮时触发
页面生命周期流程:
3 改造导航页
需将GameCenterHome组件改造为导航页,因为要求点击瀑布流组件中的游戏图片,导航到游戏详情页面,改造过程如下:
- 声明路由栈
@Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack();
需要和子页进行路由同步,因此需要使用@Provide装饰器,并通过参数传递别名子页@Consume在使用时,需要与此处别名保持一致
- 将Navigation作为根组件,并传递路由栈
@Component
export default struct GameCenterHome{
...@Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack();build() {Navigation(this.pageInfos){Scroll(){...}.navDestination(this.pagesMap)}@BuilderpagesMap(name: string, param:number){if(name === 'GameDetail'){GameDetailComponent()//游戏详情页组件}}...
Note:
- Navigation的navDestination属性:
navDestination(builder: (name: string, param: unknown) => void)
创建NavDestination组件。使用builder函数,基于name和param构造NavDestination组件。 - @Builder pagesMap(name: string, param:number):
使用条件渲染定义自定义组件作为Navigation的子页(该组件需要使用NavDestination作为根组件)
- 点击瀑布流游戏图片进行导航,并未导航子页传递参数:
//瀑布流组件WaterFlow({footer: ():void =>this.itemLoadFoot(),scroller: this.scroller}){LazyForEach(this.datasource,(item: GameInfoBean, index)=>{FlowItem(){this.waterFlowItemCell(item)}.onClick(()=>{this.pageInfos.pushPathByName('GameDetail',item.id)})})}...
4 建立导航子页
在components下创建arkts文件,命名为GameDetailComponent,并编写如下代码:
@Component
export struct GameDetailComponent {@Consume('pageInfos') pageInfos: NavPathStack;build() {NavDestination(){Column(){Text('detail:'+ this.pageInfos.getParamByName('GameDetail'))}}.title('游戏详情').backgroundColor('#f1f3f5')}}
Note:
- 需要接收导航页的路由栈,使用@Consume双向同步,按别名匹配
- 使用NavDestination作为根容器,并设置标题为:游戏详情
- 通过路由栈的getParamByName(‘GameDetail’)获取导航页传递来的参数。
预览效果
从MainPage进入预览:
跳转到导航子页:
5 布局游戏详情
5.1 界面原型
5.2 准备游戏详情数据
- 首先封装游戏详情信息,在model下新建arkts文件:GameDetailBean,定义为类实现自GameInfoBean接口,扩展如下属性:
游戏icon,游戏关注数,热度,评价数,帖子数,游戏详情图片
export default class GameDetailBean implements GameInfoBean{id: number;imageUrl: string | Resource;name: string;score: number;type: string;desc: string;//新增属性icon: string | Resource;//logoflowsCount:number;//关注数hotCount: number;//热度commentCount: number;//评论数topicCount: number;//帖子数imageUrlsArray: Array<Resource>constructor() {this.id = 10;this.imageUrl = $rawfile('gamewaterflow/game111.png');this.name = '火柴人战争2';this.score = 9;this.type = '卡通 战争 解密';this.desc = '火柴人战争游戏是一个家喻户晓的游戏,在很久很久以前,火柴人和人类发生了一场战争。';this.icon = $rawfile('gamecenter/gamelogo1.png');this.flowsCount = 1000;this.hotCount = 666;this.commentCount = 88;this.topicCount = 1890;this.imageUrlsArray = [$r('app.media.gameicon4'),$r('app.media.gameicon5'),$r('app.media.gameicon6')];}}
- 根据游戏ID返回游戏详情数据,在GameHomeViewModel添加函数getGameDetail:
getGameDetail(id:number):GameDetailBean {let gameDetailBean = new GameDetailBean();return gameDetailBean;}
- 在游戏详情组件上获取游戏详情信息,在GameDetailComponent中编写代码,首先定义要展示的游戏id,游戏详情,并在组件要出现时获取游戏详情:
private gameId:number = 0;@State gameDetailBean:GameDetailBean = new GameDetailBean();aboutToAppear(): void {this.gameId = this.pageInfos.getParamByName('GameDetail')[0] as number;console.info('gameId:'+this.gameId)this.gameDetailBean = GameHomeViewModel.getGameDetail(this.gameId)}
5.3 游戏logo部分
继续在GameDetailComponent中编码:
Column(){//Text('detail:'+ this.pageInfos.getParamByName('GameDetail'))// 游戏logo部分Row(){Row({space:5}){Image(this.gameDetailBean.icon).width(64).height(64).borderRadius(12)Text(this.gameDetailBean.name).fontSize(22).fontWeight(FontWeight.Bold)}Column(){Text('评分:'+this.gameDetailBean.score.toFixed(1))Rating({ rating: 5*this.gameDetailBean.score/10, indicator: false }).width('80')}}.width('95%').justifyContent(FlexAlign.SpaceBetween)}
预览效果:
5.4 统计栏展示部分
使用Flex布局统计栏,调用@Builder函数
//统计栏展示 flex布局this.counterBar(this.gameDetailBean)
@Builder函数封装:
@Builder counterBar(gameItem:GameDetailBean){Flex({justifyContent:FlexAlign.SpaceAround}){Column({space:5}){Text(`${gameItem.flowsCount}`).counterBarTextStyle()Text('关注').counterBarTextStyle()}Divider().vertical(true).height(30)Column({space:5}){Text(`${gameItem.hotCount}`).counterBarTextStyle()Text('热度').counterBarTextStyle()}Divider().vertical(true).height(30)Column({space:5}){Text(`${gameItem.commentCount}`).counterBarTextStyle()Text('评价').counterBarTextStyle()}Divider().vertical(true).height(30)Column({space:5}){Text(`${gameItem.topicCount}`).counterBarTextStyle()Text('帖子').counterBarTextStyle()}}.width('95%').margin(10)}
文本样式:
@Extend(Text) function counterBarTextStyle(){.fontSize(12).opacity(0.6)
}
预览效果:
5.5 详情展示
使用Swiper组件展示:
// 详情部分Text('详情').fontSize(18).fontWeight(FontWeight.Bold).width('95%').margin({top:20,bottom:10})Swiper(){ForEach(this.gameDetailBean.imageUrlsArray,(item:Resource)=>{Image(item).width('50%').height(120).borderRadius(12)},(item:Resource)=>JSON.stringify(item))}.width('95%').autoPlay(true).displayCount(2).itemSpace(10)
预览效果:
5.6 简介部分
展示游戏类型,描述和进入游戏按钮:
// 简介部分Text('简介').fontSize(18).fontWeight(FontWeight.Bold).width('95%').margin({top:20,bottom:10})this.gameIntroduce(this.gameDetailBean)
在@Builder中封装展示内容,游戏类型需拆分成字符串数组使用foreach进行展示:
@Builder gameIntroduce(gameDetail:GameDetailBean){//展示游戏类型Row({space:10}){ForEach(gameDetail.type.split(' '),(typeItem:string)=>{Text(typeItem).fontSize(14).fontColor(Color.Brown).width(60).height(30).backgroundColor(Color.Orange).borderRadius(8).textAlign(TextAlign.Center)},(typeItem:string)=>typeItem)}//游戏描述Text(gameDetail.desc).maxLines(5).margin(10).width('95%')Button('进入游戏').width('80%')}
预览效果:
参考
代码仓
https://gitee.com/snowyvalley/harmony-app-dev-basic-course.git