7-影院首页制作

219 阅读7分钟

任务

需求

构建主页,包括电影轮播图、常用功能、正在热映、即将上映,为您推荐各部分。

界面原型

轮播及正在热映:

image-20250328163240017.png

即将上映:

image-20250328163427844.png

为您推荐:

image-20250328163459531.png

涉及知识点

  1. Scroll:滚动容器

  2. Swiper:滑块视图容器(轮播组件)

  3. ForEach:循环渲染

  4. LazyForEach:懒加载

  5. Grid:网格布局

  6. List:列表组件

  7. WaterFlow:瀑布流容器

  8. 数据源监听

1 新建首页组件

在ets下新建文件夹,命名为components,在该文件夹下新建arkts文件,命名为:CinemaHome,并编写如下骨架代码:

@Component
export default struct CinemaHome{
  build() {
    Scroll(){
      Column(){
        Text('首页')
      }
    }

  }
}

[!NOTE]

Scroll:滚动容器,仅能包含一个根节点

自定义组件:

使用@Component,并且包含build函数

在MainPage中调用:

@Entry
@Component
struct MainPage {

  @State pageIndex: number = 0;//页面索引

  build() {
    Tabs({barPosition:BarPosition.End}){
      TabContent(){
        //Text('首页')
        CinemaHome()
      }
      //.tabBar('首页')
      .tabBar(this.MyTabBuilder(TabID.HOME))
      ...

预览效果:

image-20250323102102453.png

2 轮播图

页面原型:

image-20250327100847885.png

1)图片硬编码效果

  build() {
    Scroll(){
      Column(){
        Text('首页')
          .fontWeight(FontWeight.Bold)
          .fontSize(22)
          .width('95%')
          .margin(20)
        //轮播图
        Swiper(){
          Image($r('app.media.movie1'))
            .movieImageStyle()
          Image($r('app.media.movie2'))
            .movieImageStyle()

          Image($r('app.media.movie3'))
            .movieImageStyle()

          Image($r('app.media.movie4'))
            .movieImageStyle()

          Image($r('app.media.movie5'))
            .movieImageStyle()

        }
        .autoPlay(true)
        .margin(10)
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f1f3f5')

  }

图片样式:

@Extend(Image)
function movieImageStyle(){
  .width('90%')
  .height(200)
  .borderRadius(12)

}

[!NOTE]

Swiper:滑块视图容器

api参考:

developer.huawei.com/consumer/cn…

预览效果:

image-20250324161400440.png

2)准备模型层数据

在model目录下新建arkts文件,命名为:CinemaHomeViewModel:

export class CinemaHomeViewModel{
  getMovieAdvImages(): Array<Resource> {
    let movieAdvImages: Resource[] = [
      $r('app.media.movie1'),
      $r('app.media.movie2'),
      $r('app.media.movie3'),
      $r('app.media.movie4'),
      $r('app.media.movie5'),
    ];

    return movieAdvImages;
  }
}

export default new CinemaHomeViewModel()

3)将轮播图数据源改成从模拟viewmodel中获取并foreach展示:

          ForEach(CinemaHomeViewModel.getMovieAdvImages(),(item:Resource)=>{
            Image(item).movieImageStyle()
          },(item:Resource)=>JSON.stringify(item))

[!NOTE]

ForEach:接口基于数组类型数据来进行循环渲染

developer.huawei.com/consumer/cn…

预览效果:

image-20250324170727975.png

3 常用功能

页面原型:

image-20250327100926988.png

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

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

export default interface  ItemBean{
  title:string
  icon?:Resource
}

2)模拟数据

在CinemaHomeViewModel中模拟数据,添加函数:

  //常用功能列表数据
  getItemBeanData():Array<ItemBean>{
    let items: ItemBean[] = [
      {title:'电影',icon:$r('app.media.dianying')},
      {title:'影院',icon:$r('app.media.yingyuan')},
      {title:'演出',icon:$r('app.media.yanchu')},
      {title:'玩乐',icon:$r('app.media.wanle')},
      {title:'演唱会',icon:$r('app.media.yanchanghui')},
      {title:'脱口秀',icon:$r('app.media.tuokouxiu')},
      {title:'密室',icon:$r('app.media.mishi')},
      {title:'门票',icon:$r('app.media.menpiao')}
    ]
    return items
  }

3)布局常用功能列表

