写点什么

使用手势自定义截取视频时间组件

作者:
  • 2025-05-24
    广东
  • 本文字数:4857 字

    阅读完需:约 16 分钟



1. 组件原理

  • 核心机制:通过左右拖动手势动态调整视频裁剪的起止时间点,将屏幕上的像素位移量转换为时间比例。

  • 数学映射:基于 maxCropWidth(默认 260px)与视频总时长 videoDuration 的线性映射关系,计算起止时间:时间 = 位移量 / maxCropWidth * videoDuration

  • 手势驱动:左右边框独立响应拖拽事件(PanGesture),实时更新位移值并约束最小间距(minSpace),确保时间区间有效性。

2. 实现逻辑

  • 参数传递

    必须参数:videoDuration(总时长)、videoCurrentDuration(当前播放进度)、isPlay(播放状态)。

    可选参数:minSpace(最小时间间隔,默认 26px,对应 1 秒)。

  • 状态管理

    使用 @Local 变量(如 updateLOffsetupdateROffset)记录左右边框的实时位移。

    通过 @Monitor 监听视频时长和进度变化,同步更新内部状态。

  • 手势处理

    左侧拖拽:限制位移范围(0 ≤ L ≤ R - minSpace),更新 startTime

    右侧拖拽:限制位移范围(L + minSpace ≤ R ≤ maxCropWidth),更新 endTime

    手势结束后触发事件回调(onLPanActionUpdateonRPanActionUpdate)。

  • UI 渲染

    使用 Stack 布局叠加视频帧预览、进度条、边框线。

    动态计算进度条位置(progress)和裁剪区间线宽(lineWidth)。


3. 实际应用场景

  • 视频编辑工具:用于精确截取视频片段(如短视频剪辑、Vlog 制作)。

  • 播放器控件:结合播放控制,实现片段循环播放或裁剪后导出。

  • 教学/演示场景:快速定位关键帧或高光时刻。


4. 优势

  • 易用性

    提供开箱即用的拖拽交互,无需额外处理手势逻辑。

    支持通过 @BuilderParam 自定义视频帧预览 UI(imageBuilder)。

  • 灵活性

    可配置最小时间间隔(minSpace),适应不同精度需求。

    事件回调机制(initUpdateTimeonXPanActionUpdate)便于外部逻辑集成。

  • 视觉反馈

    高亮显示裁剪区间(红色边框+白色分隔线),进度条实时反映播放进度。


