任务
需求
构建主页,包括电影轮播图、常用功能、正在热映、即将上映,为您推荐各部分。
界面原型
轮播及正在热映:
即将上映:
为您推荐:
涉及知识点
-
Scroll:滚动容器
-
Swiper:滑块视图容器(轮播组件)
-
ForEach:循环渲染
-
LazyForEach:懒加载
-
Grid:网格布局
-
List:列表组件
-
WaterFlow:瀑布流容器
-
数据源监听
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))
...
预览效果:
2 轮播图
页面原型:
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参考:
预览效果:
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:接口基于数组类型数据来进行循环渲染
预览效果:
3 常用功能
页面原型:
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:网格容器,由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局
组件参考:
预览效果:
4 热映电影
页面原型:
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),设置列表为横向
组件参考:
预览效果:
5 即将上映
页面原型:
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)
预览效果:
6 为您推荐
界面原型:
使用瀑布流布局:
WaterFlow:瀑布流容器
由“行”和“列”分割的单元格所组成,通过容器自身的排列规则,将不同大小的“项目”自上而下,如瀑布般紧密布局。
[!NOTE]
瀑布流布局组件参考:
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)
}
预览效果:
参考
代码仓
##鸿蒙应用开发 ##休闲娱乐