• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2025 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16/**
17 * 视频剪辑时间长度选择组件
18 */
19
20import CommonConstants from '../constants/Constants';
21import { logger } from '../utils/Logger';
22import { RangSeekBarListener } from './RangSeekBarListener';
23import { RangSeekBarOption } from './RangSeekBarOption';
24import { TimeUtils } from '../utils/TimeUtils';
25
26const TAG: string = 'videoTrimmer_SeekBar';
27
28/**
29 * 视频播放状态
30 */
31export class MediaPlayerStatus {
32  public redLineProgress: number = 0; // [0,1]
33  public isPlaying: boolean = false; // true播放 false暂停
34}
35
36@Component
37export struct RangeSeekBarView {
38  // 裁剪选择区域变动时刷新界面
39  @Prop @Watch('watchRangSeekBarOption') mRangSeekBarOption: RangSeekBarOption;
40
41  watchRangSeekBarOption() {
42    this.configUI();
43    this.initUIRange();
44  }
45
46  // 播放状态发生变动时刷新界面
47  @Prop @Watch('watchMediaPlayerStatus') mMediaPlayerStatus: MediaPlayerStatus;
48
49  watchMediaPlayerStatus() {
50    if (this.mMediaPlayerStatus) {
51      this.showRedProgress = this.mMediaPlayerStatus.isPlaying;
52      this.touchEnable = !this.mMediaPlayerStatus.isPlaying;
53      this.redLinePosition = {
54        x: this.leftThumbRect[2] + this.transparentWidth * this.mMediaPlayerStatus.redLineProgress,
55        y: this.topPaddingWidth
56      };
57    }
58  }
59
60  // 视频选取范围侦听回调
61  @Require mRangSeekBarListener?: RangSeekBarListener;
62  @State leftThumbWidth: number = 30;
63  @State rightThumbWidth: number = 30;
64  @State topPaddingWidth: number = 10;
65  @State bottomPaddingWidth: number = 10;
66  @State transparentWidth: number = 0;
67  // 可滑动组件的整体宽度
68  @State seekCompWidth: number = 0;
69  // 可滑动组件的整体高度
70  @State seekCompHeight: number = 0;
71  // 整个组件的宽度
72  componentMaxWidth: number = 0;
73  // 整个组件的高度
74  componentMaxHeight: number = 0;
75  // 侦听选择区域变动
76  @Prop @Watch('watchComponentRect') componentRect: Array<number> = [];
77
78  watchComponentRect() {
79    logger.info(TAG, 'watchComponentRect = ' + JSON.stringify(this.componentRect));
80  }
81
82  // 侦听左边区域变动
83  @Prop @Watch('watchLeftThumbRect') leftThumbRect: Array<number> = [];
84
85  watchLeftThumbRect() {
86    logger.info(TAG, 'watchLeftThumb =' + JSON.stringify(this.leftThumbRect));
87  }
88
89  // 侦听右边区域变动
90  @Prop @Watch('watchLeftThumbRect') rightThumbRect: Array<number> = [];
91
92  watchRightThumbRect() {
93    logger.info(TAG, 'watchRightThumbRect =' + JSON.stringify(this.rightThumbRect))
94  }
95
96  // 触摸位置相关参数
97  touchLeftThumb = 1;
98  touchRightThumb = 2;
99  touchHorScroll = 3;
100  touchStatus: number = this.touchHorScroll;
101  touchXOld = 0;
102  touchDeltaX = 0;
103  @State leftThumbPosition: Position = { x: 0, y: 0 };
104  @State rightThumbPosition: Position = { x: 0, y: 0 };
105  @State middenPosition: Position = { x: 0, y: 0 };
106  @State redLinePosition: Position = { x: 0, y: 0 };
107  // 左边可滑动条状物 左边间距
108  leftPadding = 0;
109  // 右边可滑动条状物 右边间距
110  rightPadding = 0;
111  // 左右可滑动条状物 之间的最小间距
112  leftRightPaddingMin = 0;
113  // 左右可滑动条状物 之间的最大间距
114  leftRightPaddingMax = 0;
115  // 一秒钟所占的PX宽度
116  msPxAvg: number = 0;
117  scroller: Scroller = new Scroller();
118  // 滑动速度
119  speed: number = 0;
120  // 播放状态不响应触碰
121  touchEnable: boolean = true;
122  // 手势相关参数界面配置
123  private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.Left | PanDirection.Right });
124  @State leftText: string = '00:00:00';
125  @State rightText: string = '00:00:10';
126  @State leftTextPosition: Position = { x: 0, y: 0 };
127  @State rightTextPosition: Position = { x: 0, y: 0 };
128  // 显示播放进度
129  @State showRedProgress: boolean = false;
130  leftTextWidth: number = 60;
131  rightTextWidth: number = 60;
132
133  aboutToAppear() {
134    this.configUI();
135  }
136
137  build() {
138    Column() {
139      // 选取的时间范围界面
140      Stack() {
141        Text(this.leftText)
142          .id('txt_left_time')
143          .width(this.leftTextWidth)
144          .height($r('app.string.video_trimmer_full_size'))
145          .textAlign(TextAlign.Start)
146          .position(this.leftTextPosition)
147          .fontSize($r('app.integer.video_trimmer_font_size_12'))
148          .fontColor(Color.White)
149
150        Text(this.rightText)
151          .id('txt_right_time')
152          .width(this.rightTextWidth)
153          .height($r('app.string.video_trimmer_full_size'))
154          .textAlign(TextAlign.End)
155          .position(this.rightTextPosition)
156          .fontSize($r('app.integer.video_trimmer_font_size_12'))
157          .fontColor(Color.White)
158      }
159      .width(this.seekCompWidth)
160      .height($r('app.string.video_trimmer_rangeSeekBar_height'))
161
162      Stack() {
163        // 背景框
164        Flex({ justifyContent: FlexAlign.SpaceBetween, direction: FlexDirection.Column }) {
165          Row() {
166          }
167          .width($r('app.string.video_trimmer_full_size'))
168          .height(this.topPaddingWidth)
169          .backgroundColor(Color.White)
170
171          Row() {
172          }
173          .width($r('app.string.video_trimmer_full_size'))
174          .height(this.bottomPaddingWidth)
175          .backgroundColor(Color.White)
176        }
177        .width(this.transparentWidth)
178        .height($r('app.string.video_trimmer_full_size'))
179        .backgroundColor(Color.Transparent)
180        .position(this.middenPosition)
181
182        // 播放进度条
183        Stack() {
184        }
185        .width(1)
186        .height(this.seekCompHeight - this.topPaddingWidth - this.bottomPaddingWidth)
187        .backgroundColor(Color.Red)
188        .visibility(this.showRedProgress ? Visibility.Visible : Visibility.None)
189        .position(this.redLinePosition)
190
191        // 左右选择图片
192        Image($r('app.media.video_trimmer_thumb_handle'))
193          .id('left_thumb_handle')
194          .objectFit(ImageFit.Fill)
195          .width(this.leftThumbWidth)
196          .height($r('app.string.video_trimmer_full_size'))
197          .position(this.leftThumbPosition)
198        Image($r('app.media.video_trimmer_thumb_handle'))
199          .id('right_thumb_handle')
200          .objectFit(ImageFit.Fill)
201          .width(this.leftThumbWidth)
202          .height($r('app.string.video_trimmer_full_size'))
203          .position(this.rightThumbPosition)
204      }
205      .width(this.seekCompWidth)
206      .height('85%')
207      .onAreaChange((oldValue, newValue) => {
208        this.seekCompHeight = newValue.height as number;
209      })
210    }
211    .width($r('app.string.video_trimmer_full_size'))
212    .height($r('app.string.video_trimmer_full_size'))
213    .enabled(this.touchEnable)
214    .onAreaChange((oldValue, newValue) => {
215      this.componentMaxWidth = newValue.width as number;
216      this.componentMaxHeight = newValue.height as number;
217      this.initUIRange();
218    })
219    // 绑定手势
220    .parallelGesture(
221      PanGesture(this.panOption)// 根据手势计算选择视频区间的时间位置
222        .onActionStart((event?: GestureEvent) => {
223          if (this.touchInThumb(event)) {
224            // 左边滑块移动
225            if (this.touchInLeftThumb(event)) {
226              this.touchStatus = this.touchLeftThumb;
227            } else if (this.touchInRightThumb(event)) {
228              // 右边滑块移动
229              this.touchStatus = this.touchRightThumb;
230            }
231          }
232          this.touchXOld = this.clearUndefined(event?.offsetX);
233        })
234        .onActionUpdate((event?: GestureEvent) => {
235          let touchXNew: number = this.clearUndefined(event?.offsetX);
236          let delTax: number = touchXNew - this.touchXOld;
237          // 左边滑块移动
238          if (this.touchStatus == this.touchLeftThumb) {
239            this.leftThumbUpdate(delTax);
240          } else if (this.touchStatus == this.touchRightThumb) {
241            // 右边滑块移动
242            this.rightThumbUpdate(delTax);
243          } else if (this.touchStatus == this.touchHorScroll) {
244            this.scrollUpdate(delTax);
245          }
246          this.touchDeltaX = delTax;
247          this.touchXOld = this.clearUndefined(event?.offsetX);
248        })
249        .onActionEnd((event?: GestureEvent) => {
250          this.touchStatus = this.touchHorScroll;
251          this.onRangeStopScrollChanged();
252        })
253    )
254    .parallelGesture(
255      SwipeGesture({ direction: SwipeDirection.Horizontal })
256        .onAction((event?: GestureEvent) => {
257          this.speed = this.clearUndefined(event?.speed);
258
259          logger.info(TAG, 'SwipeGesture onAction speed =' + this.speed);
260        })
261    )
262  }
263
264  // 配置界面UI位置
265  configUI() {
266    if (this.mRangSeekBarOption.mThumbWidth) {
267      this.leftThumbWidth = this.mRangSeekBarOption.mThumbWidth;
268      this.rightThumbWidth = this.mRangSeekBarOption.mThumbWidth;
269    }
270    if (this.mRangSeekBarOption.mPaddingTopBottom) {
271      this.topPaddingWidth = this.mRangSeekBarOption.mPaddingTopBottom;
272      this.bottomPaddingWidth = this.mRangSeekBarOption.mPaddingTopBottom;
273    }
274  }
275
276  // 初始化视频剪辑范围
277  initUIRange() {
278    this.componentRect = [0, 0, this.componentMaxWidth, this.componentMaxHeight];
279
280    this.leftThumbRect = [0, 0, this.leftThumbWidth, this.componentMaxHeight];
281    this.leftThumbPosition = { x: this.leftThumbRect[0], y: this.leftThumbRect[1] };
282    this.leftTextPosition = { x: this.leftThumbRect[0], y: this.leftThumbRect[1] };
283
284    if (this.mRangSeekBarOption) {
285      this.msPxAvg = ((this.componentMaxWidth - (this.leftThumbWidth + this.rightThumbWidth)) * 1000.0) /
286      this.mRangSeekBarOption.mMaxCountRange;
287      this.leftRightPaddingMax =
288        ((this.mRangSeekBarOption.mMaxShootTime * 1.0) / this.mRangSeekBarOption.mMaxCountRange) *
289          (this.componentMaxWidth - (this.leftThumbWidth + this.rightThumbWidth));
290      this.leftRightPaddingMin =
291        ((this.mRangSeekBarOption.mMinShootTime * 1.0) / this.mRangSeekBarOption.mMaxCountRange) *
292          (this.componentMaxWidth - (this.leftThumbWidth + this.rightThumbWidth));
293    }
294
295    // 获取当前右边界
296    let rightMax = this.mRangSeekBarOption.mRightProgressPos / 1000;
297    // 视频时间小于录制最大时间情况
298    if (rightMax <= this.mRangSeekBarOption.mMaxShootTime) {
299      // 最大距离需要重置为 视频时间
300      this.leftRightPaddingMax = ((rightMax * 1.0) / this.mRangSeekBarOption.mMaxCountRange) *
301        (this.componentMaxWidth - (this.leftThumbWidth + this.rightThumbWidth));
302    } else {
303      rightMax = this.mRangSeekBarOption.mMaxShootTime;
304    }
305
306    // 由于mMaxShootTime <= mMaxCountRange 所以这个值必定在 当前页面内
307    let right2LeftDistance = rightMax * (this.msPxAvg / 1000.0);
308
309    this.rightThumbRect =
310      [this.leftThumbRect[2] + right2LeftDistance, 0, this.leftThumbRect[2] + right2LeftDistance + this.rightThumbWidth,
311        this.componentMaxHeight];
312    this.rightThumbPosition = { x: this.rightThumbRect[0], y: this.rightThumbRect[1] };
313    this.rightTextPosition = { x: this.rightThumbRect[2] - this.rightTextWidth, y: this.rightThumbRect[1] };
314
315    this.seekCompWidth = this.componentMaxWidth;
316    this.middenPosition = { x: this.leftThumbRect[2], y: this.leftThumbRect[1] };
317    this.transparentWidth = right2LeftDistance;
318    this.leftText = this.showThumbText(this.mRangSeekBarOption.mLeftProgressPos);
319    this.rightText = this.showThumbText(Math.min(this.mRangSeekBarOption.mRightProgressPos,
320      this.mRangSeekBarOption.mMaxShootTime * 1000));
321  }
322
323  // 更新左边滑块位置
324  leftThumbUpdate(deltaX: number) {
325    let deltaPx = deltaX;
326    // 左边距
327    if (deltaPx <= 0 && ((this.leftThumbRect[0] + deltaPx) <= this.leftPadding)) {
328      deltaPx = this.leftPadding - this.leftThumbRect[0];
329    }
330    // 右边距
331    if (deltaPx >= 0 && ((this.leftThumbRect[2] + deltaPx) >= (this.rightThumbRect[0] - this.leftRightPaddingMin))) {
332      deltaPx = (this.rightThumbRect[0] - this.leftRightPaddingMin) - this.leftThumbRect[2];
333    }
334    // 左边界 新增与右thumb的最大边距
335    if (deltaPx <= 0 && ((this.leftThumbRect[2] + deltaPx) <= (this.rightThumbRect[0] - this.leftRightPaddingMax))) {
336      deltaPx = (this.rightThumbRect[0] - this.leftRightPaddingMax) - this.leftThumbRect[2];
337    }
338
339    let newArea = [
340      (this.leftThumbRect[0] + deltaPx),
341      this.leftThumbRect[1],
342      (this.leftThumbRect[2] + deltaPx),
343      this.leftThumbRect[3]];
344
345    this.leftThumbRect = newArea;
346    this.leftThumbPosition = { x: newArea[0], y: newArea[1] };
347    this.leftTextPosition = { x: newArea[0], y: newArea[1] };
348
349    this.middenPosition = { x: this.leftThumbRect[2], y: this.leftThumbRect[1] };
350    this.transparentWidth = this.rightThumbRect[0] - this.leftThumbRect[2];
351    // 更新选取视频时间
352    this.onRangeValueChanged();
353  }
354
355  // 调用时间转换函数显示时间
356  showThumbText(time: number): string {
357    return TimeUtils.msToHHMMSS(time);
358  }
359
360  // 选取时间变动事件
361  onRangeValueChanged() {
362    let x0: number = this.scroller.currentOffset().xOffset;
363    let start: number = x0 + this.leftThumbRect[2] - this.leftThumbWidth;
364    let end: number = start + this.transparentWidth;
365
366    let startTime: number = start * CommonConstants.US_ONE_SECOND / this.msPxAvg;
367    this.leftText = this.showThumbText(startTime);
368    let endTime: number = end * CommonConstants.US_ONE_SECOND / this.msPxAvg;
369    this.rightText = this.showThumbText(endTime);
370
371    if (this.mRangSeekBarListener) {
372      this.mRangSeekBarListener.onRangeSeekBarValuesChanged(startTime, endTime);
373    }
374  }
375
376  // 视频范围选择结束
377  onRangeStopScrollChanged() {
378    let start: number = this.scroller.currentOffset().xOffset;
379    // 计算开始位置
380    let startPosition: number = start * 1000.0 / this.msPxAvg
381    startPosition = Math.max(0, Math.floor(startPosition));
382
383    // 计算结束位置
384    let endPosition: number = startPosition + this.mRangSeekBarOption.mMaxCountRange + 5;
385    let maxEnd: number = Math.floor(this.mRangSeekBarOption.mRightProgressPos / 1000);
386    endPosition = Math.min(maxEnd, endPosition);
387
388    if (this.mRangSeekBarListener) {
389      this.mRangSeekBarListener.onRangeSeekBarScrollChanged(startPosition, endPosition);
390    }
391  }
392
393  // 更新右边滑块位置
394  rightThumbUpdate(deltaX: number) {
395    let deltaPx = deltaX;
396    // 右边距
397    if (deltaPx >= 0 && ((this.rightThumbRect[2] + deltaPx) >= this.componentMaxWidth - this.rightPadding)) {
398      deltaPx = this.componentMaxWidth - this.rightPadding - this.rightThumbRect[2];
399    }
400    // 左边距
401    if (deltaPx <= 0 && ((this.rightThumbRect[0] + deltaPx) <= (this.leftThumbRect[2] + this.leftRightPaddingMin))) {
402      deltaPx = (this.leftThumbRect[2] + this.leftRightPaddingMin) - this.rightThumbRect[0];
403    }
404    // 右边距 新增 与leftThumb最大距离
405    if (deltaPx >= 0 && ((this.rightThumbRect[0] + deltaPx) >= (this.leftThumbRect[2] + this.leftRightPaddingMax))) {
406      deltaPx = (this.leftThumbRect[2] + this.leftRightPaddingMax) - this.rightThumbRect[0];
407    }
408
409    let newArea = [
410      (this.rightThumbRect[0] + deltaPx),
411      this.rightThumbRect[1],
412      (this.rightThumbRect[2] + deltaPx),
413      this.rightThumbRect[3]];
414
415    this.rightThumbRect = newArea;
416    this.rightThumbPosition = { x: newArea[0], y: newArea[1] };
417    this.rightTextPosition = { x: newArea[2] - this.rightTextWidth, y: newArea[1] };
418
419    this.middenPosition = { x: this.leftThumbRect[2], y: this.leftThumbRect[1] }
420    this.transparentWidth = this.rightThumbRect[0] - this.leftThumbRect[2];
421    // 更新选取视频时间
422    this.onRangeValueChanged();
423  }
424
425  scrollUpdate(deltaX: number) {
426    let deltaPx = -deltaX;
427    this.scroller.scrollBy(deltaPx, 0);
428    this.onRangeValueChanged();
429  }
430
431  // 判断触碰事件有效
432  touchInThumb(event?: GestureEvent): boolean {
433    if (this.touchInLeftThumb(event) || this.touchInRightThumb(event)) {
434      return true;
435    } else {
436      return false;
437    }
438  }
439
440  // 判断左边滑块滑动
441  touchInLeftThumb(event?: GestureEvent): boolean {
442
443    logger.info(TAG, ' touchInLeftThumb');
444    let pointX: number = this.clearUndefined(event?.fingerList[0].localX);
445    let pointY: number = this.clearUndefined(event?.fingerList[0].localY);
446    return this.pointInArea(pointX, pointY, this.leftThumbRect);
447  }
448
449  // 判断右边滑块滑动
450  touchInRightThumb(event?: GestureEvent): boolean {
451
452    logger.info(TAG, ' touchInRightThumb');
453    let pointX: number = this.clearUndefined(event?.fingerList[0].localX)
454    let pointY: number = this.clearUndefined(event?.fingerList[0].localY)
455    return this.pointInArea(pointX, pointY, this.rightThumbRect);
456  }
457
458  // 区域判断
459  pointInArea(x: number, y: number, area: Array<number>): boolean {
460
461    logger.info(TAG, ' x=' + x + ' y=' + y + ' area=' + area)
462    if (area.length == 4) {
463      if (x >= area[0] && x <= area[2]) {
464        return true;
465      } else {
466        return false;
467      }
468    } else {
469      return false;
470    }
471  }
472
473  // 判断位置坐标值
474  clearUndefined(num: number | undefined) {
475    if (num == undefined) {
476      return 0;
477    }
478    return num;
479  }
480}