• 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
16import { memo, __memo_context_type, __memo_id_type } from '@ohos.arkui.stateManagement' // should be insert by ui-plugins
17import { Text, TextAttribute, Column, Component, Button, ButtonAttribute, ClickEvent, UserView, Entry, Tabs, TabContent, SubTabBarStyle,
18  Row, ForEach, Position, Builder, Margin, BarMode, OnTabsAnimationStartCallback, TabsAnimationEvent, TabsOptions,animateTo,Alignment,
19  Color, FlexAlign, $r, Image, SizeOptions, Position, AnimateParam, Flex,FlexDirection,FlexAlign,ItemAlign,VideoController, TranslateOptions, SeekMode, VoidCallback, PlaybackInfo, PreparedInfo,
20  Video,$rawfile, GestureType, PanGesture, GestureEvent, RelativeContainer, Color, HitTestMode, Visibility} from '@ohos.arkui.component'  // TextAttribute should be insert by ui-plugins
21import { State, Link, StateDecoratedVariable, MutableState, stateOf, observableProxy } from '@ohos.arkui.stateManagement' // should be insert by ui-plugins
22import {Callback} from '@ohos.arkui.component'
23import hilog from '@ohos.hilog'
24import { UIContext, Router } from '@ohos.arkui.UIContext'
25import router from '@ohos.router'
26
27@Entry
28@Component
29export struct TabContentOverFlowComponent {
30  @State tabArray: string[] = ['首页', '视频',
31    '商城', '我的'];
32  @State imageArray: string[] =
33    ['app.media.tabcontentoverflow_homepage', 'app.media.tabcontentoverflow_video', 'app.media.tabcontentoverflow_mall',
34      'app.media.tabcontentoverflow_mine'];
35  @State imageClickArray: string[] =
36    ['app.media.tabcontentoverflow_homepage_filled', 'app.media.tabcontentoverflow_video_filled',
37      'app.media.tabcontentoverflow_mall_filled', 'app.media.tabcontentoverflow_mine_filled'];
38  @State index: number = 1;
39  @State offsetX: number = 0; // 用来记录实时拖动的距离
40  @State positionX: number = 0; // 用来记录拖动后的距离
41  @State playTime: string = ''; // 用来记录现在播放的时间
42  @State totalTime: number = 0; // 用来记录视频全部时长
43  @State isPlay: boolean = false; // 用来控制播放状态
44  @State isTouch: boolean = false; // 是否处于拖动状态
45  @State isTextButtonVisible: boolean = true;
46  private isSeek: boolean = false; // 是否拖拽跳转
47  private videoController: VideoController = new VideoController();
48  private screenW: number = 480; // 获取设备宽度
49
50  private dragAnimation() {
51    this.getUIContext()?.animateTo({
52      duration: 300,
53    }, () => {
54      hilog.info(0x0000, 'testTag', 'animateTo动画触发');
55      this.isTouch = true;
56    });
57  }
58
59  @Builder
60  videoTabContent() {
61    /**
62     * TODO: 高性能知识点: 界面嵌套带来了渲染和计算的大量开销,造成性能的衰退。使用扁平化布局优化嵌套层级,建议采用相对布局RelativeContainer进行扁平化布局,有效减少容器的嵌套层级,减少组件的创建时间。
63     */
64    Column() {
65      Row() {
66        Text(' ← 返回')
67      }
68      .margin(3)
69      .width('100%')
70      .onClick((e: ClickEvent) => {
71        this.getUIContext().getRouter().back();
72      })
73      Video({
74        src: $r('app.media.tabcontentoverflow_play_video'),
75        controller: this.videoController
76      })
77        .height('90%')
78        .width('100%')
79        .autoPlay(false)
80        .controls(false)
81          .onPrepared((event: PreparedInfo) => {
82            if (event !== undefined) {
83              this.totalTime = event.duration;
84            }
85          } as Callback<PreparedInfo>)
86          .onFinish(() => {
87            this.isPlay = false;
88          } as VoidCallback)
89          .onUpdate((event: PlaybackInfo) => {
90            hilog.info(0x0000, 'testTag', '更新:'+event.time);
91            if (event !== undefined) {
92              if (!this.isTouch) {
93                if (!this.isSeek) {
94                  // 当没有进行拖动进度条时,进度条根据播放进度进行变化。
95                  this.offsetX =
96                    event.time / this.totalTime * (this.screenW - 30);
97                  this.positionX = this.offsetX;
98                } else {
99                  this.isSeek = false;
100                }
101              }
102            }
103          } as Callback<PlaybackInfo>)
104        .id('video')
105      // 播放按钮
106      Image($r('app.media.tabcontentoverflow_play'))
107        .width(50)
108        .height(50)
109        .position({x: '45%', y: '45%'} as Position)
110        .id('image')
111        .onClick((e: ClickEvent) => {
112          if (this.isPlay) {
113            this.isPlay = false;
114            this.videoController.pause();
115          } else {
116            this.isPlay = true;
117            this.videoController.start();
118          }
119        })
120        .visibility(this.isPlay ? Visibility.Hidden : Visibility.Visible)
121      // 拖动进度条时展示现在播放哪个到具体的时间点
122      Text('00:' + this.playTime)
123        .fontSize(20)
124        .width(100)
125        .height(30)
126        .id('playTimeText')
127        .fontColor('#ffff0000')
128        .visibility(this.isTouch ? Visibility.Visible : Visibility.Hidden)
129        .margin({
130          left: -100,
131          top: -100
132        } as Margin)
133      // 拖动进度条时展示视频总时长
134      Text('/00:' + this.totalTime)
135        .fontSize(20)
136        .width(100)
137        .height(30)
138        .id('totalTimeText')
139        .fontColor('#ffffffff')
140        .visibility(this.isTouch ? Visibility.Visible : Visibility.Hidden)
141      // TODO: 知识点:使用三个Text组件来模拟播放进度条,第一个text位置不变,宽度不变。第二个text根据this.offsetX来变换宽度。第三个text根据this.offsetX来translate该组件在x轴的位置。
142      RelativeContainer() {
143        Text()
144          .width(this.screenW - 30)
145          .height(this.isTouch ? 20 :
146            5)
147          .borderRadius(this.isTouch ? 0 :
148            5)
149          .backgroundColor('#804e4c4d')
150          .translate({
151            y: this.isTouch ? -15 :
152              0
153          } as TranslateOptions)
154          .id('text1')
155          .margin({
156            top: 8,
157            left: 15
158          } as Margin)
159        Text()
160          .width(this.offsetX)
161          .height(this.isTouch ? 20 :
162            5)
163          .borderRadius(this.isTouch ? 0 :
164            5)
165          .backgroundColor('#999999')
166          .translate({
167            y: this.isTouch ? -15 : 0
168          } as TranslateOptions)
169          .id('text2')
170        Text()
171          .width(20)
172          .height(20)
173          .borderRadius(10)
174          .backgroundColor('#999999')
175          .translate({ x: this.offsetX } as TranslateOptions)
176          .visibility(this.isTextButtonVisible ? Visibility.Visible : Visibility.None)
177          .id('text3')
178          .margin({
179            top: -7.5,
180            left: -10
181          } as Margin)
182      }
183      .id('RelativeContainer')
184      .margin({
185        top: 30,
186      } as Margin)
187      .width(this.screenW)
188      .height(70)
189      // 左右拖动触发该手势事件
190      .gesture(
191        PanGesture()
192          .onActionStart((event: GestureEvent) => {
193            hilog.info(0x0000, 'testTag', 'onActionStart触摸准备');
194            this.dragAnimation();
195            this.isTextButtonVisible = false;
196            this.isSeek = true;
197          })/**
198           * TODO: 性能知识点: onActionUpdate是系统高频回调函数,避免在函数中进行冗余或耗时操作,例如应该减少或避免在函数打印日志,会有较大的性能损耗。
199           * 合理使用系统接口,避免冗余操作: README.md
200           */
201          .onActionUpdate((event: GestureEvent) => {
202            let playTime =
203              Math.floor(this.offsetX / (this.screenW - 30) *
204              this.totalTime);
205            this.offsetX = this.positionX + event.offsetX;
206            if (this.offsetX <= 0) {
207              this.offsetX = 0;
208            }
209            if (this.offsetX >= this.screenW - 30) {
210              this.offsetX = this.screenW - 30;
211            }
212            if (playTime >= 10) {
213              this.playTime = (playTime as Number).toString();
214            } else {
215              this.playTime = '0' + (playTime as Number).toString();
216            }
217          })
218          .onActionEnd((event: GestureEvent) => {
219            hilog.info(0x0000, 'testTag', 'onActionEnd触摸结束');
220            if (this.positionX === this.offsetX) { // 进度条未发生改变
221              this.isSeek = false;
222            } else {
223              // 拖动进度条发生改变后通过this.videoController.setCurrentTime来精准定位视频现在播放位置。
224              this.videoController.setCurrentTime(Number(((this.offsetX /
225                (this.screenW - 30) * this.totalTime) as Number).toFixed(3)),
226                SeekMode.Accurate);
227              this.positionX = this.offsetX;
228            }
229            this.isTextButtonVisible = true;
230            this.isTouch = false;
231          }) as GestureType
232      )
233    }
234
235  }
236
237  build() {
238    Column() {
239      // TODO: 知识点:将barHeight设置为0,预留60vp给自定义tabBar,TabContent的高度则可以使用calc(100% - 60vp)获取。
240      Tabs({ index: this.index } as TabsOptions) {
241        TabContent() {
242          Text($r('app.string.custom_home'))
243            .fontSize(20)
244        }
245        .align(Alignment.Center)
246        .height('calc(100% - 60vp)')
247        .width('100%')
248
249        TabContent() {
250          this.videoTabContent();
251        }
252        .align(Alignment.Top)
253
254        TabContent() {
255          Text($r('app.string.custom_store'))
256            .fontSize(20)
257        }
258        .align(Alignment.Center)
259        .height('calc(100% - 60vp)')
260        .width('100%')
261
262        TabContent() {
263          Text($r('app.string.custom_my'))
264            .fontSize(20)
265        }
266        .align(Alignment.Center)
267        .height('calc(100% - 60vp)')
268        .width('100%')
269      }
270      // TODO: 知识点:将zIndex设置为2,TabContent将在tabBar之上,显示的效果就是TabContent外溢的部分在tabBar上。
271      .zIndex(2)
272      .scrollable(false)
273      .barHeight(1)
274      .animationDuration(100)
275      // TODO: 知识点:hitTestBehavior属性可以实现在复杂的多层级场景下,一些组件能够响应手势和事件,而一些组件不能响应手势和事件。HitTestMode.Transparent的效果为,自身响应触摸测试,不会阻塞兄弟节点的触摸测试。
276      .hitTestBehavior(HitTestMode.Transparent)
277      .id('tabs')
278
279      // 页签
280      Row() {
281        ForEach(this.tabArray, (item: string, index: number) => {
282          Column() {
283            Image(this.index === index ? $r(this.imageClickArray[index]) : $r(this.imageArray[index]))
284              .width(30)
285              .height(30)
286            Text(item)
287              .fontSize(12)
288              .fontColor(this.index === index ? '#e40d0d' :
289                '#FFFFFFFF')
290          }
291          .width(50)
292          .margin({ top: 10 } as Margin)
293          // 为将底部视图扩展到非安全区域,可将原本60vp的高度设置为100vp。
294          .height(100)
295          .onClick((e: ClickEvent) => {
296            hilog.info(0x0000, 'testTag', 'tabs点击');
297            this.index = index;
298          })
299        })
300      }
301      .width('100%')
302      .backgroundColor('#FF000000')
303      .margin({top: '-60vp'} as Margin)
304      .justifyContent(FlexAlign.SpaceAround)
305      .id('tabbar')
306    }
307  }
308}