任务
需求
构建游戏中心首页,包括游戏广告轮播图、常用功能、热门游戏、即将公测、为您推荐各部分。
界面原型
轮播及常用功能及热门游戏:
即将公测:
为您推荐:
涉及知识点
- Scroll:滚动容器
- Swiper:滑块视图容器(轮播组件)
- ForEach:循环渲染
- LazyForEach:懒加载
- Grid:网格布局
- List:列表组件
- Rating:评分的组件
- WaterFlow:瀑布流容器
- 数据源监听
1 新建首页组件
在ets下新建文件夹,命名为components,在该文件夹下新建arkts文件,命名为:GameCenterHome,并编写如下骨架代码:
@Component
export default struct GameCenterHome{
build() {
Scroll(){
Column(){
Text('首页')
}
}
}
}
Note
Scroll:滚动容器,仅能包含一个根节点
自定义组件:
使用@Component,并且包含build函数
在MainPage中调用:
@Entry
@Component
struct MainPage {
@State pageIndex: number = 0;//页面索引
build() {
Tabs({barPosition:BarPosition.End}){
TabContent(){
//Text('首页')
GameCenterHome()
}
//.tabBar('首页')
.tabBar(this.MyTabBuilder(TabID.HOME))
...
预览效果:
2 轮播图
页面原型:
- 图片硬编码效果
build() {
Scroll(){
Column(){
Text('首页')
.fontWeight(FontWeight.Bold)
.fontSize(22)
.width('95%')
.margin(20)
//轮播图
Swiper(){
Image($r('app.media.game1'))
.gameImageStyle()
Image($r('app.media.game2'))
.gameImageStyle()
Image($r('app.media.game3'))
.gameImageStyle()
Image($r('app.media.game4'))
.gameImageStyle()
Image($r('app.media.game5'))
.gameImageStyle()
}
.autoPlay(true)
.margin(10)
}
}
.width('100%')
.height('100%')
.backgroundColor('#f1f3f5')
}
}
图片样式:
@Extend(Image)
function gameImageStyle(){
.width('90%')
.height(200)
.borderRadius(12)
}
Note
Swiper:滑块视图容器
api参考:
预览效果:
- 准备模型层数据
在model目录下新建arkts文件,命名为:GameHomeViewModel:
export class GameHomeViewModel{
getGameAdvImages(): Array<Resource> {
let gameAdvImages: Resource[] = [
$r('app.media.game1'),
$r('app.media.game2'),
$r('app.media.game3'),
$r('app.media.game4'),
$r('app.media.game5'),
];
return gameAdvImages;
}
}
export default new GameHomeViewModel()
- 将轮播图数据源改成从模拟viewmodel中获取并foreach展示:
ForEach(GameHomeViewModel.getGameAdvImages(),(item:Resource)=>{
Image(item).gameImageStyle()
},(item:Resource)=>JSON.stringify(item))
Note: ForEach:接口基于数组类型数据来进行循环渲染 developer.huawei.com/consumer/cn…
预览效果:
3 常用功能
页面原型:
- 将常用功能封装成接口
在model下创建arkts文件,命名为GameItemBean,封装图标和标题:
export default interface GameItemBean{
title:string
icon?:Resource
}
- 模拟数据
在GameHomeViewModel中模拟数据,添加函数:
//常用功能列表数据
getGameItemBeanData():Array<GameItemBean>{
let items: GameItemBean[] = [
{title:'游戏',icon:$r('app.media.gameicon1')},
{title:'攻略',icon:$r('app.media.gameicon2')},
{title:'预告',icon:$r('app.media.gameicon3')},
{title:'活动',icon:$r('app.media.gameicon4')},
{title:'游乐场',icon:$r('app.media.gameicon5')},
{title:'同城',icon:$r('app.media.gameicon6')},
{title:'友聊',icon:$r('app.media.gameicon7')},
{title:'门票',icon:$r('app.media.gameicon8')}
]
return items
}
- 布局常用功能列表
使用2X4网格布局:
//常见功能列表
Grid(){
ForEach(GameHomeViewModel.getGameItemBeanData(),(item:GameItemBean) =>{
GridItem(){
Column(){
Image(item.icon).width(50).height(50).borderRadius(8)
Text(item.title).fontSize(14).margin({top:3})
}
}
},(item:GameItemBean) => JSON.stringify(item))
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsTemplate('1fr 1fr')
.height(180)
.width('90%')
.backgroundColor(Color.White)
.borderRadius(15)
Note:
Grid:网格容器,由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局
组件参考:
预览效果:
4 热门游戏
页面原型:
- 准备数据
仍然使用GameItemBean接口封装数据,在GameHomeViewModel中添加getHotGame函数提供数据模拟:
//热门游戏数据
getHotGame(){
let hotGames: GameItemBean[] = [
{title:'迷你世界',icon:$r('app.media.game11')},
{title:'蛋仔派对',icon:$r('app.media.game12')},
{title:'地铁跑酷',icon:$r('app.media.game13')},
{title:'风暴奇兵',icon:$r('app.media.game14')},
{title:'石器时代',icon:$r('app.media.game15')},
{title:'葫芦娃',icon:$r('app.media.game16')},
{title:'生存战争',icon:$r('app.media.game17')},
{title:'暴走砖块',icon:$r('app.media.game18')}
]
return hotGames;
}
- 使用列表布局页面
//热门游戏
Text('热门游戏')
.width('95%')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.margin(10)
List(){
ForEach(GameHomeViewModel.getHotGame(),(item:GameItemBean)=>{
ListItem(){
Column({space:5}){
Image(item.icon).width(100).height(150).margin(10).borderRadius(8)
Text(item.title)
Button('试玩').height(30)
}
}
},(item:GameItemBean) => JSON.stringify(item))
}
.listDirection(Axis.Horizontal)
.backgroundColor(Color.White)
.margin({left:20,right:20})
.padding(10)
.borderRadius(12)
Note:
List:列表包含一系列相同宽度的列表项。适合连续、多行呈现同类数据,例如图片和文本。
listDirection(Axis.Horizontal),设置列表为横向
组件参考:
预览效果:
5 即将公测
页面原型:
- 准备数据
即将公测和热门游戏处理方式类似,仍然使用GameItemBean接口封装数据,在GameHomeViewModel中添加getBetaGames函数提供数据模拟:
//即将公测游戏数据
getBetaGames(){
let games: GameItemBean[] = [
{title:'造物厨房',icon:$r('app.media.game19')},
{title:'神秘城堡',icon:$r('app.media.game20')},
{title:'秦时明月',icon:$r('app.media.game21')},
{title:'海底世界',icon:$r('app.media.game22')},
{title:'斗罗大陆',icon:$r('app.media.game23')},
{title:'上学去',icon:$r('app.media.game24')},
{title:'未来已来',icon:$r('app.media.game25')},
{title:'异兽世界',icon:$r('app.media.game26')}
]
return games;
}
- 局部封装
即将公测和热门游戏显示类似,此处采用@Builder封装,继续在GameCenterHome中编码,将热门游戏展示部分的代码拷贝到过来,并传递参数:
@Builder listGameCell(item:GameItemBean){
Column({space:5}){
Image(item.icon).width(100).height(150).margin(10).borderRadius(8)
Text(item.title)
Button('预定').height(30)
}
}
- 展示即将公测游戏
继续在GameCenterHome中编码:
//即将公测
Text('即将公测')
.width('95%')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.margin(10)
List(){
ForEach(GameHomeViewModel.getBetaGames(),(item:GameItemBean)=>{
ListItem(){
this.listGameCell(item)
}
},(item:GameItemBean) => JSON.stringify(item))
}.listDirection(Axis.Horizontal)
.backgroundColor(Color.White)
.margin({left:20,right:20})
.padding(10)
.borderRadius(12)
预览效果:
6 为您推荐
界面原型:
使用瀑布流布局:
WaterFlow:瀑布流容器
由“行”和“列”分割的单元格所组成,通过容器自身的排列规则,将不同大小的“项目”自上而下,如瀑布般紧密布局。
瀑布流布局组件参考:
developer.huawei.com/consumer/cn…
- 封装数据接口
在model下新建arkts文件,命名为:GameInfoBean,封装推荐的游戏信息:
export interface GameInfoBean {
id: number;
imageUrl: Resource | string;
name: string;
score: number;
type: string;
desc: string; //描述
}
- 准备waterFlow初始数据
在GameHomeViewModel中添加loadWaterFlowData方法,为waterFlow准备初始数据:
loadWaterFlowData(): GameInfoBean[] {
let gameInfos : GameInfoBean[] = [
{
id:1,
imageUrl:$rawfile('gamewaterflow/game108.png'),
name:'火柴人',
score:9.0,
desc:'火柴人大战人类',
type:'战争 射击 像素'
},
{
id:2,
imageUrl:$rawfile('gamewaterflow/game109.png'),
name:'鹅城对决',
score:9.2,
desc:'当一回蜘蛛侠',
type:'超级英雄 二次元 都市'
},
{
id:3,
imageUrl:$rawfile('gamewaterflow/game110.png'),
name:'皮特的小镇',
score:9.8,
desc:'玩偶应用类游戏,你可以成为玩偶的主人',
type:'Q版 生活模拟 卡通'
},
{
id:4,
imageUrl:$rawfile('gamewaterflow/game101.png'),
name:'钓鱼人生',
score:9.6,
desc:'一起来钓鱼吧',
type:'动物 钓鱼'
},
{
id:5,
imageUrl:$rawfile('gamewaterflow/game102.png'),
name:'大排档',
score:9.0,
desc:'休闲游戏,到大排档喝啤酒去',
type:'卡通 模拟经营 都市'
},
{
id:6,
imageUrl:$rawfile('gamewaterflow/game103.png'),
name:'海上餐厅',
score:9.3,
desc:'海上餐厅,边吃边钓鱼,大物出现',
type:'航海 钓鱼'
},
{
id:7,
imageUrl:$rawfile('gamewaterflow/game104.png'),
name:'茶叶蛋大冒险',
score:9.8,
desc:'烧脑分析解密通关',
type:'闯关 卡通'
},
{
id:8,
imageUrl:$rawfile('gamewaterflow/game105.png'),
name:'饥饿的鲨鱼',
score:9.0,
desc:'鲨鱼宝宝来了,别跑',
type:'动作 卡通'
},
{
id:9,
imageUrl:$rawfile('gamewaterflow/game106.png'),
name:'煎饼侠',
score:9.6,
desc:'经营模拟游戏,一个煎饼铺的故事',
type:'模拟经营 卡通'
},
{
id:10,
imageUrl:$rawfile('gamewaterflow/game107.png'),
name:'文具店',
score:9.5,
desc:'给你一家文具店,脑洞大开的文具店,不可思议的游戏开始了',
type:'动作 射击 解谜'
},
]
return gameInfos;
}
- 模拟waterflow加载更多数据
在GameHomeViewModel中添加loadMoreWaterFlowData方法,模拟为waterFlow加载更多数据:
//为waterflow加载更多数据
loadMoreWaterFlowData(): GameInfoBean[] {
let gameInfos : GameInfoBean[] = [
{
id:11,
imageUrl:$rawfile('gamewaterflow/game111.png'),
name:'植物变异',
score:9.0,
desc:'植物大战人类',
type:'战争 射击 像素'
},
{
id:12,
imageUrl:$rawfile('gamewaterflow/game112.png'),
name:'楼兰对决',
score:9.3,
desc:'当一回侠客去古国',
type:'超级英雄 二次元'
},
{
id:13,
imageUrl:$rawfile('gamewaterflow/game113.png'),
name:'神秘厨娘',
score:9.8,
desc:'做一个厨娘的感觉咋样',
type:'Q版 生活模拟 卡通'
},
{
id:14,
imageUrl:$rawfile('gamewaterflow/game114.png'),
name:'游戏人生',
score:9.4,
desc:'一起来游戏吧',
type:'动物 卡通'
},
{
id:15,
imageUrl:$rawfile('gamewaterflow/game115.png'),
name:'大排档',
score:9.0,
desc:'休闲游戏,到大排档喝啤酒去',
type:'卡通 模拟经营 都市'
},
{
id:16,
imageUrl:$rawfile('gamewaterflow/game116.png'),
name:'海上餐厅',
score:9.3,
desc:'海上餐厅,边吃边钓鱼,大物出现',
type:'航海 钓鱼'
},
{
id:17,
imageUrl:$rawfile('gamewaterflow/game117.png'),
name:'蛋仔大冒险',
score:9.4,
desc:'烧脑分析解密通关',
type:'闯关 卡通'
},
{
id:18,
imageUrl:$rawfile('gamewaterflow/game118.png'),
name:'玛卡巴卡的鲨鱼',
score:9.7,
desc:'天线宝宝来了,别跑',
type:'动作 卡通'
},
{
id:19,
imageUrl:$rawfile('gamewaterflow/game119.png'),
name:'背锅侠',
score:9.0,
desc:'经营模拟游戏,一个背锅铺的故事',
type:'模拟经营 休闲益智'
},
{
id:20,
imageUrl:$rawfile('gamewaterflow/game120.png'),
name:'门禁大楼',
score:9.2,
desc:'你能分辨出哪些人该进入大门吗',
type:'动作 解谜'
},
]
return gameInfos;
}
- 实现数据源接口
在model下新建arkts文件,命名为:GameHomeWaterFlowDataSource.ets,用于游戏中心首页瀑布流组件数据源接口实现:
首先实现数据源接口中的相关方法:
import { GameInfoBean } from "./GameInfoBean";
export class GameHomeWaterFlowDataSource implements IDataSource {
public dataArray: GameInfoBean[] = [];
private listeners: DataChangeListener[] = [];
constructor(dataArray: GameInfoBean[]) {
for (let i = 0; i < dataArray.length ;i ++){
this.dataArray.push(dataArray[i]);
}
}
totalCount(): number {
return this.dataArray.length;
}
getData(index: number): GameInfoBean {
return this.dataArray[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
if(this.listeners.indexOf(listener) < 0){
this.listeners.push(listener);
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
let pos = this.listeners.indexOf(listener);
if(pos >= 0){
this.listeners.splice(pos,1);
}
}
}
继续添加更多数据,刷新数据,删除数据对应的方法。
notifyDataAdd(index: number): void {
this.listeners.forEach(listener =>{
listener.onDataAdd(index);
})
}
notifyDataDelete(index:number): void {
this.listeners.forEach((listener =>{
listener.onDataDelete(index);
}))
}
public addMoreData(gameDataArray: GameInfoBean[]): void {
let idx = this.dataArray.length;
for (let element of gameDataArray) {
element.id = idx +1;
this.dataArray.push(element);
idx ++;
}
this.notifyDataAdd(this.dataArray.length-1);
}
public refreshData(gameDataArray: GameInfoBean[]): void {
this.dataArray = [];
for (let element of gameDataArray) {
this.dataArray.push(element);
}
this.listeners.forEach(listener =>{
listener.onDataReloaded()
})
}
public deleteItem(id: number): void {
let delIdx = -1;
for (let index = 0; index < this.dataArray.length; index ++) {
if(this.dataArray[index].id === id){
delIdx = index;
this.dataArray.splice(delIdx,1);
this.notifyDataDelete(delIdx);
break;
}
}
}
- 瀑布流UI
在GameCenterHome中编码,首先定义数据源,加载条状态控制及滚动条控制,缓存记录数等相关变量:
@State datasource: GameHomeWaterFlowDataSource =
new GameHomeWaterFlowDataSource(GameHomeViewModel.loadWaterFlowData());
private isEnd: boolean = false;
private scroller:Scroller = new Scroller();
private cachedCount: number = 2;//缓存2条
定义加载等待条:
@Builder
itemLoadFoot():void {
Row({ space: 5 }) {
LoadingProgress()
.width(30)
.height(30)
Text('正在加载...').fontSize(11).fontColor(Color.Gray)
}
.justifyContent(FlexAlign.Center)
.width('100%')
.margin({top: 5})
.visibility(this.isEnd ? Visibility.Hidden:Visibility.Visible)
}
当数据小于5条时,不显示加载条,在aboutToAppear中编码:
aboutToAppear(): void {
if(this.datasource.totalCount() <=5){//数据源总数低于5时不显示加载条
this.isEnd = true;
}
}
在build函数中显示节标题:
//为您推荐
Text('为您推荐')
.width('95%')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.margin({ top:20 })
使用瀑布流组件:
//瀑布流组件
WaterFlow({
footer: ():void =>this.itemLoadFoot(),
scroller: this.scroller
}){
LazyForEach(this.datasource,(item: GameInfoBean, index)=>{
FlowItem(){
this.waterFlowItemCell(item)
}
})
}
.height('100%')
.columnsTemplate('1fr 1fr')
.columnsGap(8)
.rowsGap(8)
.margin(20)
.padding({
bottom:40
})
.cachedCount(6)
.nestedScroll({//设置向前向后两个方向上的嵌套滚动模式,实现与父组件的滚动联动。
scrollForward:NestedScrollMode.PARENT_FIRST,
scrollBackward:NestedScrollMode.SELF_FIRST
})
.onScrollIndex((first:number,last: number)=>{
console.info('last:'+last+'first:'+first)
//缓存2个,总数是10
if((last + this.cachedCount) === this.datasource.totalCount()){
setTimeout(()=>{
this.datasource.addMoreData(GameHomeViewModel.loadMoreWaterFlowData())
},500)
}
})
游戏单元样式:
@Builder waterFlowItemCell(item: GameInfoBean) {
Column() {
Image(item.imageUrl)
.width('100%')
.objectFit(ImageFit.Contain)
.borderRadius({ topLeft: 12, topRight: 12 })
}
}
预览效果:
游戏单元文字描述部分样式,继续在waterFlowItemCell中添加代码文本部分添加在内嵌的column中最终样式如下:
@Builder waterFlowItemCell(item: GameInfoBean) {
Column() {
Image(item.imageUrl)
.width('100%')
.objectFit(ImageFit.Contain)
.borderRadius({ topLeft: 12, topRight: 12 })
Column(){
Text(item.name)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.maxLines(2)
.alignSelf(ItemAlign.Start)
.margin({
top:8,
bottom:4
})
.width('100%')
Text(item.type)
.fontSize(12)
.opacity(0.6)
.width('100%')
Text(item.desc)
.fontSize(14)
.width('100%')
.textOverflow({overflow:TextOverflow.Ellipsis})
.maxLines(1)
.fontColor(Color.Brown)
Row(){
Text('评分:')
.fontSize(12)
.opacity(0.6)
Rating({ rating: 5*item.score/10, indicator: true })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
.margin(5)
}
.backgroundColor(Color.White)
.borderRadius(12)
}
Note: Rating:评分组件: developer.huawei.com/consumer/cn…
预览效果:
参考
代码仓
##鸿蒙应用开发 ##休闲娱乐