5. 视觉效果

  • 动态边框:红色边框(#FF5A57)随拖拽动态扩展/收缩,增强操作感知。

  • 进度指示:白色竖线(ProgressBuilder)实时反映当前播放位置。

  • 约束提示:拖拽超过 minSpace 时自动吸附,避免区间无效。


6. 可定制性

  • UI 自定义

    通过 @BuilderParam imageBuilder 替换默认视频帧布局(如缩略图列表)。

    可调整边框颜色、线宽、圆角等样式属性。

  • 参数扩展

    支持外部传入 maxCropWidth 适配不同屏幕尺寸。

    可扩展事件类型(如拖拽开始、中间状态回调)。


7. 性能与适配

  • 性能优化

    使用轻量级手势监听(PanGestureOptions 设置 distance:1 减少误触发)。

    避免频繁 UI 重绘,仅在位移变化时更新必要元素。

  • 适配建议

    默认 maxCropWidth=260 需根据屏幕宽度动态计算(如百分比布局)。

    考虑高刷新率屏幕下的手势流畅性(如使用异步渲染)。


8. 代码(实现场景)

@ComponentV2export struct CropVideoTimeCom {  //需要传入的参数  //视频总时长  @Param videoDuration: number = 0  //视频当前时长  @Param videoCurrentDuration: number = 0  //是否播放  @Param isPlay: boolean = false  //最小间距//可不传  @Param minSpace: number = 26 //传入视频时间,可控制最小间距为1秒  //=======================================================================  //左侧手势开始的位移  @Local startLOffset: number = 0  //左侧中间位移  @Local updateLOffset: number = 0  //左侧手势结束的位移  @Local endLOffset: number = 0  //右侧手势开始的位移  @Local startROffset: number = 0  //右侧中间位移  @Local updateROffset: number = 260  //右侧手势结束的位移  @Local endROffset: number = 260  //线宽度  @Local lineWidth: number = 260  //=======================================================================  //视频进度  @Local progress: number = this.endLOffset  //视频开始时间  @Local startTime: number = 0  //视频结束时间  @Local endTime: number = 0  //=======================================================================  //左右边框宽度  frameWidth: number = 15  //最大宽度  maxCropWidth: number = 260  //拖动位移  @Local dragOffsetRx: number = this.updateLOffset + this.lineWidth  private LPanOption: PanGestureOptions = new PanGestureOptions({    direction: PanDirection.Right | PanDirection.Left,    distance: 1,  })  private RPanOption: PanGestureOptions = new PanGestureOptions({    direction: PanDirection.Left | PanDirection.Right,    distance: 1,  })  //初始化时间//组件创建触发回调  @Event initUpdateTime: (startTime: number, endTime: number) => void = () => {  }  //左侧手势结束触发回调  @Event onLPanActionUpdate: (startTime: number, endTime: number) => void = () => {  }  //右侧手势结束触发回调  @Event onRPanActionUpdate: (startTime: number, endTime: number) => void = () => {  }
@Builder defaultBuilder() { Row().width(this.maxCropWidth).height(46).backgroundColor(Color.Blue) }
//传入视频帧图片UI结构 @BuilderParam imageBuilder: () => void = this.defaultBuilder
//当视频总时长改变时赋值传出,直接赋值传出可能获取不到视频时间 @Monitor('videoDuration') videoDurationChange() { this.endTime = this.videoDuration this.initUpdateTime(this.startTime, this.endTime) }
//监听视频当前进度 @Monitor('videoCurrentDuration') videoCurrentDurationChange() { if (this.videoCurrentDuration > 0) { this.progress = this.videoCurrentDuration / this.videoDuration * this.maxCropWidth } }
//左侧手势更新逻辑 leftGestureLogic(event: GestureEvent) { if (event && this.updateLOffset >= 0 && this.updateLOffset <= this.maxCropWidth) { //中间位移 this.updateLOffset = this.endLOffset + event.offsetX console.log('updateL' + this.updateLOffset) } if (this.updateLOffset <= 0) { this.updateLOffset = 0 } if (this.updateLOffset >= this.updateROffset - this.minSpace) { this.updateLOffset = this.updateROffset - this.minSpace } this.lineWidth = Math.abs(this.dragOffsetRx - this.updateLOffset) >= this.maxCropWidth ? this.maxCropWidth : Math.abs(this.dragOffsetRx - this.updateLOffset) }
//右侧手势更新逻辑 rightGestureLogic(event: GestureEvent) { if (event && this.updateROffset >= 0 && this.updateROffset <= this.maxCropWidth) { this.updateROffset = this.endROffset + event.offsetX console.log('updateR' + this.updateROffset) } if (this.updateROffset <= 0) { this.updateROffset = 0 } if (this.updateROffset <= this.minSpace + this.updateLOffset) { this.updateROffset = this.minSpace + this.updateLOffset } this.dragOffsetRx = this.updateROffset this.lineWidth = Math.abs(this.updateROffset - this.updateLOffset) >= this.maxCropWidth ? this.maxCropWidth : Math.abs(this.updateROffset - this.updateLOffset) }
leftEndLogic(event: GestureEvent) { this.endLOffset = this.updateLOffset this.progress = this.endLOffset this.startTime = this.endLOffset / this.maxCropWidth * this.videoDuration this.onLPanActionUpdate(this.startTime, this.endTime) console.log('endLL' + this.endLOffset) }
rightEndLogic(event: GestureEvent) { this.endROffset = this.updateROffset this.progress = this.endROffset this.endTime = this.endROffset / this.maxCropWidth * this.videoDuration this.onRPanActionUpdate(this.startTime, this.endTime) console.log('endRR' + this.endROffset) }
//上下边框线 @Builder TBBorderBuilder() { Column() { //状态改变 Row().width(this.lineWidth).height(3).backgroundColor('#FF5A57') Row().width(this.lineWidth).height(3).backgroundColor('#FF5A57') } .height(52) .position({ x: this.updateLOffset + this.frameWidth }) .width(this.lineWidth) .justifyContent(FlexAlign.SpaceBetween) }
//进度条 @Builder ProgressBuilder() { Row() .width(4) .height(60) .backgroundColor(Color.White) .borderRadius(2) .position({ x: this.progress + this.frameWidth, y: -3 }) .zIndex(1) }
//左边边框 @Builder LRBorderBuilder() { Row() { //左边手势 Column() { Text().width(1).height(11).backgroundColor('#FFFFFF') } .width(this.frameWidth) .height(52) .borderRadius({ topLeft: 6, bottomLeft: 6 }) .backgroundColor('#FF5A57') .justifyContent(FlexAlign.Center) .offset({ x: this.updateLOffset }) .gesture( PanGesture(this.LPanOption) .onActionUpdate((event: GestureEvent) => { this.leftGestureLogic(event) }) .onActionEnd((event: GestureEvent) => { this.leftEndLogic(event) }) )
//右边手势 Column() { Text().width(1).height(11).backgroundColor('#FFFFFF') } .width(this.frameWidth) .height(52) .borderRadius({ topRight: 6, bottomRight: 6 }) .backgroundColor('#FF5A57') .justifyContent(FlexAlign.Center) .offset({ x: this.updateROffset }) .gesture( PanGesture(this.RPanOption) .onActionUpdate((event: GestureEvent) => { this.rightGestureLogic(event) }) .onActionEnd((event: GestureEvent) => { this.rightEndLogic(event) }) ) } .width('100%') }
build() { Stack({ alignContent: Alignment.Center }) { this.TBBorderBuilder() this.imageBuilder() this.ProgressBuilder() this.LRBorderBuilder() } .width(290) .height(52) }}
复制代码

9. 代码示例(使用场景)

typescript

复制

下载

// 父组件调用示例@Componentstruct ParentComponent {  @State videoDur: number = 60; // 总时长60秒  @State currentTime: number = 0;  @State isPlaying: boolean = false;
build() { Column() { CropVideoTimeCom({ videoDuration: this.videoDur, videoCurrentDuration: this.currentTime, isPlay: this.isPlaying, minSpace: 26 // 1秒间隔 }) .initUpdateTime((start: number, end: number) => { console.log(`初始时间区间: ${start} - ${end}`); }) .onLPanActionUpdate((start: number, end: number) => { console.log(`左侧拖动更新: ${start} - ${end}`); }) .onRPanActionUpdate((start: number, end: number) => { console.log(`右侧拖动更新: ${start} - ${end}`); }) } }}
复制代码



通过以上设计,CropVideoTimeCom 组件在视频处理场景中实现了高效、直观的时间裁剪功能,兼具灵活性与可维护性,适合集成到多媒体应用

用户头像

关注

还未添加个人签名 2025-05-06 加入

还未添加个人简介

评论

发布
暂无评论
使用手势自定义截取视频时间组件_鸿蒙_林_InfoQ写作社区