HarmonyOS NEXT边学边玩:从零实现一个影视App(六、视频播放页的实现)

在HarmonyOS NEXT中,ArkUI是一个非常强大的UI框架,能够帮助开发者快速构建出美观且功能丰富的用户界面。本文将详细介绍如何使用ArkUI实现一个影视App的视频播放页面。将从零开始,逐步构建一个功能完善的视频播放页面,并解释每一部分的代码实现。

1. 项目结构

在开始之前,先来看一下项目的结构。我的项目结构如下:

src/
├── common/
│   ├── bean/
│   │   └── ApiTypes.ts
│   └── dialog/
│       └── EpisodeDialogView.ts
├── pages/
│   └── VideoPlayerPage.ts
  • ApiTypes.ts 定义了视频数据的类型。
  • EpisodeDialogView.ts 是一个自定义的剧集选择对话框组件。
  • VideoPlayerPage.ts 是视频播放页面的主文件。

2. 视频播放页面的实现

2.1 引入必要的模块

首先,需要引入一些必要的模块和组件:

typescript 复制代码
import { VideoItem } from "../../common/bean/ApiTypes";
import { window } from "@kit.ArkUI";
import { common } from "@kit.AbilityKit";
import { EpisodeDialogView } from "../../common/dialog/EpisodeDialogView";
  • VideoItem 是定义的一个视频数据类型的接口。
  • windowcommon 是HarmonyOS提供的系统模块,用于控制窗口和UI能力。
  • EpisodeDialogView 是自定义的剧集选择对话框组件。

2.2 构建视频播放页面

使用 @Builder 装饰器来构建视频播放页面:

typescript 复制代码
@Builder
export function VideoPlayerPageBuilder() {
  VideoPlayer()
}

VideoPlayerPageBuilder 是一个构建函数,它调用了 VideoPlayer 组件来构建整个页面。

2.3 视频播放组件

接下来,定义 VideoPlayer 组件:

typescript 复制代码
@Component
struct VideoPlayer {
  @State title: string = '';
  private controller: VideoController | undefined;
  @State previewUri: Resource = $r('app.media.play_circle_fill');
  @State videoSrc: string = 'http://staticvip.iyuba.cn/video/small/202412/1009544_c.mp4'; // 使用时请替换为实际视频加载网址
  @State tvUrls?: string[] = [];

  @State isTv: boolean = false;
  @State tvIndex: number = 0;
  private description: string = '';
  private rawTitle: string = '';
  private isToggle = false;
  @State toggleText: string = '';
  @State toggleBtn: string = '展开';
  • @State 是ArkUI中的状态管理装饰器,用于管理组件的状态。
  • controller 是视频播放器的控制器,用于控制视频的播放、暂停等操作。
  • previewUri 是视频的预览图资源。
  • videoSrc 是视频的播放地址。
  • tvUrls 是电视剧的剧集列表。
  • isTv 用于判断当前播放的是否是电视剧。
  • tvIndex 是当前播放的剧集索引。
  • description 是视频的简介。
  • rawTitle 是视频的原始标题。
  • isToggle 用于控制简介的展开和收起状态。
  • toggleText 是简介的显示文本。
  • toggleBtn 是简介展开/收起按钮的文本。

2.4 页面生命周期

aboutToAppear 生命周期函数中,可以进行一些初始化操作:

typescript 复制代码
aboutToAppear() {
  // 初始化操作
}

2.5 屏幕方向切换

定义了一个 changeOrientation 方法,用于切换屏幕方向:

typescript 复制代码
private changeOrientation(isLandscape: boolean) {
  let context = getContext(this) as common.UIAbilityContext;
  window.getLastWindow(context).then((lastWindow) => {
    lastWindow.setPreferredOrientation(isLandscape ? window.Orientation.LANDSCAPE : window.Orientation.PORTRAIT);
  });
}
  • getContext 用于获取当前组件的上下文。
  • window.getLastWindow 获取当前窗口,并设置其方向为横屏或竖屏。

2.6 剧集选择对话框

定义了一个剧集选择对话框,并为其设置了回调函数:

typescript 复制代码
episodeDialogController: CustomDialogController = new CustomDialogController({
  builder: EpisodeDialogView({
    current: this.tvIndex,
    tvUrls: this.tvUrls,
    clickCallback: (item: string, index: number) => { this.onDialogClickCallback(item, index) }
  }),
  alignment: DialogAlignment.Bottom,
  offset: { dx: 0, dy: -30 },
  customStyle: true
});

private onDialogClickCallback(item: string, index: number) {
  this.videoSrc = item;
  this.tvIndex = index;
  this.title = this.rawTitle + ' 第' + (index + 1) + '集';
  this.episodeDialogController.close();
  this.controller?.start();
}
  • CustomDialogController 是自定义对话框的控制器。
  • EpisodeDialogView 是剧集选择对话框的视图组件。
  • onDialogClickCallback 是剧集选择后的回调函数,用于更新视频播放地址和标题。

剧集选择框的实现:

typescript 复制代码
/**
 * @author: 猫哥
 * @date: 2025/1/15 0:30
 * @description:
 * @version:
 */
/**
 * 电视剧集数选择对话框
 */
@Preview
@CustomDialog
export struct EpisodeDialogView {
  controller: CustomDialogController
  @Prop current: number
  @Prop tvUrls?:string[] = []