使用2X4网格布局:

        //常见功能列表
        Grid(){
          ForEach(CinemaHomeViewModel.getItemBeanData(),(item:ItemBean) =>{
            GridItem(){
              Column(){
                Image(item.icon).width(50).height(50).borderRadius(8)
                Text(item.title).fontSize(14).margin({top:3})
              }

            }
          },(item:ItemBean) => 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-20250327084803294.png

4 热映电影

页面原型:

image-20250327100958012.png

1)准备数据

仍然使用ItemBean接口封装数据,在CinemaHomeViewModel中添加getHotMovie函数提供数据模拟:

  //热映电影数据
  getHotMovie(){
    let hotMovies: ItemBean[] = [
      {title:'恐龙日记',icon:$r('app.media.movie11')},
      {title:'胜券在握',icon:$r('app.media.movie12')},
      {title:'哈利波特',icon:$r('app.media.movie13')},
      {title:'海洋奇缘1',icon:$r('app.media.movie14')},
      {title:'海洋奇缘2',icon:$r('app.media.movie15')},
      {title:'疯狂外星人1',icon:$r('app.media.movie16')},
      {title:'疯狂外星人2',icon:$r('app.media.movie17')},
      {title:'那个不为人知的故事',icon:$r('app.media.movie18')}
    ]
    return hotMovies;

  }

2)使用列表布局页面

        //正在热映
        Text('正在热映')
          .width('95%')
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .margin(10)

        List(){
          ForEach(CinemaHomeViewModel.getHotMovie(),(item:ItemBean)=>{
            ListItem(){
              Column({space:5}){
                Image(item.icon).width(100).height(150).margin(10).borderRadius(8)
                Text(item.title)
                Button('购票').height(30)
              }
            }
          },(item:ItemBean) => 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-20250327093743518.png

5 即将上映

页面原型:

image-20250327104210764.png

1)准备数据

即将上映电影和正在热映电影处理方式类似,仍然使用ItemBean接口封装数据,在CinemaHomeViewModel中添加getTomorrowMovies函数提供数据模拟:

  //即将上映电影数据
  getTomorrowMovies(){
    let movies: ItemBean[] = [
      {title:'回家的你',icon:$r('app.media.movie19')},
      {title:'完美的日子',icon:$r('app.media.movie20')},
      {title:'周末狂飙',icon:$r('app.media.movie21')},
      {title:'热烈',icon:$r('app.media.movie22')},
      {title:'不完美逃脱',icon:$r('app.media.movie23')},
      {title:'折翼的天使',icon:$r('app.media.movie24')},
      {title:'狮子王',icon:$r('app.media.movie25')},
      {title:'好运来',icon:$r('app.media.movie26')}
    ]
    return movies;

  }

2)局部封装

即将上映电影和热映电影显示类似,此处采用@Builder封装,继续在CinemaHome中编码,将热映电影影片展示部分的代码拷贝到过来,并传递参数:

  @Builder tomorrowMovieCell(item:ItemBean){
    Column({space:5}){
      Image(item.icon).width(100).height(150).margin(10).borderRadius(8)
      Text(item.title)
      Button('预定').height(30)
    }
  }

3)展示即将上映电影

在CinemaHome中编码:

        //正在热映
        Text('即将上映')
          .width('95%')
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .margin(10)

        List(){
          ForEach(CinemaHomeViewModel.getTomorrowMovies(),(item:ItemBean)=>{
            ListItem(){
             this.tomorrowMovieCell(item)
              
            }
          },(item:ItemBean) => JSON.stringify(item))

        }.listDirection(Axis.Horizontal)
        .backgroundColor(Color.White)
        .margin({left:20,right:20})
        .padding(10)
        .borderRadius(12)

预览效果:

image-20250327104109503.png

6 为您推荐

界面原型:

image-20250328163630705.png

使用瀑布流布局:

WaterFlow:瀑布流容器

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

[!NOTE]

瀑布流布局组件参考:

developer.huawei.com/consumer/cn…

1)封装数据接口

在model下新建arkts文件,命名为:MovieItem,封装推荐的电影信息。

export interface MovieItem {
  id: number;
  imageUrl: Resource | string;
  name: string;
  score: number;//电影评分
  type: string ;//类型
  actors: string ;//演员,多个演员使用/分隔
  showTime: string;//上映时间

}

2)准备waterFlow初始数据

在CinemaHomeViewModel中添加loadWaterFlowData方法,为waterFlow准备初始数据:

loadWaterFlowData():MovieItem[] {
    let movieItems:MovieItem[] = [
      {
      id:1,
      imageUrl:$r('app.media.movie101'),
      name:'笑来运转',
      score: 9.0,
      type:'家庭',
      actors:'白客/乔杉/王大陆',
      showTime:'2024-11-30 09:00'
    },
      {
        id:2,
        imageUrl:$r('app.media.movie102'),
        name:'指环王',
        score: 9.0,
        type:'奇幻 冒险 动作 动画',
        actors:'盖亚·怀斯/卢克·帕斯夸尼洛',
        showTime:'2024-12-30 11:00'
      },
      {
        id:3,
        imageUrl:$r('app.media.movie103'),
        name:'海洋奇缘2',
        score: 9.0,
        type:'冒险 喜剧 动画',
        actors:'杰森·汉德/奥丽伊·卡瓦洛',
        showTime:'2024-11-29 09:00'
      },
      {
        id:4,
        imageUrl:$r('app.media.movie104'),
        name:'哈利·波特',
        score: 9.5,
        type:'奇幻 冒险',
        actors:'艾玛·沃特森/丹尼尔·雷德克',
        showTime:'2024-11-30 09:00'
      },
      {
        id:5,
        imageUrl:$r('app.media.movie105'),
        name:'恐龙日记',
        score: 9.0,
        type:'动画 冒险',
        actors:'小林由美子/楢桥 美纪/森川智之',
        showTime:'2024-11-23 09:00'
      },
      {
        id:6,
        imageUrl:$r('app.media.movie106'),
        name:'狮子王',
        score: 9.7,
        type:'动画 冒险',
        actors:'乔恩·费儒/唐纳德·格罗弗',
        showTime:'2024-11-30 09:00'
      },
      {
        id:7,
        imageUrl:$r('app.media.movie107'),
        name:'窗前明月,咣',
        score: 8.0,
        type:'喜剧',
        actors:'费翔/傅菁',
        showTime:'2024-12-31 09:00'
      },
      {
        id:8,
        imageUrl:$r('app.media.movie108'),
        name:'呼吸',
        score: 9.0,
        type:'冒险 剧情',
        actors:'朱颜曼滋/马伯骞',
        showTime:'2025-11-30 19:00'
      },
      {
        id:9,
        imageUrl:$r('app.media.movie109'),
        name:'今年二十二',
        score: 9.0,
        type:'纪录片',
        actors:'徐必成/倪萌',
        showTime:'2024-11-30 09:00'
      },
      {
        id:10,
        imageUrl:$r('app.media.movie110'),
        name:'哈利·波特',
        score: 9.0,
        type:'奇幻 冒险',
        actors:'乔恩·费儒/唐纳德·格罗弗',
        showTime:'2024-11-30 09:00'
      }
    ]

    return movieItems

}

3)模拟waterflow加载更多数据

