02-万少带你精读鸿蒙codelabs 一AVPlayer视频播放器
前言
本文将深入解析华为开发者联盟 CodeLabs 上的优质基于AVPlayer实现视频播放器功能——VideoPlayer。该项目使用ArkTS语言实现视频播放器,主要包括视频获取和视频播放功能:
- 获取本地视频和网络视频。
- 通过AVPlayer进行视频播放。
- 通过手势调节屏幕亮度和视频播放音量。
项目地址:https://developer.huawei.com/consumer/cn/codelabsPortal/carddetails/tutorials_NEXT-VideoPlayer
功能效果演示
工程目录
ets
│
├─common
│ ├─constants
│ │ ├─CommonConstants.ets - 定义全局通用常量,如状态码、事件名称等
│ │ ├─HomeConstants.ets - 首页相关的常量定义
│ │ └─PlayConstants.ets - 播放页面相关的常量定义
│ │
│ ├─model
│ │ ├─HomeTabModel.ets - 首页标签数据模型
│ │ └─PlayerModel.ets - 播放器数据模型,存储播放状态、进度、音量等信息
│ │
│ └─util
│ ├─DateFormatUtil.ets - 日期格式化工具,用于转换视频时间显示
│ ├─GlobalContext.ets - 全局上下文管理器,用于跨组件数据共享
│ ├─Logger.ets - 日志工具类,统一日志输出
│ └─ScreenUtil.ets - 屏幕相关工具类,获取屏幕参数
│
├─controller
│ └─VideoController.ets - 视频控制器核心类,负责视频播放的状态管理、手势控制等核心业务逻辑
│
├─entryability
│ └─EntryAbility.ts - 应用入口能力类,负责应用初始化和生命周期管理
│
├─pages
│ ├─HomePage.ets - 首页,展示视频列表
│ └─PlayPage.ets - 播放页面,整合播放器各组件
│
├─view
│ ├─HomeTabContent.ets - 首页标签内容组件
│ ├─HomeTabContentButton.ets - 首页标签内容按钮组件
│ ├─HomeTabContentDialog.ets - 首页对话框组件
│ ├─HomeTabContentList.ets - 首页视频列表组件
│ ├─HomeTabContentListItem.ets - 首页视频列表项组件
│ ├─PlayControl.ets - 播放控制组件,包含播放/暂停、上一个/下一个按钮
│ ├─PlayPlayer.ets - 播放器核心渲染组件,集成XComponent
│ ├─PlayProgress.ets - 播放进度条组件
│ ├─PlayTitle.ets - 播放页面顶部标题组件
│ └─PlayTitleDialog.ets - 播放页面标题对话框组件
│
└─viewmodel
├─HomeDialogModel.ets - 首页对话框数据模型
├─HomeVideoListModel.ets - 首页视频列表数据模型
├─VideoItem.ets - 视频项数据模型
└─VideoSpeed.ets - 视频播放速度数据模型
业务流程
首页
首页中主要工作就是获取视频,获取本地视频和网络视频
首页通过Tabs组件分成了本地视频和网络视频两个子页面
本地视频
本地视频主要通过扫描raw目录下的,暂时写死文件的视频文件来加载,实际开发中可以调整读取相册视频等方式,或者访问缓存目录。
其中VideoItem的类型表示为
@Observed
export class VideoItem {
name: string;
src: resourceManager.RawFileDescriptor;
iSrc: string;
pixelMap?: image.PixelMap;
constructor(name: string, src: resourceManager.RawFileDescriptor, iSrc: string, pixelMap?: image.PixelMap) {
this.name = name;
this.src = src;
this.iSrc = iSrc;
this.pixelMap = pixelMap;
}
}
获取到数据之后,存放到了全局中,也可以通过函数返回
// HomeVideoListModel.ets
async getLocalVideo() {
this.videoLocalList = [];
await this.assemblingVideoItem();
GlobalContext.getContext().setObject('videoLocalList', this.videoLocalList);
return this.videoLocalList;
}
网络视频
网络视频的获取主要通过CustomDialogController显示弹出层后获取
这里的链接校验其实分为两个步骤:
- 校验链接是否为空
checkSrcNull
- 校验链接是否真的可以播放
checkSrcValidity
,直接通过创建AVPlayer示例,根据示例的状态来判定
校验链接是否为空 checkSrcNull
校验链接是否真的可以播放 checkSrcValidity
,直接通过创建AVPlayer示例,根据示例的状态来判定
校验通过后,点击添加,便开始初始化AVPlayer准备播放网络视频。
视频播放
视频播放主要是通过AVPlayer来实现的,一般都根据基于它进行封装,先看使用介绍
播放步骤
调用createAVPlayer()创建AVPlayer实例,初始化进入idle状态。
设置业务需要的监听事件,搭配全流程场景使用。支持的监听事件包括:
事件类型 说明 stateChange 必要事件,监听播放器的state属性改变。 error 必要事件,监听播放器的错误信息。 durationUpdate 用于进度条,监听进度条长度,刷新资源时长。 timeUpdate 用于进度条,监听进度条当前位置,刷新当前时间。 seekDone 响应API调用,监听seek()请求完成情况。当使用seek()跳转到指定播放位置后,如果seek操作成功,将上报该事件。 speedDone 响应API调用,监听setSpeed()请求完成情况。当使用setSpeed()设置播放倍速后,如果setSpeed操作成功,将上报该事件。 volumeChange 响应API调用,监听setVolume()请求完成情况。当使用setVolume()调节播放音量后,如果setVolume操作成功,将上报该事件。 bitrateDone 响应API调用,用于HLS协议流,监听setBitrate()请求完成情况。当使用setBitrate()指定播放比特率后,如果setBitrate操作成功,将上报该事件。 availableBitrates 用于HLS协议流,监听HLS资源的可选bitrates,用于setBitrate()。 bufferingUpdate 用于网络播放,监听网络播放缓冲信息。 startRenderFrame 用于视频播放,监听视频播放首帧渲染时间。当AVPlayer首次起播进入playing状态后,等到首帧视频画面被渲染到显示画面时,将上报该事件。应用通常可以利用此事件上报,进行视频封面移除,达成封面与视频画面的顺利衔接。 videoSizeChange 用于视频播放,监听视频播放的宽高信息,可用于调整窗口大小、比例。 audioInterrupt 监听音频焦点切换信息,搭配属性audioInterruptMode使用。如果当前设备存在多个媒体正在播放,音频焦点被切换(即播放其他媒体如通话等)时将上报该事件,应用可以及时处理。 设置资源:设置属性url,AVPlayer进入initialized状态。
说明
下面代码示例中的url仅作示意使用,开发者需根据实际情况,确认资源有效性并设置:
- 如果使用本地资源播放,必须确认资源文件可用,并使用应用沙箱路径访问对应资源,参考获取应用文件路径。应用沙箱的介绍及如何向应用沙箱推送文件,请参考文件管理。
- 如果使用网络播放路径,需声明权限:ohos.permission.INTERNET。
- 如果使用ResourceManager.getRawFd打开HAP资源文件描述符,使用方法可参考ResourceManager API参考。
- 需要使用支持的播放格式与协议。
设置窗口:获取并设置属性SurfaceID,用于设置显示画面。
应用需要从XComponent组件获取surfaceID,获取方式请参考XComponent。
准备播放:调用prepare(),AVPlayer进入prepared状态,此时可以获取duration,设置缩放模式、音量等。
视频播控:播放play(),暂停pause(),跳转seek(),停止stop() 等操作。
(可选)更换资源:调用reset()重置资源,AVPlayer重新进入idle状态,允许更换资源url。
退出播放:调用release()销毁实例,AVPlayer进入released状态,退出播放。
AVPlayer播放状态图
这个状态图很重要,avplayer的播放流程,以及程序封装都需要参考它
官网示例代码
import { media } from '@kit.MediaKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
export class AVPlayerDemo {
private count: number = 0;
private surfaceID: string = ''; // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接见上面Xcomponent创建方法。
private isSeek: boolean = true; // 用于区分模式是否支持seek操作。
private fileSize: number = -1;
private fd: number = 0;
constructor(surfaceID: string) {
this.surfaceID = surfaceID;
}
// 注册avplayer回调函数。
setAVPlayerCallback(avPlayer: media.AVPlayer) {
// startRenderFrame首帧渲染回调函数。
avPlayer.on('startRenderFrame', () => {
console.info(`AVPlayer start render frame`);
});
// seek操作结果回调函数。
avPlayer.on('seekDone', (seekDoneTime: number) => {
console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
});
// error回调监听函数,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程。
avPlayer.on('error', (err: BusinessError) => {
console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);
avPlayer.reset(); // 调用reset重置资源,触发idle状态。
});
// 状态机变化回调函数。
avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
switch (state) {
case 'idle': // 成功调用reset接口后触发该状态机上报。
console.info('AVPlayer state idle called.');
avPlayer.release(); // 调用release接口销毁实例对象。
break;
case 'initialized': // avplayer 设置播放源后触发该状态上报。
console.info('AVPlayer state initialized called.');
avPlayer.surfaceId = this.surfaceID; // 设置显示画面,当播放的资源为纯音频时无需设置。
avPlayer.prepare();
break;
case 'prepared': // prepare调用成功后上报该状态机。
console.info('AVPlayer state prepared called.');
avPlayer.play(); // 调用播放接口开始播放。
break;
case 'playing': // play成功调用后触发该状态机上报。
console.info('AVPlayer state playing called.');
if (this.count !== 0) {
if (this.isSeek) {
console.info('AVPlayer start to seek.');
avPlayer.seek(avPlayer.duration); //seek到视频末尾。
} else {
// 当播放模式不支持seek操作时继续播放到结尾。
console.info('AVPlayer wait to play end.');
}
} else {
avPlayer.pause(); // 调用暂停接口暂停播放。
}
this.count++;
break;
case 'paused': // pause成功调用后触发该状态机上报。
console.info('AVPlayer state paused called.');
avPlayer.play(); // 再次播放接口开始播放。
break;
case 'completed': // 播放结束后触发该状态机上报。
console.info('AVPlayer state completed called.');
avPlayer.stop(); //调用播放结束接口。
break;
case 'stopped': // stop接口成功调用后触发该状态机上报。
console.info('AVPlayer state stopped called.');
avPlayer.reset(); // 调用reset接口初始化avplayer状态。
break;
case 'released':
console.info('AVPlayer state released called.');
break;
default:
console.info('AVPlayer state unknown called.');
break;
}
});
}
// 以下demo为使用fs文件系统打开沙箱地址获取媒体文件地址并通过url属性进行播放示例。
async avPlayerUrlDemo() {
// 创建avPlayer实例对象。
let avPlayer: media.AVPlayer = await media.createAVPlayer();
// 创建状态机变化回调函数。
this.setAVPlayerCallback(avPlayer);
let fdPath = 'fd://';
let context = getContext(this) as common.UIAbilityContext;
// 通过UIAbilityContext获取沙箱地址filesDir,以Stage模型为例。
let pathDir = context.filesDir;
let path = pathDir + '/H264_AAC.mp4';
// 打开相应的资源文件地址获取fd,并为url赋值触发initialized状态机上报。
let file = await fs.open(path);
fdPath = fdPath + '' + file.fd;
this.isSeek = true; // 支持seek操作。
avPlayer.url = fdPath;
}
// 以下demo为使用资源管理接口获取打包在HAP内的媒体资源文件并通过fdSrc属性进行播放示例。
async avPlayerFdSrcDemo() {
// 创建avPlayer实例对象。
let avPlayer: media.AVPlayer = await media.createAVPlayer();
// 创建状态机变化回调函数。
this.setAVPlayerCallback(avPlayer);
// 通过UIAbilityContext的resourceManager成员的getRawFd接口获取媒体资源播放地址。
// 返回类型为{fd,offset,length},fd为HAP包fd地址,offset为媒体资源偏移量,length为播放长度。
let context = getContext(this) as common.UIAbilityContext;
let fileDescriptor = await context.resourceManager.getRawFd('H264_AAC.mp4');
let avFileDescriptor: media.AVFileDescriptor =
{ fd: fileDescriptor.fd, offset: fileDescriptor.offset, length: fileDescriptor.length };
this.isSeek = true; // 支持seek操作。
// 为fdSrc赋值触发initialized状态机上报。
avPlayer.fdSrc = avFileDescriptor;
}
// 以下demo为使用fs文件系统打开沙箱地址获取媒体文件地址并通过dataSrc属性进行播放(seek模式)示例。
async avPlayerDataSrcSeekDemo() {
// 创建avPlayer实例对象。
let avPlayer: media.AVPlayer = await media.createAVPlayer();
// 创建状态机变化回调函数。
this.setAVPlayerCallback(avPlayer);
// dataSrc播放模式的的播放源地址,当播放为Seek模式时fileSize为播放文件的具体大小,下面会对fileSize赋值。
let src: media.AVDataSrcDescriptor = {
fileSize: -1,
callback: (buf: ArrayBuffer, length: number, pos: number | undefined) => {
let num = 0;
if (buf == undefined || length == undefined || pos == undefined) {
return -1;
}
num = fs.readSync(this.fd, buf, { offset: pos, length: length });
if (num > 0 && (this.fileSize >= pos)) {
return num;
}
return -1;
}
};
let context = getContext(this) as common.UIAbilityContext;
// 通过UIAbilityContext获取沙箱地址filesDir,以Stage模型为例。
let pathDir = context.filesDir;
let path = pathDir + '/H264_AAC.mp4';
await fs.open(path).then((file: fs.File) => {
this.fd = file.fd;
});
// 获取播放文件的大小。
this.fileSize = fs.statSync(path).size;
src.fileSize = this.fileSize;
this.isSeek = true; // 支持seek操作。
avPlayer.dataSrc = src;
}
// 以下demo为使用fs文件系统打开沙箱地址获取媒体文件地址并通过dataSrc属性进行播放(No seek模式)示例。
async avPlayerDataSrcNoSeekDemo() {
// 创建avPlayer实例对象。
let avPlayer: media.AVPlayer = await media.createAVPlayer();
// 创建状态机变化回调函数。
this.setAVPlayerCallback(avPlayer);
let context = getContext(this) as common.UIAbilityContext;
let src: media.AVDataSrcDescriptor = {
fileSize: -1,
callback: (buf: ArrayBuffer, length: number) => {
let num = 0;
if (buf == undefined || length == undefined) {
return -1;
}
num = fs.readSync(this.fd, buf);
if (num > 0) {
return num;
}
return -1;
}
};
// 通过UIAbilityContext获取沙箱地址filesDir,以Stage模型为例。
let pathDir = context.filesDir;
let path = pathDir + '/H264_AAC.mp4';
await fs.open(path).then((file: fs.File) => {
this.fd = file.fd;
});
this.isSeek = false; // 不支持seek操作。
avPlayer.dataSrc = src;
}
// 以下demo为通过url设置网络地址来实现播放直播码流的demo。
async avPlayerLiveDemo() {
// 创建avPlayer实例对象。
let avPlayer: media.AVPlayer = await media.createAVPlayer();
// 创建状态机变化回调函数。
this.setAVPlayerCallback(avPlayer);
this.isSeek = false; // 不支持seek操作。
avPlayer.url = 'http://xxx.xxx.xxx.xxx:xx/xx/index.m3u8'; // 播放hls网络直播码流。
}
// 以下demo为通过setMediaSource设置自定义头域及媒体播放优选参数实现初始播放参数设置。
async preDownloadDemo() {
// 创建avPlayer实例对象。
let avPlayer: media.AVPlayer = await media.createAVPlayer();
let mediaSource : media.MediaSource = media.createMediaSourceWithUrl("http://xxx", {"User-Agent" : "User-Agent-Value"});
let playbackStrategy : media.PlaybackStrategy = {preferredWidth: 1, preferredHeight: 2, preferredBufferDuration: 3, preferredHdr: false};
// 设置媒体来源和播放策略。
avPlayer.setMediaSource(mediaSource, playbackStrategy);
}
// 以下demo为通过selectTrack设置音频轨道,通过deselectTrack取消上次设置的音频轨道并恢复到视频默认音频轨道。
async multiTrackDemo() {
// 创建avPlayer实例对象。
let avPlayer: media.AVPlayer = await media.createAVPlayer();
let audioTrackIndex: Object = 0;
avPlayer.getTrackDescription((error: BusinessError, arrList: Array<media.MediaDescription>) => {
if (arrList != null) {
for (let i = 0; i < arrList.length; i++) {
if (i != 0) {
// 获取音频轨道列表。
audioTrackIndex = arrList[i][media.MediaDescriptionKey.MD_KEY_TRACK_INDEX];
}
}
} else {
console.error(`audio getTrackDescription fail, error:${error}`);
}
});
// 选择其中一个音频轨道。
avPlayer.selectTrack(parseInt(audioTrackIndex.toString()));
// 取消选择上次选中的音频轨道,并恢复到默认音频轨道。
avPlayer.deselectTrack(parseInt(audioTrackIndex.toString()));
}
}
这里看一下当前项目的封装 HomeDialogModel.ets
HomeDialogModel 成员变量
成员变量 | 类型 | 说明 |
---|---|---|
homeTabModel | HomeTabModel | 首页标签数据模型,用于管理UI状态和数据 |
avPlayer | media.AVPlayer | null | 视频播放器实例,用于验证视频链接 |
url | string | 存储视频URL地址 |
duration | number | 视频时长,用于判断视频链接有效性 |
checkFlag | number | 检查标志,用于确定是验证还是添加数据(0: 仅验证链接,1: 验证并添加数据) |
isLoading | boolean | 加载状态标志,防止重复操作 |
HomeDialogModel 函数 | ||
函数名 | 参数 | 返回值 |
------ | ---- | ------ |
constructor | 无 | 无 |
createAvPlayer | 无 | 无 |
bindState | 无 | 无 |
checkSrcValidity | checkFlag: number | Promise<void> |
checkUrlValidity | 无 | 无 |
failureCallback | error: Error | 无 |
showPrompt | msg: Resource | 无 |
checkSrcNull | 无 | boolean |
checkNameNull | 无 | boolean |
// HomeDialogModel.ets
/*
* Copyright (c) 2023 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { promptAction } from "@kit.ArkUI";
import { media } from "@kit.MediaKit";
import Logger from "../common/util/Logger";
import { HomeConstants } from "../common/constants/HomeConstants";
import { AvplayerStatus, Events } from "../common/constants/CommonConstants";
import { HomeTabModel } from "../common/model/HomeTabModel";
export class HomeDialogModel {
/**
* 首页标签数据模型,用于管理UI状态和数据
*/
public homeTabModel: HomeTabModel;
/**
* 视频播放器实例,用于验证视频链接
*/
private avPlayer: media.AVPlayer | null = null;
/**
* 存储视频URL地址
*/
private url: string = "";
/**
* 视频时长,用于判断视频链接有效性
*/
private duration: number = 0;
/**
* 检查标志,用于确定是验证还是添加数据
* 0: 仅验证链接
* 1: 验证并添加数据
*/
private checkFlag: number = 0;
/**
* 加载状态标志,防止重复操作
*/
private isLoading: boolean = false;
constructor() {
this.homeTabModel = new HomeTabModel();
this.isLoading = false;
}
/**
* 创建视频播放器对象。
*/
createAvPlayer() {
media
.createAVPlayer()
.then((video: media.AVPlayer) => {
if (video != null) {
this.avPlayer = video;
this.bindState();
this.url = this.homeTabModel.src;
this.avPlayer.url = this.url;
} else {
Logger.info(`[HomeDialogModel] createAVPlayer fail`);
}
})
.catch((err: Error) => {
this.failureCallback(err);
});
}
/**
* 绑定播放器状态变化监听
*/
bindState() {
if (this.avPlayer === null) {
return;
}
this.avPlayer.on(
Events.STATE_CHANGE,
async (state: media.AVPlayerState) => {
if (this.avPlayer === null) {
return;
}
if (state === AvplayerStatus.INITIALIZED) {
this.avPlayer.prepare();
} else if (state === AvplayerStatus.PREPARED) {
this.duration = this.avPlayer.duration;
this.checkUrlValidity();
} else if (state === AvplayerStatus.ERROR) {
this.avPlayer.reset();
}
}
);
this.avPlayer.on(Events.ERROR, (error: Error) => {
this.isLoading = false;
this.homeTabModel.linkCheck = $r("app.string.link_check");
this.homeTabModel.loadColor = $r(
"app.color.index_tab_selected_font_color"
);
if (this.avPlayer !== null) {
this.avPlayer.release();
}
this.failureCallback(error);
});
}
/**
* 验证网络连接。
*
* @param checkFlag 确定是验证还是添加数据。
*/
async checkSrcValidity(checkFlag: number) {
if (this.isLoading) {
return;
}
this.isLoading = true;
this.homeTabModel.linkCheck = $r("app.string.link_checking");
this.homeTabModel.loadColor = $r(
"app.color.index_tab_unselected_font_color"
);
this.checkFlag = checkFlag;
this.createAvPlayer();
}
/**
* 验证网络连接。
*/
checkUrlValidity() {
this.isLoading = false;
this.homeTabModel.linkCheck = $r("app.string.link_check");
this.homeTabModel.loadColor = $r("app.color.index_tab_selected_font_color");
if (this.avPlayer !== null) {
this.avPlayer.release();
}
if (this.duration === HomeConstants.DURATION_TWO) {
// 链接验证失败
this.showPrompt($r("app.string.link_check_fail"));
} else if (this.duration === HomeConstants.DURATION_ONE) {
// 地址不正确或网络不可用
this.showPrompt($r("app.string.link_check_address_internet"));
} else {
this.duration = 0;
if (this.checkFlag === 0) {
this.showPrompt($r("app.string.link_check_success"));
} else {
this.homeTabModel!.confirm();
this.homeTabModel!.controller!.close();
}
}
}
/**
* 当函数调用发生错误时,用于报告错误信息。
*
* @param error 错误信息。
*/
failureCallback(error: Error) {
Logger.error(`[HomeDialogModel] error happened: ` + JSON.stringify(error));
this.showPrompt($r("app.string.link_check_fail"));
}
/**
* 提示对话框。
*
* @param msg 验证信息。
*/
showPrompt(msg: Resource) {
promptAction.showToast({
duration: HomeConstants.DURATION,
message: msg,
});
}
/**
* 检查播放路径是否为空。
*/
checkSrcNull(): boolean {
if (this.isLoading) {
return false;
}
if (this.homeTabModel.src.trim() === "") {
promptAction.showToast({
duration: HomeConstants.DURATION,
message: $r("app.string.place_holder_src"),
});
return false;
}
return true;
}
/**
* 检查名称是否为空。
*/
checkNameNull(): boolean {
if (this.isLoading) {
return false;
}
if (this.homeTabModel.name.trim() === "") {
promptAction.showToast({
duration: HomeConstants.DURATION,
message: $r("app.string.place_holder_name"),
});
return false;
}
return true;
}
}
手势控制
手势控制gesture的功能主要体现在控制播放视频时屏幕的亮度上和音量大小上
上下滑动是控制屏幕亮度
左右滑动是控制音量大小
ArkUI中提供了常见的手势控制方案,其中这里使用的是PanGesture-拖动手势
PanGesture(value?:{ fingers?:number, direction?:PanDirection, distance?:number})
拖动手势用于触发拖动手势事件,滑动达到最小滑动距离(默认值为5vp)时拖动手势识别成功,拥有三个可选参数:
- fingers:用于声明触发拖动手势所需要的最少手指数量,最小值为1,最大值为10,默认值为1。
- direction:用于声明触发拖动的手势方向,此枚举值支持逻辑与(&)和逻辑或(|)运算。默认值为Pandirection.All。
- distance:用于声明触发拖动的最小拖动识别距离,单位为vp,默认值为5。
基本示例:
// xxx.ets
@Entry
@Component
struct Index {
@State offsetX: number = 0;
@State offsetY: number = 0;
@State positionX: number = 0;
@State positionY: number = 0;
build() {
Column() {
Text('PanGesture Offset:\nX: ' + this.offsetX + '\n' + 'Y: ' + this.offsetY)
.fontSize(28)
.height(200)
.width(300)
.padding(20)
.border({ width: 3 })
// 在组件上绑定布局位置信息
.translate({ x: this.offsetX, y: this.offsetY, z: 0 })
.gesture(
// 绑定拖动手势
PanGesture()
.onActionStart((event: GestureEvent|undefined) => {
console.info('Pan start');
})
// 当触发拖动手势时,根据回调函数修改组件的布局位置信息
.onActionUpdate((event: GestureEvent|undefined) => {
if(event){
this.offsetX = this.positionX + event.offsetX;
this.offsetY = this.positionY + event.offsetY;
}
})
.onActionEnd(() => {
this.positionX = this.offsetX;
this.positionY = this.offsetY;
})
)
}
.height(200)
.width(250)
}
}
当前项目中的用法,如音量大小,分别在拖动的开始、过程、结束中进行业务处理。
比如将拖动的距离和音量进行相关联
总结
主要功能
本视频播放器实现了以下主要功能:
- 基本播放控制:播放、暂停、上一个、下一个
- 进度控制:进度条拖动、显示当前时间和总时间
- 手势控制:
- 垂直滑动调节屏幕亮度
- 水平滑动调节视频播放音量
- 多视频源支持:支持本地视频和网络视频播放
- 自适应布局:根据视频尺寸自动调整播放器布局
技术特点
- 状态管理:通过监听 AVPlayer 的状态变化实现复杂的播放控制逻辑
- 高性能渲染:使用 XComponent 实现高性能视频渲染
- 手势识别:通过 PanGesture 实现直观的手势控制
- 模块化设计:将 UI 和业务逻辑分离,提高代码可维护性
如果你兴趣想要了解更多的开发细节和最新资讯,欢迎在评论区留言或者私信或者看我个人信息,可以加入技术交流群。