  //确认按钮回调
  clickCallback?: (item:string,idx:number) => void

  // 添加一个方法来获取按钮的背景颜色
  private getButtonColor(idx: number): Color {
    if (this.current === idx) {
      return Color.Blue // 当前选中的按钮背景色为蓝色
    } else {
      return Color.White // 其他按钮背景色为白色
    }
  }
  build() {
    Column({space: 10}){
      Grid() {
        ForEach(this.tvUrls, (item: string,idx) => {
          GridItem() {
            Button('第'+(idx+1)+'集', { buttonStyle: ButtonStyleMode.NORMAL, role: ButtonRole.NORMAL })
              .borderRadius(5)
              .fontColor('#fffab52a')
              .width(80)
              .height(40)
              .padding(5)
              .backgroundColor(this.getButtonColor(idx))
              .onClick(() => {
                this.clickCallback?.(item,idx)
              })
          }
        }, (item: string) => item.toString())
      }
      .width('100%')
      .scrollBar(BarState.Off)
      .columnsTemplate('1fr 1fr 1fr 1fr')
      .columnsGap(4)
      .rowsGap(4)
      .height(300)
    }.width('98%').height('40%')
    .backgroundColor(Color.White)
    .padding(20).borderRadius(20)
  }
}

2.7 按钮背景颜色

定义了一个方法来获取按钮的背景颜色:

typescript 复制代码
private getButtonColor(idx: number): Color {
  if (this.tvIndex === idx) {
    return Color.Blue; // 当前选中的按钮背景色为蓝色
  } else {
    return Color.White; // 其他按钮背景色为白色
  }
}

2.8 构建页面布局

最后,使用 build 方法来构建页面的布局:

typescript 复制代码
build() {
  NavDestination() {
    Column() {
      Row() {
        Stack() {
          Video({
            src: this.videoSrc,
            previewUri: this.previewUri,
            controller: this.controller
          })
            .width('100%')
            .muted(false) //设置是否静音
            .controls(true) //设置是否显示默认控制条
            .autoPlay(true) //设置是否自动播放
            .loop(false) //设置是否循环播放
            .objectFit(ImageFit.Contain) //设置视频适配模式
            .onError(() => {          //失败事件回调
              console.info("Video error.");
            })
            .onFullscreenChange(event => {
              if (event.fullscreen) {
                this.changeOrientation(true);
              } else {
                this.changeOrientation(false);
              }
            })
            .zIndex(1);

          Text(this.title).fontColor(Color.White).width('100%').padding(5)
            .alignSelf(ItemAlign.Start).margin({ bottom: 280 }).zIndex(2);
        }
      }.width('100%').height('40%');

      Column() {
        Text('简介').fontSize(18).padding({ bottom: 10 }).fontWeight(FontWeight.Bold).alignSelf(ItemAlign.Start);

        Text(this.toggleText).fontSize(14).lineHeight(20).alignSelf(ItemAlign.Start);
        Text(this.toggleBtn).fontSize(14).fontColor(Color.Gray).padding(10).alignSelf(ItemAlign.End).onClick(() => {
          this.isToggle = !this.isToggle;
          if (this.isToggle) {
            this.toggleBtn = '收起';
            this.toggleText = this.description;
          } else {
            this.toggleBtn = '展开';
            this.toggleText = this.description.substring(0, 100) + '...';
          }
        });
      }.padding(10);

      if (this.isTv) {
        Flex({
          direction: FlexDirection.Row,
          justifyContent: FlexAlign.SpaceBetween,
          alignContent: FlexAlign.SpaceBetween
        }) {
          Text('选集').fontSize(18).fontWeight(FontWeight.Bold);
          SymbolGlyph($r('sys.symbol.more'))
            .fontWeight(FontWeight.Lighter)
            .fontSize(32)
            .fontColor(['#fffab52a'])
            .onClick(() => {
              this.episodeDialogController.open();
            });
        }.padding(10);

        Scroll() {
          Row({ space: 10 }) {
            ForEach(this.tvUrls, (item: string, idx) => {
              Button('第' + (idx + 1) + '集', { buttonStyle: ButtonStyleMode.NORMAL, role: ButtonRole.NORMAL })
                .borderRadius(5)
                .fontColor('#fffab52a')
                .width(80)
                .height(40)
                .padding(5)
                .backgroundColor(this.getButtonColor(idx))
                .onClick(() => {
                  this.videoSrc = item;
                  this.tvIndex = idx;
                  this.title = this.rawTitle + ' 第' + (idx + 1) + '集';
                  this.controller?.start();
                });
            });
          }.alignItems(VerticalAlign.Center).padding(10);
        }.scrollable(ScrollDirection.Horizontal);
      }
    }.width('100%');
  }
  .width("100%")
  .height("100%")
  .onReady(ctx => {
    interface params {
      item: VideoItem;
    }
    let par = ctx.pathInfo.param as params;
    this.videoSrc = par.item.video;
    this.rawTitle = par.item.title;
    this.title = this.rawTitle;
    this.description = par.item.desc;
    this.tvUrls = par.item.tvurls;
    if (this.tvUrls?.length ?? 0 > 0) {
      this.isTv = true;
    }
    this.tvIndex = 0;
    this.toggleText = this.description.substring(0, 100) + '...';
  })
  .onShown(() => {
    console.info('VideoPlayer onShown');
  });
}
  • NavDestination 是导航目的地组件,用于定义页面的导航行为。
  • ColumnRow 是布局组件,用于构建页面的布局。
  • Video 是视频播放组件,用于播放视频。
  • Text 是文本组件,用于显示标题和简介。
  • Button 是按钮组件,用于选择剧集。
  • Scroll 是滚动组件,用于实现剧集列表的横向滚动。

3. 总结

通过以上步骤,实现了一个功能完善的视频播放页面。这个页面不仅支持视频的播放、暂停、全屏等基本功能,还支持电视剧的剧集选择和简介的展开/收起。希望这篇文章能够帮助你更好地理解如何使用HarmonyOS NEXT和ArkUI来构建一个影视App的视频播放页面。

如果你有任何问题或建议,欢迎在评论区留言!

作者介绍

作者:csdn猫哥

原文链接:https://blog.csdn.net/yyz_1987

团队介绍

坚果派团队由坚果等人创建,团队拥有12个华为HDE带领热爱HarmonyOS/OpenHarmony的开发者,以及若干其他领域的三十余位万粉博主运营。专注于分享HarmonyOS/OpenHarmony、ArkUI-X、元服务、仓颉等相关内容,团队成员聚集在北京、上海、南京、深圳、广州、宁夏等地,目前已开发鸿蒙原生应用和三方库60+,欢迎交流。

版权声明

本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

相关推荐
知本知至8 小时前
kubuntu24.04配置vmware17.5.1
ubuntu·typora·vmware·net·kubuntu
周杰伦_Jay8 小时前
Ubuntu20.4和docker终端指令、安装Go环境、安装搜狗输入法、安装WPS2019:保姆级图文详解
linux·python·ubuntu·docker·centos
Danileaf_Guo8 小时前
Ubuntu磁盘空间不足或配置错误时,如何操作扩容?
linux·运维·服务器·ubuntu
zjuter8 小时前
Android 播放SMB共享视频
音视频
鸭梨山大。8 小时前
ubuntu安全配置基线
linux·安全·ubuntu
数据行者9 小时前
ubuntu mysql 通过ip登录指南
mysql·ubuntu·域名访问
无限大.9 小时前
使用Python和FFmpeg批量转换视频为GIF
python·ffmpeg·音视频
AI服务老曹10 小时前
基于多个边缘盒子部署的综合视频安防系统的智慧地产开源了
人工智能·开源·音视频·能源
__Benco10 小时前
openharmony应用开发快速入门
harmonyos
如一@深声科技10 小时前
AI数字人PPT课件视频——探索新一代教学视频生成工具
大数据·人工智能·ai·aigc·音视频·交互