Skip to content

02-万少带你精读鸿蒙codelabs 一AVPlayer视频播放器

前言

本文将深入解析华为开发者联盟 CodeLabs 上的优质基于AVPlayer实现视频播放器功能——VideoPlayer。该项目使用ArkTS语言实现视频播放器,主要包括视频获取和视频播放功能:

  1. 获取本地视频和网络视频。
  2. 通过AVPlayer进行视频播放。
  3. 通过手势调节屏幕亮度和视频播放音量。

项目地址:https://developer.huawei.com/consumer/cn/codelabsPortal/carddetails/tutorials_NEXT-VideoPlayer

功能效果演示

PixPin_2025-04-11_07-54-30

工程目录

image-20250411080012539

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 - 视频播放速度数据模型

业务流程

image-20250411083259055

首页

首页中主要工作就是获取视频,获取本地视频和网络视频

image-20250411083324095

首页通过Tabs组件分成了本地视频网络视频两个子页面

本地视频

本地视频主要通过扫描raw目录下的,暂时写死文件的视频文件来加载,实际开发中可以调整读取相册视频等方式,或者访问缓存目录。

image-20250411083806523

其中VideoItem的类型表示为

typescript
@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;
  }
}

获取到数据之后,存放到了全局中,也可以通过函数返回

typescript
// HomeVideoListModel.ets
async getLocalVideo() {
    this.videoLocalList = [];
    await this.assemblingVideoItem();
    GlobalContext.getContext().setObject('videoLocalList', this.videoLocalList);
    return this.videoLocalList;
  }

网络视频

image-20250411084313893

网络视频的获取主要通过CustomDialogController显示弹出层后获取

image-20250411084340843

这里的链接校验其实分为两个步骤:

  1. 校验链接是否为空 checkSrcNull
  2. 校验链接是否真的可以播放 checkSrcValidity直接通过创建AVPlayer示例,根据示例的状态来判定

校验链接是否为空 checkSrcNull

image-20250411084906697


image-20250411085117684


校验链接是否真的可以播放 checkSrcValidity直接通过创建AVPlayer示例,根据示例的状态来判定

image-20250411085128503


image-20250411085137261

校验通过后,点击添加,便开始初始化AVPlayer准备播放网络视频。

视频播放

视频播放主要是通过AVPlayer来实现的,一般都根据基于它进行封装,先看使用介绍

播放步骤

  1. 调用createAVPlayer()创建AVPlayer实例,初始化进入idle状态。

  2. 设置业务需要的监听事件,搭配全流程场景使用。支持的监听事件包括:

    事件类型说明
    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使用。如果当前设备存在多个媒体正在播放,音频焦点被切换(即播放其他媒体如通话等)时将上报该事件,应用可以及时处理。
  3. 设置资源:设置属性url,AVPlayer进入initialized状态。

    说明

    下面代码示例中的url仅作示意使用,开发者需根据实际情况,确认资源有效性并设置:

  4. 设置窗口:获取并设置属性SurfaceID,用于设置显示画面。

    应用需要从XComponent组件获取surfaceID,获取方式请参考XComponent

  5. 准备播放:调用prepare(),AVPlayer进入prepared状态,此时可以获取duration,设置缩放模式、音量等。

  6. 视频播控:播放play(),暂停pause(),跳转seek(),停止stop() 等操作。

  7. (可选)更换资源:调用reset()重置资源,AVPlayer重新进入idle状态,允许更换资源url。

  8. 退出播放:调用release()销毁实例,AVPlayer进入released状态,退出播放。

AVPlayer播放状态图

这个状态图很重要,avplayer的播放流程,以及程序封装都需要参考它

image-20250411085709994

官网示例代码

typescript
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 成员变量

成员变量类型说明
homeTabModelHomeTabModel首页标签数据模型,用于管理UI状态和数据
avPlayermedia.AVPlayer | null视频播放器实例,用于验证视频链接
urlstring存储视频URL地址
durationnumber视频时长,用于判断视频链接有效性
checkFlagnumber检查标志,用于确定是验证还是添加数据(0: 仅验证链接,1: 验证并添加数据)
isLoadingboolean加载状态标志,防止重复操作
HomeDialogModel 函数
函数名参数返回值
----------------
constructor
createAvPlayer
bindState
checkSrcValiditycheckFlag: numberPromise<void>
checkUrlValidity
failureCallbackerror: Error
showPromptmsg: Resource
checkSrcNullboolean
checkNameNullboolean
typescript
// 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的功能主要体现在控制播放视频时屏幕的亮度上和音量大小上

上下滑动是控制屏幕亮度

PixPin_2025-04-11_09-09-15

左右滑动是控制音量大小

PixPin_2025-04-11_09-11-44


ArkUI中提供了常见的手势控制方案,其中这里使用的是PanGesture-拖动手势

image-20250411091404140

typescript
PanGesture(value?:{ fingers?:number, direction?:PanDirection, distance?:number})

拖动手势用于触发拖动手势事件,滑动达到最小滑动距离(默认值为5vp)时拖动手势识别成功,拥有三个可选参数:

  • fingers:用于声明触发拖动手势所需要的最少手指数量,最小值为1,最大值为10,默认值为1。
  • direction:用于声明触发拖动的手势方向,此枚举值支持逻辑与(&)和逻辑或(|)运算。默认值为Pandirection.All。
  • distance:用于声明触发拖动的最小拖动识别距离,单位为vp,默认值为5。

基本示例:

typescript
// 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)
  }
}

20250411091622

当前项目中的用法,如音量大小,分别在拖动的开始、过程、结束中进行业务处理。

image-20250411091751524

比如将拖动的距离和音量进行相关联

image-20250411091856473

image-20250411091924788

image-20250411091937606

总结

主要功能

本视频播放器实现了以下主要功能:

  1. 基本播放控制:播放、暂停、上一个、下一个
  2. 进度控制:进度条拖动、显示当前时间和总时间
  3. 手势控制
    • 垂直滑动调节屏幕亮度
    • 水平滑动调节视频播放音量
  4. 多视频源支持:支持本地视频和网络视频播放
  5. 自适应布局:根据视频尺寸自动调整播放器布局

技术特点

  1. 状态管理:通过监听 AVPlayer 的状态变化实现复杂的播放控制逻辑
  2. 高性能渲染:使用 XComponent 实现高性能视频渲染
  3. 手势识别:通过 PanGesture 实现直观的手势控制
  4. 模块化设计:将 UI 和业务逻辑分离,提高代码可维护性

如果你兴趣想要了解更多的开发细节和最新资讯,欢迎在评论区留言或者私信或者看我个人信息,可以加入技术交流群。

Released under the MIT License.