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 版权协议,转载请附上原文出处链接和本声明。

相关推荐
byte轻骑兵3 小时前
【HFP】蓝牙HFP协议中音频连接转移与拨号功能的深度解析
音视频·蓝牙技术·hfp
老兵发新帖7 小时前
Ubuntu 上安装 Conda
linux·ubuntu·conda
高心星10 小时前
HarmonyOS 5.0应用开发——MVVM模式的应用
harmonyos·mvvm·鸿蒙5.0·备忘录应用
别说我什么都不会10 小时前
【仓颉三方库】工具类—— compress4cj
harmonyos
别说我什么都不会10 小时前
【仓颉三方库】工具类—— uuid4cj
harmonyos
foo1st12 小时前
Tomcat Web应用(Ubuntu 18.04.6 LTS)部署笔记
ubuntu·tomcat
cosX+sinY15 小时前
ubuntu 20.04 编译运行lio-sam,并保存为pcd
linux·ubuntu·机器人
xiaoh_715 小时前
解决视频处理中的 HEVC 解码错误:Could not find ref with POC xxx【已解决】
python·ffmpeg·音视频
FREEDOM_X17 小时前
ubuntu20.04 远程桌面Xrdp方式
ubuntu·vmware
__Benco17 小时前
OpenHarmony - 小型系统内核(LiteOS-A)(十),魔法键使用方法,用户态异常信息说明
人工智能·harmonyos