1/* 2 * Copyright (c) 2022 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 Matrix4 from '@ohos.matrix4'; 17import router from '@system.router'; 18import MediaLib from '@ohos.multimedia.mediaLibrary'; 19import { MediaDataItem } from '@ohos/base/src/main/ets/data/MediaDataItem'; 20import { EventPipeline } from '../utils/EventPipeline'; 21import { Broadcast } from '@ohos/base/src/main/ets/utils/Broadcast'; 22import { Log } from '@ohos/base/src/main/ets/utils/Log'; 23import { Constants } from '../constants/Constants'; 24import screenManager from '@ohos/base/src/main/ets/manager/ScreenManager'; 25import { RouterConstants } from '@ohos/base/src/main/ets/constants/RouterConstants'; 26import { startTrace, finishTrace } from '@ohos/base/src/main/ets/utils/TraceControllerUtils'; 27import { LoadingPanel } from './LoadingPanel'; 28import { VideoIcon } from './VideoIcon'; 29import { MediaConstants } from '@ohos/base/src/main/ets/constants/MediaConstants'; 30import { getThumbnail } from '../model/ThumbnailModel'; 31 32const TAG = "PhotoItem" 33 34@Component 35export struct PhotoItem { 36 item: MediaDataItem; 37 pageName: string 38 @State matrix: Matrix4.Matrix4Transit = Matrix4.identity().copy(); 39 @State photoItemDirection: PanDirection = PanDirection.Vertical; 40 @Consume broadCast: Broadcast; 41 @State objectFit: ImageFit = ImageFit.Contain; 42 @State thumbnail: string = ''; 43 @State ratio: number = 1.0; 44 @State showError: boolean = false; 45 @Consume isPullingDown: boolean; 46 @Consume pageFrom: number; 47 @Consume('transitionIndex') @Watch('onTransitionChange') updateTransition: number; 48 private eventPipeline: EventPipeline; 49 private animationOption: any = null; 50 private animationEndMatrix: any = null; 51 @Provide isLoading: boolean = true; 52 private imgScale: number = 1; 53 private firstLoad: boolean = true; 54 private preItem = { rotate: 0, height: 0, width: 0 }; 55 private timeStamp: string = ''; 56 57 onTransitionChange() { 58 Log.info(TAG, `onTransitionChange, ${this.updateTransition} ${this.item.index}`); 59 this.updateThumbnail() 60 if (this.updateTransition == this.item.index) { 61 this.broadCast.emit(Constants.PHOTO_SHOW_STATE, [!this.showError]); 62 } else { 63 this.isPullingDown = false; 64 } 65 // reset matrix 66 if (this.imgScale != 1) { 67 this.matrix = Matrix4.identity().scale({ 68 x: this.imgScale, 69 y: this.imgScale 70 }).copy(); 71 this.eventPipeline && this.eventPipeline.setDefaultScale(this.imgScale); 72 } else { 73 this.matrix = Matrix4.identity().copy(); 74 this.eventPipeline && this.eventPipeline.reset(); 75 } 76 Log.info(TAG, `onTransitionChange end, ${this.updateTransition} ${this.item.index}`); 77 } 78 79 aboutToAppear(): void { 80 Log.info(TAG, `aboutToAppear`); 81 this.timeStamp = (new Date()).valueOf().toString(); 82 this.eventPipeline = new EventPipeline(this.broadCast, this.item, this.timeStamp, this.updateMatrix.bind(this)); 83 this.matrix = Matrix4.identity().copy(); 84 this.firstLoad = true; 85 this.isLoading = true; 86 this.thumbnail = this.item.getThumbnail(this.item.imgWidth, this.item.imgHeight); 87 this.item.load(true).then(() => { 88 this.isLoading = false 89 this.updateThumbnail() 90 Log.info(TAG, `aboutToAppear loadFinish thumbnail ${this.thumbnail}`); 91 }) 92 // register event handling 93 this.broadCast.on(Constants.TOUCH_EVENT + this.item.uri + this.timeStamp, (matrix: any) => { 94 this.matrix = matrix; 95 }); 96 97 this.broadCast.on(Constants.DIRECTION_CHANGE + this.item.uri + this.timeStamp, (direction: PanDirection) => { 98 Log.info(TAG, `direction: ${direction}`); 99 this.photoItemDirection = direction; 100 }); 101 102 this.broadCast.on(Constants.ANIMATION_EVENT + this.item.uri + this.timeStamp, (animationOption: any, animationEndMatrix: any) => { 103 Log.info(TAG, `animationOption: ${JSON.stringify(animationOption)}`); 104 this.animationOption = animationOption; 105 this.animationEndMatrix = animationEndMatrix; 106 }); 107 this.onTransitionChange(); 108 this.onViewDataChanged(); 109 Log.info(TAG, `photoItem aboutToAppear ${this.item.uri} end ${this.item.orientation}}`); 110 } 111 112 private updateMatrix(matrix) { 113 Log.info(TAG, `updateMatrix start ${matrix}`); 114 this.matrix = matrix; 115 } 116 117 private updateThumbnail() { 118 getThumbnail(this.item, this.updateTransition == this.item.index).then((thumbnail: string) => { 119 this.thumbnail = thumbnail 120 }) 121 } 122 123 aboutToDisappear(): void { 124 Log.info(TAG, `aboutToDisappear ${this.item.uri}/${this.thumbnail}`); 125 // clean up event registration 126 this.broadCast.off(Constants.TOUCH_EVENT + this.item.uri + this.timeStamp, null); 127 this.broadCast.off(Constants.DIRECTION_CHANGE + this.item.uri + this.timeStamp, null); 128 this.broadCast.off(Constants.ANIMATION_EVENT + this.item.uri + this.timeStamp, null); 129 this.isPullingDown = false; 130 Log.info(TAG, `aboutToDisappear ${this.item.uri}/${this.thumbnail} end`); 131 } 132 133 onViewDataChanged(): void { 134 Log.info(TAG, `onViewDataChanged start`); 135 this.ratio = this.calcRatio(this.item); 136 if ((this.preItem.rotate == this.item.orientation && this.preItem.height == this.item.imgHeight && 137 this.preItem.width == this.item.imgWidth) && !this.firstLoad) { 138 this.preItem.rotate = this.item.orientation; 139 this.preItem.width = this.item.imgWidth; 140 this.preItem.height = this.item.imgHeight; 141 this.eventPipeline && this.eventPipeline.onDataChanged(this.item) 142 return; 143 } 144 this.matrix = Matrix4.identity().copy(); 145 this.eventPipeline && this.eventPipeline.setDefaultScale(1); 146 this.eventPipeline && this.eventPipeline.onDataChanged(this.item) 147 this.firstLoad = false; 148 this.preItem.width = this.item.imgWidth; 149 this.preItem.height = this.item.imgHeight; 150 Log.debug(TAG, `onViewDataChanged thumbnail:${JSON.stringify(this.thumbnail)}`); 151 } 152 153 calcRatio(info: any): number { 154 if (info == null || info == undefined) { 155 return 1; 156 } 157 if (info.width == 0 || info.height == 0) { 158 return 1; 159 } 160 return info.width / info.height; 161 } 162 163 build() { 164 Stack() { 165 Column() { 166 LoadingPanel() 167 } 168 .width('100%') 169 .height('100%') 170 171 Row() { 172 Image(this.thumbnail) 173 .rotate({ x: 0, y: 0, z: 1, angle: 0 }) 174 .syncLoad(true) 175 .transform(this.matrix) 176 .autoResize(false) 177 .fitOriginalSize(false) 178 .aspectRatio(this.ratio) 179 .objectFit(ImageFit.Cover) 180 .width("100%") 181 .height("100%") 182 .constraintSize({maxWidth:"100%", maxHeight:"100%"}) 183 .sharedTransition(this.item.index == this.updateTransition ? this.pageName + this.item.getHashCode() : Constants.DEFAULT_TRANSITION_ID, { 184 duration: Constants.SHARE_TRANSITION_DURATION, 185 zIndex: 0, 186 }) 187 .onComplete(() => { 188 startTrace(`PhotoItemComplete_${this.item}`); 189 Log.info(TAG, `onComplete finish, index: ${this.item.index}, item: ${JSON.stringify(this.item)}, uri: ${this.thumbnail}.`); 190 this.showError = false; 191 this.isLoading = false; 192 if (this.updateTransition == this.item.index) { 193 this.broadCast.emit(Constants.PHOTO_SHOW_STATE, [true]); 194 } 195 finishTrace(`PhotoItemComplete_${this.item.index}`); 196 }) 197 .onError(() => { 198 Log.error(TAG, `image show error ${this.thumbnail} ${this.item.width} ${this.item.height}`); 199 if (this.thumbnail.length == 0 || this.item.width == 0 || this.item.height == 0) { 200 this.updateThumbnail() 201 return 202 } 203 this.showError = true; 204 this.isLoading = true; 205 if (this.updateTransition == this.item.index) { 206 this.broadCast.emit(Constants.PHOTO_SHOW_STATE, [false]); 207 } 208 }) 209 } 210 .width('100%') 211 .height('100%') 212 .justifyContent(FlexAlign.Center) 213 .alignItems(VerticalAlign.Center) 214 .onClick(() => { 215 this.broadCast.emit(Constants.TOGGLE_BAR, [null]); 216 }) 217 .parallelGesture( 218 GestureGroup(GestureMode.Parallel, 219 PinchGesture({ 220 fingers: 2, 221 distance: 1 222 }) 223 .onActionStart((event: GestureEvent) => { 224 Log.debug(TAG, 'PinchGesture onActionStart'); 225 startTrace("PinchGesture onActionStart") 226 if (this.isPullingDown) { 227 Log.debug(TAG, 'Not allow to pinch when pullingDown'); 228 finishTrace("PinchGesture onActionStart") 229 return; 230 } 231 Log.debug(TAG, `PinchGesture onActionStart scale:\ 232 ${event.scale}, cx: ${event.pinchCenterX}, cy: ${event.pinchCenterY}`); 233 if (this.item.mediaType == MediaLib.MediaType.IMAGE) { 234 this.eventPipeline.onScaleStart(event.scale, event.pinchCenterX, event.pinchCenterY); 235 } 236 finishTrace("PinchGesture onActionStart") 237 }) 238 .onActionUpdate((event) => { 239 Log.debug(TAG, `PinchGesture onActionUpdate scale: ${event.scale}`); 240 startTrace("PinchGesture onActionUpdate") 241 if (this.isPullingDown) { 242 Log.debug(TAG, 'Not allow to pinch when pullingDown'); 243 finishTrace("PinchGesture onActionUpdate") 244 return; 245 } 246 if (this.item.mediaType == MediaLib.MediaType.IMAGE) { 247 this.eventPipeline.onScale(event.scale); 248 } 249 finishTrace("PinchGesture onActionUpdate") 250 }) 251 .onActionEnd(() => { 252 Log.debug(TAG, 'PinchGesture onActionEnd'); 253 startTrace("PinchGesture onActionEnd") 254 if (this.isPullingDown) { 255 Log.debug(TAG, 'Not allow to pinch when pullingDown'); 256 finishTrace("PinchGesture onActionEnd") 257 return; 258 } 259 if (this.item.mediaType == MediaLib.MediaType.IMAGE) { 260 this.eventPipeline.onScaleEnd(); 261 if (this.animationOption != null) { 262 animateTo({ 263 duration: this.animationOption.duration, 264 curve: this.animationOption.curve, 265 onFinish: () => { 266 this.eventPipeline.onAnimationEnd(this.animationEndMatrix); 267 this.animationOption = null; 268 this.animationEndMatrix = null; 269 } 270 }, () => { 271 this.matrix = this.animationEndMatrix; 272 }) 273 } 274 } 275 finishTrace("PinchGesture onActionEnd") 276 }), 277 PanGesture({ 278 direction: this.photoItemDirection 279 }) 280 .onActionStart((event: GestureEvent) => { 281 startTrace("PanGesture onActionStart") 282 Log.debug(TAG, `PanGesture start offsetX:\ 283 ${vp2px(event.offsetX)}, offsetY: ${vp2px(event.offsetY)}`); 284 this.eventPipeline.onMoveStart(vp2px(event.offsetX), vp2px(event.offsetY)); 285 finishTrace("PanGesture onActionStart") 286 }) 287 .onActionUpdate((event: GestureEvent) => { 288 startTrace("PanGesture onActionUpdate") 289 Log.debug(TAG, `PanGesture update offsetX:\ 290 ${vp2px(event.offsetX)}, offsetY: ${vp2px(event.offsetY)}`); 291 this.eventPipeline.onMove(vp2px(event.offsetX), vp2px(event.offsetY)); 292 finishTrace("PanGesture onActionUpdate") 293 }) 294 .onActionEnd((event: GestureEvent) => { 295 startTrace("PanGesture onActionEnd") 296 Log.debug(TAG, `PanGesture end offsetX:\ 297 ${vp2px(event.offsetX)}, offsetY: ${vp2px(event.offsetY)}`); 298 this.eventPipeline.onMoveEnd(vp2px(event.offsetX), vp2px(event.offsetY)); 299 if (this.animationOption != null) { 300 animateTo({ 301 duration: this.animationOption.duration, 302 curve: this.animationOption.curve, 303 onFinish: () => { 304 this.eventPipeline.onAnimationEnd(this.animationEndMatrix); 305 this.animationOption = null; 306 this.animationEndMatrix = null; 307 } 308 }, () => { 309 this.matrix = this.animationEndMatrix; 310 }) 311 } 312 finishTrace("PanGesture onActionEnd") 313 }), 314 TapGesture({ 315 count: 2 316 }) 317 .onAction((event: GestureEvent) => { 318 if (this.isPullingDown) { 319 Log.debug(TAG, 'Not allow to double tap when pullingDown'); 320 return; 321 } 322 Log.debug(TAG, `onDoubleTap event: ${JSON.stringify(event)}`); 323 if (this.item.mediaType == MediaLib.MediaType.VIDEO) { 324 return; 325 } 326 this.eventPipeline.onDoubleTap(event.fingerList[0].localX, event.fingerList[0].localY); 327 if (this.animationOption != null) { 328 Log.debug(TAG, 'TapGesture animateTo start'); 329 animateTo({ 330 duration: this.animationOption.duration, 331 curve: this.animationOption.curve, 332 onFinish: () => { 333 this.eventPipeline.onAnimationEnd(this.animationEndMatrix); 334 this.animationOption = null; 335 this.animationEndMatrix = null; 336 } 337 }, () => { 338 this.matrix = this.animationEndMatrix; 339 }) 340 } 341 }) 342 ) 343 ) 344 .clip(true) 345 .onTouch((event) => { 346 this.eventPipeline.onTouch(event); 347 }) 348 // TODO Remind users when pictures of other devices cannot be show 349 if ((this.showError || this.item.mediaType == MediaLib.MediaType.VIDEO) && 350 this.pageFrom == RouterConstants.ENTRY_FROM.DISTRIBUTED) { 351 Row() { 352 Text((this.item.mediaType == MediaLib.MediaType.VIDEO) ? 353 $r('app.string.no_distributed_photo_show_video') : 354 $r('app.string.no_distributed_photo_show_image')) 355 .fontSize($r('sys.float.ohos_id_text_size_body2')) 356 .fontFamily($r('app.string.id_text_font_family_regular')) 357 .fontColor($r('sys.color.ohos_id_color_text_tertiary')) 358 } 359 .margin({ 360 top: (this.item.mediaType == MediaLib.MediaType.VIDEO) ? $r('app.float.input_text_notify_margin') : 0 361 }) 362 } 363 if (this.isVideoPlayBtnShow()) { 364 Row() { 365 VideoIcon() 366 } 367 .width($r('app.float.icon_video_size')) 368 .height($r('app.float.icon_video_size')) 369 .onClick(() => { 370 if (this.item != undefined) { 371 router.push({ 372 uri: 'feature/browser/view/VideoBrowser', 373 params: { 374 uri: this.item.uri, 375 dateAdded: this.item.dateAdded, 376 previewUri: this.thumbnail 377 } 378 }) 379 } 380 }) 381 } 382 383 }.width('100%') 384 .height('100%') 385 } 386 387 private isVideoPlayBtnShow(): boolean { 388 Log.debug(TAG, `isVideoPlayBtnShow: ${this.item.mediaType}`); 389 return ((this.item) && (this.item.mediaType == MediaLib.MediaType.VIDEO)); 390 } 391} 392