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)
}
@Builder
pagesMap(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;//logo
flowsCount: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