8-游戏中心首页

0 阅读7分钟

任务

需求

构建游戏中心首页,包括游戏广告轮播图、常用功能、热门游戏、即将公测、为您推荐各部分。

界面原型

轮播及常用功能及热门游戏:

image.png

即将公测:

image 1.png

为您推荐:

image 2.png

涉及知识点

  1. Scroll:滚动容器
  2. Swiper:滑块视图容器(轮播组件)
  3. ForEach:循环渲染
  4. LazyForEach:懒加载
  5. Grid:网格布局
  6. List:列表组件
  7. Rating:评分的组件
  8. WaterFlow:瀑布流容器
  9. 数据源监听

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))
      ...

预览效果:

image 3.png

2 轮播图

页面原型:

image 4.png

  1. 图片硬编码效果
  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参考:

developer.huawei.com/consumer/cn…

预览效果:

image 5.png

  1. 准备模型层数据

在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()
  1. 将轮播图数据源改成从模拟viewmodel中获取并foreach展示:
          ForEach(GameHomeViewModel.getGameAdvImages(),(item:Resource)=>{
            Image(item).gameImageStyle()
          },(item:Resource)=>JSON.stringify(item))

Note: ForEach:接口基于数组类型数据来进行循环渲染 developer.huawei.com/consumer/cn…

预览效果:

image 4.png

3 常用功能

页面原型:

image 6.png

  1. 将常用功能封装成接口

在model下创建arkts文件,命名为GameItemBean,封装图标和标题:

export default interface  GameItemBean{
  title:string
  icon?:Resource
}
  1. 模拟数据

在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
  }
  1. 布局常用功能列表

使用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:网格容器,由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局

组件参考:

developer.huawei.com/consumer/cn…

预览效果:

image 6.png

4 热门游戏

页面原型:

image 7.png

  1. 准备数据

仍然使用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;

  }
  1. 使用列表布局页面
        //热门游戏
        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),设置列表为横向

组件参考:

developer.huawei.com/consumer/cn…

预览效果:

image 7.png

5 即将公测

页面原型:

image 8.png

  1. 准备数据

即将公测和热门游戏处理方式类似,仍然使用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;

  }
  1. 局部封装

即将公测和热门游戏显示类似,此处采用@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)
    }
  }
  1. 展示即将公测游戏

继续在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)

预览效果:

image 8.png

6 为您推荐

界面原型:

image 2.png

使用瀑布流布局:

WaterFlow:瀑布流容器

由“行”和“列”分割的单元格所组成,通过容器自身的排列规则,将不同大小的“项目”自上而下,如瀑布般紧密布局。

瀑布流布局组件参考:

developer.huawei.com/consumer/cn…

  1. 封装数据接口

在model下新建arkts文件,命名为:GameInfoBean,封装推荐的游戏信息:

export interface GameInfoBean {
  id: number;
  imageUrl: Resource | string;
  name: string;
  score: number;
  type: string;
  desc: string; //描述
}
  1. 准备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;
	}
  1. 模拟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;
  }
  1. 实现数据源接口

在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;
      }
    }
  }
  1. 瀑布流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 })
    }
  }

预览效果:

image 9.png

游戏单元文字描述部分样式,继续在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…

预览效果:

image 2.png

参考

代码仓

gitee.com/snowyvalley…

##鸿蒙应用开发 ##休闲娱乐