在CinemaHomeViewModel中添加loadMoreWaterFlowData方法,模拟为waterFlow加载更多数据:

  //为waterflow加载更多数据
  loadMoreWaterFlowData(): MovieItem[] {
  let movieItems: MovieItem[] = [
    {
      id: 11,
      imageUrl: $r('app.media.movie111'),
      name: '大突围',
      score: 9.1,
      type: '战争 青春 动作',
      actors: '任天野/敖子逸/艾米',
      showTime: '2024-11-29 12:00'
    },
    {
      id: 12,
      imageUrl: $r('app.media.movie112'),
      name: '角斗士II',
      score: 9.1,
      type: '冒险 动作 动画',
      actors: '保罗·麦斯卡/康妮·尼尔森',
      showTime: '2024-12-30 11:00'
    },
    {
      id: 13,
      imageUrl: $r('app.media.movie113'),
      name: '爱你很久很久',
      score: 7.9,
      type: '爱情 青春 喜剧',
      actors: '李沐/曹佑宁',
      showTime: '2024-11-29 09:00'
    },
    {
      id: 14,
      imageUrl: $r('app.media.movie114'),
      name: '猎金游戏',
      score: 9.5,
      type: '剧情',
      actors: '刘德华/倪妮',
      showTime: '2025-05-01 09:00'
    },
    {
      id: 15,
      imageUrl: $r('app.media.movie115'),
      name: '星际宝贝史迪奇',
      score: 9.0,
      type: '剧情 喜剧 动作 科幻',
      actors: '玛雅·凯洛哈/克里斯·桑德斯/西德尼·阿古顿',
      showTime: '2025-11-23 09:00'
    },
    {
      id: 16,
      imageUrl: $r('app.media.movie116'),
      name: '天才游戏',
      score: 8.7,
      type: '悬疑',
      actors: '彭昱畅/丁禹兮',
      showTime: '2025-11-20 09:00'
    },
    {
      id: 17,
      imageUrl: $r('app.media.movie117'),
      name: '侏罗纪世界',
      score: 8.0,
      type: '动作 冒险 科幻',
      actors: '科林·特雷沃罗/克里斯·帕拉特',
      showTime: '2024-12-31 09:00'
    },
    {
      id: 18,
      imageUrl: $r('app.media.movie118'),
      name: '驯龙高手',
      score: 9.0,
      type: '剧情 喜剧 奇幻',
      actors: '梅森·泰晤士/妮可·帕克',
      showTime: '2025-10-10 19:00'
    },
    {
      id: 19,
      imageUrl: $r('app.media.movie119'),
      name: '火星计划',
      score: 9.6,
      type: '喜剧 剧情',
      actors: '贾冰/范丞丞',
      showTime: '2025-05-30 09:00'
    },
    {
      id: 20,
      imageUrl: $r('app.media.movie120'),
      name: '有朵云像你',
      score: 8.0,
      type: '爱情 剧情 奇幻',
      actors: '屈楚萧/王子文',
      showTime: '2025-11-30 09:00'
    }
  ]

  return movieItems

}

4)实现数据源接口

在model下新建arkts文件,命名为:MovieHomeWaterFlowDataSource.ets,用于影院首页瀑布流组件数据源接口实现:

实现数据源接口中的相关方法,并添加添加更多数据,刷新数据,删除数据对应的方法。

export class MovieHomeWaterFlowDataSource implements IDataSource {
  public dataArray: MovieItem[] = [];
  private listeners: DataChangeListener [] = [];

  constructor(dataArray: MovieItem[]) {
    for (let i = 0; i < dataArray.length; i++) {
      this.dataArray.push(dataArray[i]);
    }
  }

  totalCount(): number {
    return this.dataArray.length;
  }

  getData(index: number): MovieItem {
    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);
    })
  }

  nodifyDataDelete(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataDelete(index);
    })
  }

  public addMoreData(movieDataArray: MovieItem[]): void {
    let idx = this.dataArray.length;
    for (let element of movieDataArray) {
      element.id = idx + 1;
      this.dataArray.push(element);
      idx++;

    }
    this.notifyDataAdd(this.dataArray.length -1)
  }

  public refreshData(movieDataArray: MovieItem[]):void{
    this.dataArray = [];
    for (let element of movieDataArray) {
      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.nodifyDataDelete(delIdx);
       break;

     }

    }
  }
}

5)瀑布流UI

在CinemaHome中编码,首先定义数据源,加载条状态控制及滚动条控制,缓存记录数等相关变量:

  @State datasource: MovieHomeWaterFlowDataSource =
    new MovieHomeWaterFlowDataSource(CinemaHomeViewModel.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: MovieItem, 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)
          if((last + this.cachedCount) === this.datasource.totalCount()){//缓存2个,总数是10
            setTimeout(()=>{
              this.datasource.addMoreData(CinemaHomeViewModel.loadMoreWaterFlowData())
            },500)
          }

        })

影片单元样式:

  @Builder waterFlowItemCell(item: MovieItem){
    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

          })

        Row(){
          Text(item.type)
            .fontSize(12)
            .opacity(0.6)
          Text(item.score.toFixed(1)+'分')
            .fontColor(Color.Orange)
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        Text('演员:'+item.actors)
          .fontSize(12)
          .opacity(0.6)
          .width('100%')
          .textOverflow({overflow:TextOverflow.Ellipsis})
          .maxLines(1)
        Text('上映时间:')
          .fontSize(12)
          .opacity(0.6)
          .width('100%')
        Text(item.showTime)
          .fontSize(12)
          .opacity(0.6)
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .margin({
            bottom:10
          })
      }
      .margin(5)



    }
    .backgroundColor(Color.White)
    .borderRadius(12)
  }

预览效果:

image-20250328163648218.png

参考

代码仓

gitee.com/snowyvalley…

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