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}