1/* 2 * Copyright (c) 2023 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 curves from '@ohos.curves'; 17import PiPWindow from '@ohos.PiPWindow'; 18import pip from '@ohos.pip'; 19 20const TAG: string = 'PiPVideo'; 21const TIMEOUT: number = 3000; 22 23@Styles 24function fillSize() { 25 .size({ width: '100%', height: '100%' }) 26} 27 28@Entry 29@Component 30export struct PiPVideo { 31 xComponentId: string = 'pip'; 32 windowType: PiPWindow.PiPTemplateType = PiPWindow.PiPTemplateType.VIDEO_PLAY; 33 private hideEventId: number = -1; 34 @State private showControl: boolean = false; 35 private xComponentController: XComponentController = new XComponentController(); 36 private surfaceId: string = ''; 37 private controlTransEffect: TransitionEffect = TransitionEffect.OPACITY; 38 @Provide @Watch('onHideControlNow') hideControlNow: boolean = false; 39 @Provide @Watch('onHideControlDelay') hideControlDelay: boolean = false; 40 41 onHideControlNow() { 42 if (this.hideControlNow) { 43 this.switchToHideWithoutAnime(); 44 } 45 this.hideControlNow = false; 46 } 47 48 onHideControlDelay() { 49 if (this.hideControlDelay) { 50 this.delayHide(); 51 } 52 this.hideControlDelay = false; 53 } 54 55 switchToShow() { 56 animateTo({ curve: curves.responsiveSpringMotion(0.25, 1) }, () => { 57 this.showControl = true; 58 }); 59 this.delayHide(); 60 } 61 62 switchToHide() { 63 if (this.hideEventId !== -1) { 64 clearTimeout(this.hideEventId); 65 } 66 animateTo({ curve: curves.responsiveSpringMotion(0.25, 1) }, () => { 67 this.showControl = false; 68 }); 69 } 70 71 switchToHideWithoutAnime() { 72 if (this.hideEventId !== -1) { 73 clearTimeout(this.hideEventId); 74 } 75 this.showControl = false; 76 } 77 78 delayHide() { 79 if (this.hideEventId !== -1) { 80 clearTimeout(this.hideEventId); 81 } 82 this.hideEventId = this.showControl ? 83 setTimeout(() => { 84 animateTo({ curve: curves.responsiveSpringMotion(0.25, 1) }, () => { 85 this.showControl = false; 86 }); 87 }, TIMEOUT) : 88 -1; 89 } 90 91 build() { 92 Stack() { 93 XComponent({ id: this.xComponentId, type: 'surface', controller: this.xComponentController }) 94 .onLoad(() => { 95 pip.initXComponentController(this.xComponentController); 96 console.debug(TAG, 'XComponent onLoad done'); 97 }) 98 .fillSize(); 99 100 RelativeContainer() { 101 Stack() 102 .fillSize() 103 .id('fill_stack') 104 if (this.showControl) { 105 RelativeContainer() { 106 DefaultControl(); 107 VideoControl(); 108 } 109 .fillSize() 110 .transition(this.controlTransEffect) 111 .alignRules({ 112 top: { anchor: '__container__', align: VerticalAlign.Top }, 113 right: { anchor: '__container__', align: HorizontalAlign.End } 114 }) 115 .id('control_inner') 116 } 117 } 118 .fillSize() 119 .id('control') 120 .gesture( 121 GestureGroup(GestureMode.Exclusive, 122 TapGesture({ count: 2 }) 123 .onAction((event: GestureEvent) => { 124 this.switchToHideWithoutAnime(); 125 pip.processScale(); 126 }), 127 TapGesture({ count: 1 }) 128 .onAction((event: GestureEvent) => { 129 if (this.showControl) { 130 this.switchToHide(); 131 } else { 132 this.switchToShow(); 133 } 134 }), 135 PanGesture() 136 .onActionStart((event: GestureEvent) => { 137 this.switchToHide(); 138 pip.startMove(); 139 }) 140 ) 141 ) 142 } 143 .fillSize() 144 } 145} 146 147@Component 148struct DefaultControl { 149 @Consume hideControlNow: boolean; 150 151 build() { 152 RelativeContainer() { 153 Button({ type: ButtonType.Circle }) { 154 Image($r('sys.media.ohos_ic_public_close')) 155 .size({ width: 24, height: 24 }) 156 .fillColor($r('sys.color.ohos_id_color_primary_contrary')) 157 .objectFit(ImageFit.Contain) 158 } 159 .backgroundColor('#00FFFFFF') 160 .size({ width: 24, height: 24 }) 161 .margin(12) 162 .alignRules({ 163 center: { anchor: '__container__', align: VerticalAlign.Center }, 164 left: { anchor: '__container__', align: HorizontalAlign.Start } 165 }) 166 .id('control_exit') 167 .responseRegion({ x: '-50%', y: '-50%', width: '200%', height: '200%' }) 168 .onClick(() => { 169 this.hideControlNow = true; 170 pip.close(); 171 console.debug(TAG, 'action: exit'); 172 }) 173 174 Button({ type: ButtonType.Circle }) { 175 Image($r('sys.media.ohos_ic_public_restore')) 176 .fillColor($r('sys.color.ohos_id_color_primary_contrary')) 177 .objectFit(ImageFit.Contain) 178 } 179 .backgroundColor('#00FFFFFF') 180 .size({ width: 24, height: 24 }) 181 .margin(12) 182 .alignRules({ 183 center: { anchor: '__container__', align: VerticalAlign.Center }, 184 right: { anchor: '__container__', align: HorizontalAlign.End } 185 }) 186 .id('control_restore') 187 .responseRegion({ x: '-50%', y: '-50%', width: '200%', height: '200%' }) 188 .onClick(() => { 189 this.hideControlNow = true; 190 pip.restore(); 191 console.debug(TAG, 'action: restore'); 192 }) 193 } 194 .width('100%') 195 .height(48) 196 .linearGradient({ 197 angle: 180, 198 colors: [['#30000000', 0.0], ['#00000000', 1.0]] 199 }) 200 .alignRules({ 201 top: { anchor: '__container__', align: VerticalAlign.Top }, 202 left: { anchor: '__container__', align: HorizontalAlign.Start } 203 }) 204 .id('default_control') 205 } 206} 207 208 209@Component 210struct VideoControl { 211 @State isPlaying: boolean = true; 212 @State shouldShowNextAndPrev: boolean = true; 213 @State horMargin: number = 12; 214 @Consume hideControlDelay: boolean; 215 216 build() { 217 RelativeContainer() { 218 Button({ type: ButtonType.Circle }) { 219 Image(this.isPlaying ? $r('sys.media.ohos_ic_public_pause') : $r('sys.media.ohos_ic_public_play')) 220 .fillColor($r('sys.color.ohos_id_color_primary_contrary')) 221 .objectFit(ImageFit.Contain) 222 } 223 .backgroundColor('#00FFFFFF') 224 .size({ width: 24, height: 24 }) 225 .margin({ top: 12, bottom: 12 }) 226 .alignRules({ 227 center: { anchor: '__container__', align: VerticalAlign.Center }, 228 middle: { anchor: '__container__', align: HorizontalAlign.Center } 229 }) 230 .id('control_play') 231 .responseRegion({ x: '-50%', y: '-50%', width: '200%', height: '200%' }) 232 .onClick(() => { 233 this.isPlaying = !this.isPlaying; 234 this.hideControlDelay = true; 235 pip.triggerAction('playbackStateChanged'); 236 console.debug(TAG, 'action: play or pause'); 237 }) 238 239 Button({ type: ButtonType.Circle }) { 240 Image($r('sys.media.ohos_ic_public_play_last')) 241 .fillColor($r('sys.color.ohos_id_color_primary_contrary')) 242 .objectFit(ImageFit.Contain) 243 } 244 .backgroundColor('#00FFFFFF') 245 .size({ width: 24, height: 24 }) 246 .margin({ left: this.horMargin, right: this.horMargin * 2, top: 12, bottom: 12 }) 247 .visibility(this.shouldShowNextAndPrev ? Visibility.Visible : Visibility.None) 248 .alignRules({ 249 center: { anchor: '__container__', align: VerticalAlign.Center }, 250 right: { anchor: 'control_play', align: HorizontalAlign.Start } 251 }) 252 .id('control_play_last') 253 .responseRegion({ x: '-50%', y: '-50%', width: '200%', height: '200%' }) 254 .onClick(() => { 255 this.hideControlDelay = true; 256 pip.triggerAction('previousVideo'); 257 console.debug(TAG, 'action: play last'); 258 }) 259 260 Button({ type: ButtonType.Circle }) { 261 Image($r('sys.media.ohos_ic_public_play_next')) 262 .fillColor($r('sys.color.ohos_id_color_primary_contrary')) 263 .objectFit(ImageFit.Contain) 264 } 265 .backgroundColor('#00FFFFFF') 266 .size({ width: 24, height: 24 }) 267 .margin({ left: this.horMargin * 2, right: this.horMargin, top: 12, bottom: 12 }) 268 .visibility(this.shouldShowNextAndPrev ? Visibility.Visible : Visibility.None) 269 .alignRules({ 270 center: { anchor: '__container__', align: VerticalAlign.Center }, 271 left: { anchor: 'control_play', align: HorizontalAlign.End } 272 }) 273 .id('control_play_next') 274 .responseRegion({ x: '-50%', y: '-50%', width: '200%', height: '200%' }) 275 .onClick(() => { 276 this.hideControlDelay = true; 277 pip.triggerAction('nextVideo'); 278 console.debug(TAG, 'action: play next'); 279 }) 280 } 281 .width('100%') 282 .height(48) 283 .linearGradient({ 284 angle: 0, 285 colors: [['#30000000', 0.0], ['#00000000', 1.0]] 286 }) 287 .onAreaChange((oldValue, newValue) => { 288 if (newValue.width < 104) { 289 this.shouldShowNextAndPrev = false; 290 } else if (newValue.width < 152) { 291 this.horMargin = (newValue.width as number - 104) / 4; 292 this.shouldShowNextAndPrev = true; 293 } else { 294 this.horMargin = 12; 295 this.shouldShowNextAndPrev = true; 296 } 297 }) 298 .alignRules({ 299 bottom: { anchor: '__container__', align: VerticalAlign.Bottom }, 300 left: { anchor: '__container__', align: HorizontalAlign.Start } 301 }) 302 .id('video_control') 303 } 304} 305