边玩边学鸿蒙影视App开发之核心页面实现 #跟着猫哥学鸿蒙
在鸿蒙应用开发中,页面是用户与应用交互的主要界面。一个完整的应用通常由多个功能页面组成,这些页面通过导航相互连接,共同构成用户体验。
在"爱影家"APP中,主要实现了首页、电影列表页、电影详情页、搜索页面和视频播放页面等核心页面。
课程地址:https://blog.csdn.net/yyz_1987/article/details/153418477
本次学习几笔将详细介绍鸿蒙应用核心页面的实现方法,包括数据管理、UI布局、用户交互以及页面导航等方面,帮助开发者掌握鸿蒙应用页面开发的完整流程。
二、首页轮播图实现详解
1. 数据源设计
在实现轮播图之前,首先需要设计一个高效的数据源。在鸿蒙开发中,可以实现IDataSource接口来创建自定义数据源。
class BasicDataSource<T> implements IDataSource {
private listeners: DataChangeListener[] = [];
private originDataArray: T[] = [];
totalCount(): number {
return this.originDataArray.length;
}
getData(index: number): T {
return this.originDataArray[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener);
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
this.listeners.slice(pos, 1);
}
}
notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded();
})
}
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index);
})
}
}
2. 实现懒加载数据源
为了提升轮播图的性能,我们可以扩展基础数据源,实现懒加载功能:
class SwiperDataSource<T> extends BasicDataSource<T> {
private dataArray: T[] = [];
totalCount(): number {
return this.dataArray.length;
}
getData(index: number): T {
return this.dataArray[index];
}
// 在列表末尾添加数据并通知监听器
pushData(data: T): void {
this.dataArray.push(data);
this.notifyDataAdd(this.dataArray.length - 1);
}
// 重载数据
reloadData(): void {
this.dataArray = [];
this.notifyDataReload();
}
}
3. 轮播图组件实现
使用Swiper组件和LazyForEach实现高性能的轮播图:
@Component
struct BannerComponent {
private dataSource: SwiperDataSource<BannerItem> = new SwiperDataSource<BannerItem>();
private bannerList: BannerItem[] = [];
@State currentIndex: number = 0;
async aboutToAppear() {
// 获取轮播图数据
try {
const result = await getBannerList();
this.bannerList = result.data;
// 更新数据源
this.bannerList.forEach(item => {
this.dataSource.pushData(item);
});
} catch (error) {
console.error('Failed to load banner:', error);
}
}
onBannerClick(id: string) {
// 跳转到电影详情页
router.push({
uri: 'pages/detail/DetailPage',
params: { movieId: id }
});
}
build() {
Column() {
Swiper({
index: 0,
autoPlay: true,
interval: 3000,
indicatorStyle: {
size: 8,
selectedSize: 8,
selectedColor: Color.White,
color: Color.Gray
}
})
.width('100%')
.height(200)
.onChange((index: number) => {
this.currentIndex = index;
})
{
LazyForEach(this.dataSource, (item: BannerItem) => {
Stack() {
Image(item.imageUrl)
.width('100%')
.height('100%')
.objectFit(ImageFit.Cover)
// 标题遮罩
Flex() {
Text(item.title)
.fontSize(16)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
}
.position({ bottom: 0 })
.width('100%')
.padding(10)
.backgroundColor('rgba(0, 0, 0, 0.5)')
}
.onClick(() => this.onBannerClick(item.movieId))
}, (item: BannerItem) => item.movieId)
}
// 自定义指示器(可选)
Row() {
ForEach(this.bannerList, (_, index) => {
Column() {
Text()
.width(index === this.currentIndex ? 20 : 8)
.height(8)
.backgroundColor(index === this.currentIndex ? Color.White : Color.Gray)
.borderRadius(4)
}
.margin({ left: 5, right: 5 })
})
}
.margin({ top: 10 })
}
}
}
三、电影详情页实现
1. 页面布局设计
电影详情页需要展示丰富的信息,包括电影海报、标题、评分、简介、演职员信息等。合理的布局设计对于提升用户体验至关重要。
@Component
struct DetailPage {
@State detailData: DetailMvResp | null = null;
private srcData: MvSourceResp | null = null;
private movieId: string = '';
// 控制简介展开/收起
@State isExpanded: boolean = false;
async onPageShow() {
// 获取页面参数
const params = router.getParams();
if (params && params.movieId) {
this.movieId = params.movieId as string;
await this.loadMovieData();
}
}
async loadMovieData() {
try {
// 获取电影详情
const detailResult = await getDetailMv(this.movieId);
this.detailData = detailResult;
// 获取电影播放源
const srcResult = await getMovieSrc(this.movieId);
this.srcData = srcResult;
} catch (error) {
console.error('Failed to load movie data:', error);
}
}
toggleDescription() {
this.isExpanded = !this.isExpanded;
}
navigateToPlayer() {
if (this.srcData?.sources && this.srcData.sources.length > 0) {
router.push({
uri: 'pages/player/PlayerPage',
params: {
sources: this.srcData.sources,
title: this.detailData?.title
}
});
}
}
build() {
NavDestination() {
Scroll() {
Column() {
if (this.detailData) {
// 电影基本信息区域
Row() {
Image(this.detailData.images)
.width(120)
.borderRadius(5)
Column({ space: 8 }) {
Text(this.detailData.title)
.fontSize(18)
.fontWeight(FontWeight.Bold)
Text(`${this.detailData.year} · ${this.detailData.genre}`)
.fontSize(14)
.fontColor(Color.Gray)
// 评分区域
Row() {
Text(`${this.detailData.rating}`)
.fontSize(20)
.fontColor(Color.Red)
.fontWeight(FontWeight.Bold)
Rating({
rating: this.detailData.rating / 2,
maxRating: 5,
numStars: 5,
indicator: true
})
}
// 想看/看过统计
Row() {
Badge({
count: this.detailData.wish_count,
maxCount: 10000,
position: BadgePosition.RightTop,
style: { badgeSize: 22, badgeColor: '#fffab52a' }
}) {
Row() {
Text() {
SymbolSpan($r('sys.symbol.heart'))
.fontWeight(FontWeight.Lighter)
.fontSize(32)
.fontColor(['#fffab52a'])
}
Text('想看')
}
.backgroundColor('#f8f4f5')
.borderRadius(5)
.padding(5)
}
.padding(8)
}
}
.flexGrow(1)
.marginLeft(10)
}
.padding(15)
// 操作按钮
Button('播放', { type: ButtonType.Capsule })
.onClick(() => this.navigateToPlayer())
.backgroundColor(Color.Red)
.fontColor(Color.White)
.padding({ left: 40, right: 40, top: 10, bottom: 10 })
.margin({ top: 10, bottom: 20 })
// 电影简介
Column() {
Text('剧情简介')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 10 })
Text(this.detailData.summary)
.fontSize(14)
.lineHeight(22)
.maxLines(this.isExpanded ? undefined : 3)
Text(this.isExpanded ? '收起' : '展开')
.fontSize(14)
.fontColor(Color.Blue)
.margin({ top: 5 })
.onClick(() => this.toggleDescription())
}
.padding(15)
.backgroundColor(Color.White)
.marginBottom(10)
// 演职员信息
Column() {
Text('演职员')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 10 })
if (this.detailData.casts && this.detailData.casts.length > 0) {
List() {
ForEach(this.detailData.casts, (cast: DetailMvRespCast) => {
ListItem() {
Column() {
Image(cast.avatar)
.width(80)
.height(100)
.borderRadius(5)
Text(cast.name)
.fontSize(14)
.margin({ top: 5 })
Text(cast.role)
.fontSize(12)
.fontColor(Color.Gray)
}
.padding(5)
}
}, (cast: DetailMvRespCast) => cast.id)
}
.listDirection(Axis.Horizontal)
.showsHorizontalScrollbar(false)
}
}
.padding(15)
.backgroundColor(Color.White)
} else {
// 加载状态
LoadingProgress()
.height(50)
.color(Color.Blue)
}
}
}
}
.title('电影详情')
}
}
2. 组件复用与封装
在实现详情页时,我们可以将一些可复用的UI元素封装成独立的组件,提高代码的可维护性。
// 评分组件
@Component
struct RatingBar {
private score: number;
constructor(score: number) {
this.score = score;
}
build() {
Row() {
Text(`${this.score}`)
.fontSize(20)
.fontColor(Color.Red)
.fontWeight(FontWeight.Bold)
Rating({
rating: this.score / 2,
maxRating: 5,
numStars: 5,
indicator: true
})
}
}
}
// 演员卡片组件
@Component
struct CastCard {
private cast: DetailMvRespCast;
constructor(cast: DetailMvRespCast) {
this.cast = cast;
}
build() {
Column() {
Image(this.cast.avatar)
.width(80)
.height(100)
.borderRadius(5)
Text(this.cast.name)
.fontSize(14)
.margin({ top: 5 })
Text(this.cast.role)
.fontSize(12)
.fontColor(Color.Gray)
}
.padding(5)
}
}
四、搜索页面实现
1. 搜索功能实现
搜索页面是用户查找特定电影的重要入口。我们需要实现实时搜索、搜索历史记录、热门搜索等功能。
@Component
struct SearchPage {
@State keyword: string = '';
@State searchResults: MovieSearchItem[] = [];
@State isSearching: boolean = false;
@State historyKeywords: string[] = [];
@State hotKeywords: string[] = ['热门', '最新', '科幻', '动作', '喜剧'];
// 防抖的搜索函数
private debouncedSearch = debounce(async (text: string) => {
if (text.trim().length > 0) {
await this.performSearch(text);
} else {
this.searchResults = [];
}
}, 500);
onPageShow() {
// 加载搜索历史
this.loadSearchHistory();
}
async performSearch(text: string) {
this.isSearching = true;
try {
const result = await searchMovies(text);
this.searchResults = result.data;
// 保存到搜索历史
this.saveToHistory(text);
} catch (error) {
console.error('Search failed:', error);
} finally {
this.isSearching = false;
}
}
saveToHistory(keyword: string) {
// 去重
const index = this.historyKeywords.indexOf(keyword);
if (index > -1) {
this.historyKeywords.splice(index, 1);
}
// 添加到开头
this.historyKeywords.unshift(keyword);
// 最多保存10条
if (this.historyKeywords.length > 10) {
this.historyKeywords.pop();
}
// 保存到存储
// TODO: 实现存储功能
}
loadSearchHistory() {
// TODO: 从存储加载历史记录
}
clearHistory() {
this.historyKeywords = [];
// TODO: 清空存储
}
onKeywordChange(event: TextInputChangeEvent) {
this.keyword = event.text;
this.debouncedSearch(this.keyword);
}
onSearchSubmit() {
if (this.keyword.trim().length > 0) {
this.performSearch(this.keyword);
}
}
navigateToDetail(id: string) {
router.push({
uri: 'pages/detail/DetailPage',
params: { movieId: id }
});
}
build() {
NavDestination() {
Column() {
// 搜索框
Row() {
Search({
value: this.keyword,
placeholder: '搜索电影',
onSubmit: () => this.onSearchSubmit(),
onChange: (value: string) => this.keyword = value
})
.backgroundColor('#f0f0f0')
.textFont({ size: 16 })
.height(40)
.width('85%')
Button('搜索')
.onClick(() => this.onSearchSubmit())
.marginLeft(10)
}
.padding(10)
// 内容区域
if (this.keyword.trim().length === 0) {
// 搜索前显示历史和热门
Column() {
// 搜索历史
if (this.historyKeywords.length > 0) {
Row() {
Text('搜索历史')
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text('清空')
.fontSize(14)
.fontColor(Color.Gray)
.onClick(() => this.clearHistory())
}
.justifyContent(FlexAlign.SpaceBetween)
.padding(10)
FlowLayout() {
ForEach(this.historyKeywords, (item: string) => {
Text(item)
.fontSize(14)
.padding({ left: 15, right: 15, top: 5, bottom: 5 })
.backgroundColor('#f0f0f0')
.borderRadius(15)
.margin(5)
.onClick(() => {
this.keyword = item;
this.performSearch(item);
})
}, (item: string) => item)
}
}
// 热门搜索
Text('热门搜索')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.padding(10)
.alignSelf(ItemAlign.Start)
FlowLayout() {
ForEach(this.hotKeywords, (item: string) => {
Text(item)
.fontSize(14)
.padding({ left: 15, right: 15, top: 5, bottom: 5 })
.backgroundColor('#f0f0f0')
.borderRadius(15)
.margin(5)
.onClick(() => {
this.keyword = item;
this.performSearch(item);
})
}, (item: string) => item)
}
}
} else if (this.isSearching) {
// 搜索中
LoadingProgress()
.height(50)
.color(Color.Blue)
} else {
// 搜索结果
if (this.searchResults.length > 0) {
List() {
ForEach(this.searchResults, (item: MovieSearchItem) => {
ListItem() {
Row() {
Image(item.posterUrl)
.width(80)
.height(120)
.borderRadius(5)
Column({ space: 8 })
.marginLeft(10)
.flexGrow(1)
{
Text(item.title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text(`${item.year} · ${item.genre}`)
.fontSize(14)
.fontColor(Color.Gray)
Rating({
rating: item.rating / 2,
maxRating: 5,
numStars: 5,
indicator: true
})
}
}
.padding(15)
.onClick(() => this.navigateToDetail(item.id))
}
}, (item: MovieSearchItem) => item.id)
}
.divider({ strokeWidth: 1, color: '#f0f0f0' })
} else {
Text('没有找到相关电影')
.fontSize(16)
.fontColor(Color.Gray)
.marginTop(50)
}
}
}
}
.title('搜索')
}
}
2. 防抖函数实现
为了避免频繁搜索请求,我们可以实现一个防抖函数:
function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: number | null = null;
return function(...args: Parameters<T>) {
if (timeout !== null) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
func.apply(this, args);
}, wait);
};
}
五、视频播放页面实现
1. 视频播放核心功能
视频播放是影视APP的核心功能,我们需要实现视频播放、暂停、全屏切换、剧集选择等功能。
@Component
struct PlayerPage {
@State isPlaying: boolean = false;
@State isFullScreen: boolean = false;
@State currentSourceIndex: number = 0;
private sources: string[] = [];
private title: string = '';
private videoController: VideoController = new VideoController();
onPageShow() {
// 获取页面参数
const params = router.getParams();
if (params) {
this.sources = params.sources as string[] || [];
this.title = params.title as string || '视频播放';
}
// 自动播放第一个视频
if (this.sources.length > 0) {
this.videoController.play();
this.isPlaying = true;
}
}
togglePlay() {
if (this.isPlaying) {
this.videoController.pause();
} else {
this.videoController.play();
}
this.isPlaying = !this.isPlaying;
}
toggleFullScreen() {
// 切换全屏模式
this.isFullScreen = !this.isFullScreen;
// TODO: 实现屏幕方向切换
}
onChangeSource(index: number) {
this.currentSourceIndex = index;
// 重置播放状态
this.videoController.start();
this.isPlaying = true;
}
build() {
NavDestination() {
Column() {
if (this.sources.length > 0) {
// 视频播放区域
Stack() {
Video({
src: this.sources[this.currentSourceIndex],
currentProgressRate: PlaybackSpeed.Speed_Forward_1_00_X,
controller: this.videoController
})
.width('100%')
.height(this.isFullScreen ? '100%' : 300)
.autoPlay(true)
.onStart(() => {
console.log('Video started');
this.isPlaying = true;
})
.onPause(() => {
console.log('Video paused');
this.isPlaying = false;
})
.onFinish(() => {
console.log('Video finished');
this.isPlaying = false;
})
// 自定义播放控制
Row() {
Button() {
Image(this.isPlaying ? '/assets/pause.png' : '/assets/play.png')
.width(24)
.height(24)
}
.onClick(() => this.togglePlay())
.backgroundColor('rgba(0, 0, 0, 0.5)')
.borderRadius(20)
.width(40)
.height(40)
Button() {
Image('/assets/fullscreen.png')
.width(24)
.height(24)
}
.onClick(() => this.toggleFullScreen())
.backgroundColor('rgba(0, 0, 0, 0.5)')
.borderRadius(20)
.width(40)
.height(40)
.marginLeft(10)
}
.position({ bottom: 20, right: 20 })
}
// 剧集选择
if (this.sources.length > 1) {
Column() {
Text('选择剧集')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 10 })
FlowLayout() {
ForEach(this.sources, (_, index: number) => {
Text(`第${index + 1}集`)
.fontSize(14)
.padding({ left: 15, right: 15, top: 5, bottom: 5 })
.backgroundColor(index === this.currentSourceIndex ? Color.Red : '#f0f0f0')
.fontColor(index === this.currentSourceIndex ? Color.White : Color.Black)
.borderRadius(15)
.margin(5)
.onClick(() => this.onChangeSource(index))
}, (_, index: number) => index.toString())
}
}
.padding(15)
}
} else {
Text('暂无播放源')
.fontSize(16)
.fontColor(Color.Gray)
.marginTop(50)
}
}
}
.title(this.title)
}
}
2. 全屏播放实现
全屏播放是视频播放器的重要功能,我们可以使用鸿蒙的窗口管理API来实现:
import { window } from '@kit.ArkUI';
// 在toggleFullScreen方法中添加
async function toggleFullScreen() {
const windowClass = await window.getLastWindow(getContext(this));
if (this.isFullScreen) {
// 退出全屏
await windowClass.setWindowMode(window.WindowMode.WINDOW_MODE_UNDEFINED);
await windowClass.setPreferredOrientation(window.Orientation.PORTRAIT);
} else {
// 进入全屏
await windowClass.setWindowMode(window.WindowMode.FULLSCREEN);
await windowClass.setPreferredOrientation(window.Orientation.LANDSCAPE);
}
this.isFullScreen = !this.isFullScreen;
}
六、页面导航与参数传递
1. 基本导航功能
鸿蒙提供了router模块用于页面导航:
import router from '@ohos.router';
// 跳转到详情页
router.push({
uri: 'pages/detail/DetailPage',
params: {
movieId: '1292052',
title: '肖申克的救赎'
}
});
// 返回上一页
router.back();
// 替换当前页面(不保留历史记录)
router.replace({
uri: 'pages/login/LoginPage'
});
2. 参数接收
在目标页面中,可以通过router.getParams()方法获取传递的参数:
onPageShow() {
const params = router.getParams();
if (params) {
this.movieId = params.movieId as string;
this.title = params.title as string || '';
// 使用参数加载数据
this.loadMovieData();
}
}
3. 导航动画
鸿蒙支持自定义导航动画,提升用户体验:
router.push({
uri: 'pages/detail/DetailPage',
params: { movieId: '1292052' },
animation: {
type: router.AnimationType.Slide,
duration: 300,
curve: router.AnimationCurve.EaseOut
}
});
七、总结
鸿蒙应用页面开发是一个综合性的工作,涉及UI布局、数据处理、用户交互等多个方面。通过本文的学习,了解了鸿蒙应用核心页面的实现方法,包括首页轮播图、电影详情页、搜索页面和视频播放页面等。
在实际开发中,需要注重以下几点:
- 性能优化:使用懒加载、缓存等技术提升页面性能
- 用户体验:合理的布局设计、流畅的动画效果、清晰的交互反馈
- 代码复用:将可复用的UI元素封装成组件,提高代码的可维护性
- 错误处理:完善的错误处理机制,确保应用的稳定性
通过不断的实践和总结,我们能够更好地掌握鸿蒙应用页面开发的技巧,构建出高质量的鸿蒙应用。
更多推荐
所有评论(0)