• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2022-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 { Log } from '../utils/Log';
18import { Constants } from '../model/common/Constants';
19import { Constants as PhotoConstants } from '../model/browser/photo/Constants';
20import { MediaItem } from '../model/browser/photo/MediaItem';
21import { DateUtil } from '../utils/DateUtil';
22import { BroadCast } from '../utils/BroadCast';
23import { BroadCastConstants } from '../model/common/BroadCastConstants';
24import { Action } from './browserOperation/Action';
25import { ImageUtil } from '../utils/ImageUtil';
26import { ColumnSize, ScreenManager } from '../model/common/ScreenManager';
27import { TraceControllerUtils } from '../utils/TraceControllerUtils';
28import { UserFileManagerAccess } from '../access/UserFileManagerAccess';
29import { MultimodalInputManager } from '../model/common/MultimodalInputManager';
30import { BigDataConstants, ReportToBigDataUtil } from '../utils/ReportToBigDataUtil';
31import { AlbumDefine } from '../model/browser/AlbumDefine';
32import { MediaDataSource } from '../model/browser/photo/MediaDataSource';
33import { BroadCastManager } from '../model/common/BroadCastManager';
34
35const TAG: string = 'common_ImageGridItemComponent';
36
37@Extend(Image) function focusSetting(uri: string, handleEvent: Function) {
38  .key('ImageGridFocus_' + uri)
39  .focusable(true)
40  .onKeyEvent((event: KeyEvent) => {
41    handleEvent(event);
42  })
43}
44
45// General grid picture control
46@Component
47export struct ImageGridItemComponent {
48  item: MediaItem;
49  @StorageLink('isHorizontal') isHorizontal: boolean = ScreenManager.getInstance().isHorizontal();
50  @Consume @Watch('onModeChange') isSelectedMode: boolean;
51  @State isSelected: boolean = false;
52  isRecycle: boolean = false;
53  @Consume broadCast: BroadCast;
54  @Consume @Watch('onShow') isShow: boolean;
55  @Link selectedCount: number;
56  @State autoResize: boolean = true;
57  loaded = false;
58  mPosition: number;
59  pageName = '';
60  @State isLoadImageError: boolean = false;
61  @State pressAnimScale: number = 1.0;
62  @State recycleDays: number = 0;
63  @Consume rightClickMenuList: Array<Action>;
64  onMenuClicked: Function;
65  onMenuClickedForSingleItem: Function;
66  @State geometryTransitionString: string = 'default_id';
67  @State isTap: boolean = false;
68  @StorageLink('placeholderIndex') @Watch('verifyTapStatus') placeholderIndex: number = -1;
69  @StorageLink('geometryTransitionBrowserId') @Watch('verifyTapStatus') geometryTransitionBrowserId: string = '';
70  private imageThumbnail: string = undefined;
71  private transitionId: string;
72  private isEnteringPhoto = false;
73  private isThird = false;
74  private isThirdMultiPick: boolean = false;
75  private OPTS = {
76    'sampleSize': 1,
77    'rotateDegrees': 0,
78    'editable': false,
79    'desiredSize': {
80      'width': 0,
81      'height': 0
82    },
83    'desiredRegion': {
84      'size': {
85        'width': 0,
86        'height': 0
87      },
88      'x': 0,
89      'y': 0
90    },
91    'desiredPixelFormat': 3,
92  };
93  private albumUri: string = '';
94  private dataSource: MediaDataSource;
95  private geometryTapIndex: number;
96  private isTapStatusChange: boolean = false;
97
98  verifyTapStatus() {
99    if (this.placeholderIndex === Constants.INVALID) {
100      this.isTap = false;
101      return;
102    }
103    this.updateGeometryTapInfo();
104    let pageFromGlobal = this.geometryTransitionBrowserId.split(':')[0];
105    let pageFrom = this.geometryTransitionString.split(':')[0];
106    let oldTapStatus = this.isTap;
107    let newTapStatus = (pageFromGlobal === pageFrom) && (this.placeholderIndex === this.geometryTapIndex);
108    this.isTapStatusChange = oldTapStatus !== newTapStatus;
109    this.isTap = newTapStatus;
110    if (this.isTap) {
111      this.geometryTransitionString = this.geometryTransitionBrowserId;
112      Log.debug(TAG, 'update placeholderIndex = ' + this.placeholderIndex +
113        'geometryTapIndex = ' + this.geometryTapIndex + ', isTap = ' + this.isTap +
114        ', geometryTransitionString = ' + this.geometryTransitionString);
115    }
116  }
117
118  aboutToAppear(): void {
119    this.imageThumbnail = this.item?.thumbnail;
120    this.albumUri = AppStorage.Get<string>(Constants.KEY_OF_ALBUM_URI);
121    if (this.isSelected) {
122      this.transitionId = `${this.item.hashCode}_${this.albumUri}_${this.isSelected}`;
123    } else {
124      this.transitionId = `${this.item.hashCode}_${this.albumUri}`;
125    }
126    if (this.isRecycle) {
127      this.calculateRecycleDays();
128    }
129    Log.info(TAG, `transitionId: ${this.transitionId}`);
130    this.isTap = this.geometryTransitionString === this.geometryTransitionBrowserId;
131  }
132
133  aboutToDisappear(): void {
134    Log.debug(TAG, `aboutToDisappear: ${this.item.uri}`);
135    this.resetPressAnim();
136  }
137
138  onModeChange(newMode: boolean): void {
139    Log.debug(TAG, `newMode ${newMode}`);
140    if (!this.isSelectedMode) {
141      this.isSelected = false;
142    }
143  }
144
145  onAllSelect(newMode: boolean): boolean {
146    Log.debug(TAG, `onAllSelect ${newMode}`);
147    return newMode;
148  }
149
150  async routePage(isError: boolean) {
151    Log.info(TAG, `routePage ${isError}`);
152    try {
153      TraceControllerUtils.startTrace('enterPhotoBrowser');
154      if (this.isThird) {
155        this.broadCast.emit(BroadCastConstants.JUMP_THIRD_PHOTO_BROWSER, [this.pageName, this.item]);
156      } else {
157        this.broadCast.emit(BroadCastConstants.JUMP_PHOTO_BROWSER, [this.pageName, this.item]);
158      }
159    } catch (err) {
160      Log.error(TAG, `fail callback, code: ${err.code}, msg: ${err.msg}`);
161    }
162  }
163
164  async routeToPreviewPage() {
165    try {
166      Log.info(TAG, 'routeToPreviewPage');
167      this.updateGeometryTapInfo();
168      this.broadCast.emit(BroadCastConstants.JUMP_THIRD_PHOTO_BROWSER,
169        [this.pageName, this.item, this.geometryTapIndex, this.geometryTransitionString]);
170    } catch (err) {
171      Log.error(TAG, `fail callback, code: ${err.code}, msg: ${err.msg}`);
172    }
173  }
174
175  selectStateChange() {
176    Log.info(TAG, 'change selected.');
177    let newState = !this.isSelected;
178    AppStorage.SetOrCreate('focusUpdate', true);
179    if (this.item.uri) {
180      this.mPosition = this.getPosition();
181      this.broadCast.emit(BroadCastConstants.SELECT, [this.mPosition, this.item.uri, newState, function(isSelected){
182        Log.info(TAG, `enter callback, select status ${this.mPosition} ${this.item.uri} ${newState} ${this.isSelected}`);
183        this.isSelected = isSelected == undefined ? newState : isSelected;
184      }.bind(this)]);
185    }
186  }
187
188  @Builder RightClickMenuBuilder() {
189    Column() {
190      ForEach(this.rightClickMenuList, (menu: Action) => {
191        Text(this.changeTextResToPlural(menu))
192          .key('RightClick_' + this.mPosition + menu.componentKey)
193          .width('100%')
194          .height($r('app.float.menu_height'))
195          .fontColor(menu.fillColor)
196          .fontSize($r('sys.float.ohos_id_text_size_body1'))
197          .fontWeight(FontWeight.Regular)
198          .maxLines(2)
199          .textOverflow({ overflow: TextOverflow.Ellipsis })
200          .onClick(() => {
201            Log.info(TAG, 'on right click menu, action: ' + menu.actionID);
202            if (menu == Action.MULTISELECT) {
203              this.selectStateChange();
204            } else {
205              // 1.当鼠标对着被选中的项按右键时,菜单中的功能,针对所有被选中的项做处理
206              // 2.当鼠标对着未被选中的项按右键时,菜单中的功能,仅针对当前项处理,其他被选中的项不做任何处理
207              if (this.isSelectedMode && this.isSelected) {
208                this.onMenuClicked && this.onMenuClicked(menu);
209              } else {
210                this.onMenuClickedForSingleItem && this.onMenuClickedForSingleItem(menu, this.item);
211              }
212            }
213          })
214      }, menu => menu.actionID.toString())
215    }
216    .width(ScreenManager.getInstance().getColumnsWidth(ColumnSize.COLUMN_TWO))
217    .borderRadius($r('sys.float.ohos_id_corner_radius_card'))
218    .padding({
219      top: $r('app.float.menu_padding_vertical'),
220      bottom: $r('app.float.menu_padding_vertical'),
221      left: $r('app.float.menu_padding_horizontal'),
222      right: $r('app.float.menu_padding_horizontal')
223    })
224    .backgroundColor(Color.White)
225    .margin({
226      right: $r('sys.float.ohos_id_max_padding_end'),
227      bottom: $r('app.float.menu_margin_bottom')
228    })
229  }
230
231  build() {
232    Column() {
233      if (this.isTap) {
234        Column() {
235        }
236        .aspectRatio(1)
237        .rotate({ x: 0, y: 0, z: 1, angle: 0 })
238        .backgroundColor($r('app.color.default_background_color'))
239        .width("100%")
240        .height("100%")
241        .zIndex(-1)
242      } else {
243        this.buildNormal()
244      }
245    }
246  }
247
248  @Builder buildImage() {
249    Image(this.imageThumbnail)
250      .width('100%')
251      .height('100%')
252      .rotate({ x: 0, y: 0, z: 1, angle: 0 })
253      .objectFit(ImageFit.Cover)
254      .autoResize(false)
255      .focusSetting(this.item.uri, this.handleKeyEvent.bind(this))
256      .onError(() => {
257        this.isLoadImageError = true;
258        AppStorage.SetOrCreate('focusUpdate', true);
259        Log.error(TAG, 'item Image error');
260      })
261      .onComplete(() => {
262        Log.debug(TAG, `Draw the image! ${this.imageThumbnail}`);
263      })
264      .onAppear(() => {
265        this.requestFocus('ImageGridFocus_');
266      })
267      .geometryTransition(this.geometryTransitionString)
268        // @ts-ignore
269      .transition(TransitionEffect.asymmetric(
270        // @ts-ignore
271        TransitionEffect.scale({ x: AppStorage.Get('geometryScale'), y: AppStorage.Get('geometryScale') }),
272        // @ts-ignore
273        TransitionEffect.opacity(0.99)))
274
275    if (this.geometryTransitionBrowserId === '' || !this.isTapStatusChange) {
276      this.buildIcon();
277    }
278  }
279
280  @Builder
281  buildIcon() {
282    if (this.item.mediaType == UserFileManagerAccess.MEDIA_TYPE_VIDEO || this.isRecycle) {
283      Row() {
284        // 缩略图左下角视频时长
285        if (this.item.mediaType == UserFileManagerAccess.MEDIA_TYPE_VIDEO) {
286          Text(DateUtil.getFormattedDuration(this.item.duration))
287            .fontSize($r('sys.float.ohos_id_text_size_caption'))
288            .fontFamily($r('app.string.id_text_font_family_regular'))
289            .fontColor($r('app.color.text_color_above_picture'))
290            .lineHeight(12)
291            .margin({
292              left: $r('app.float.grid_item_text_margin_lr'),
293              bottom: $r('app.float.grid_item_text_margin_bottom')
294            })
295            .key('VideoDurationOfIndex' + this.mPosition)
296        }
297        // 缩略图右下角距离删除天数
298        if (this.isRecycle && !this.isSelectedMode) {
299          Blank()
300
301          Text($r('app.plural.recycle_days', this.recycleDays, this.recycleDays))
302            .fontSize($r('sys.float.ohos_id_text_size_caption'))
303            .fontFamily($r('app.string.id_text_font_family_regular'))
304            .fontColor(this.recycleDays <= Constants.RECYCLE_DAYS_WARN ? $r('sys.color.ohos_id_color_warning') : $r('app.color.text_color_above_picture'))
305            .lineHeight(12)
306            .margin({
307              right: $r('app.float.grid_item_text_margin_lr'),
308              bottom: $r('app.float.grid_item_text_margin_bottom')
309            })
310        }
311      }
312      .position({ x: '0%', y: '50%' })
313      .height('50%')
314      .width('100%')
315      .alignItems(VerticalAlign.Bottom)
316      .linearGradient({ angle: 0, colors:
317      [[$r('app.color.album_cover_gradient_start_color'), 0], [$r('app.color.transparent'), 1.0]] })
318    }
319
320    if (this.item.isFavor) {
321      Image($r('app.media.ic_favorite_overlay'))
322        .height($r('app.float.overlay_icon_size'))
323        .width($r('app.float.overlay_icon_size'))
324        .fillColor($r('sys.color.ohos_id_color_primary_dark'))
325        .objectFit(ImageFit.Contain)
326        .position({ x: '100%', y: '0%' })
327        .markAnchor({
328          x: $r('app.float.grid_item_favor_markAnchor_x'),
329          y: $r('app.float.grid_item_favor_markAnchor_y')
330        })
331        .key('Favor_' + this.mPosition)
332    }
333
334    // 当三方拉起 picker 时, 只有多选模式下才显示蒙层
335    if (this.isSelected && this.isSelectedMode && (!this.isThird || this.isThirdMultiPick)) {
336      Column()
337        .key('MaskLayer_' + this.mPosition)
338        .height('100%')
339        .width('100%')
340        .backgroundColor($r('app.color.item_selection_bg_color'))
341    }
342
343    // 缩略图上方功能图标
344    if (this.isSelectedMode) {
345      Image($r('app.media.ic_photo_preview'))
346        .key('Previewer_' + this.mPosition)
347        .height($r('app.float.icon_size'))
348        .width($r('app.float.icon_size'))
349        .position({ x: '0%', y: '0%' })
350        .markAnchor({
351          x: $r('app.float.grid_item_preview_padding'),
352          y: $r('app.float.grid_item_preview_padding')
353        })
354        .onClick(() => {
355          Log.info(TAG, 'onClick loadThumbnailUri' + this.imageThumbnail);
356          this.routeToPreviewPage();
357          Log.info(TAG, 'expand.');
358        })
359    }
360    if (this.isSelectedMode && (!this.isThird || this.isThirdMultiPick)) {
361      Checkbox()
362        .key('Selector_' + this.mPosition)
363        .select(this.isSelected)
364        .margin(0)
365        .position({ x: '100%', y: '100%' })
366        .markAnchor({
367          x: $r('app.float.grid_item_checkbox_markAnchor'),
368          y: $r('app.float.grid_item_checkbox_markAnchor')
369        })
370        .focusable(false)
371        .hitTestBehavior(HitTestMode.None)
372    }
373  }
374
375  @Builder
376  buildNormal() {
377    Stack({ alignContent: Alignment.Start }) {
378      // 缩略图
379      if (this.isLoadImageError) {
380        Image((this.item.mediaType == UserFileManagerAccess.MEDIA_TYPE_VIDEO)
381          ? $r('app.media.alt_video_placeholder') : $r('app.media.alt_placeholder'))
382          .aspectRatio(1)
383          .rotate({ x: 0, y: 0, z: 1, angle: 0 })
384          .objectFit(ImageFit.Cover)
385          .autoResize(false)
386          .focusSetting(this.item.uri, this.handleKeyEvent.bind(this))
387          .onAppear(() => {
388            Log.debug(TAG, `appear the default image!`);
389          })
390
391        if (this.geometryTransitionBrowserId === '' || !this.isTapStatusChange) {
392          this.buildIcon();
393        }
394      } else {
395        if (this.albumUri === UserFileManagerAccess.getInstance()
396          .getSystemAlbumUri(UserFileManagerAccess.TRASH_ALBUM_SUB_TYPE)
397        || this.pageName === Constants.PHOTO_TRANSITION_TIMELINE) {
398          this.buildImage();
399        } else {
400          Stack() {
401            this.buildImage();
402          }
403          .borderRadius(0)
404          .clip(true)
405          .geometryTransition(this.transitionId)
406        }
407      }
408    }
409    .key('Gesture_' + this.mPosition)
410    .height('100%')
411    .width('100%')
412    .bindContextMenu(this.RightClickMenuBuilder, ResponseType.RightClick) // 右键点击菜单,后续整改至新组件
413    .scale({
414      x: this.pressAnimScale,
415      y: this.pressAnimScale
416    })
417    .onTouch(event => {
418      Log.debug(TAG, `onTouch trigger: isSelectedMode: ${this.isSelectedMode},
419                    isEnteringPhoto: ${this.isEnteringPhoto}, ${JSON.stringify(event)}`);
420      if (this.isSelectedMode) {
421        return;
422      }
423
424      // Press animation
425      if (event.type == TouchType.Down) {
426        animateTo({
427          duration: Constants.PRESS_ANIM_DURATION,
428          curve: Curve.Ease
429        }, () => {
430          this.pressAnimScale = Constants.PRESS_ANIM_SCALE;
431        })
432      }
433
434      if ((event.type == TouchType.Up || event.type == TouchType.Cancel) && this.pressAnimScale != 1) {
435        animateTo({
436          duration: Constants.PRESS_ANIM_DURATION,
437          curve: Curve.Ease
438        }, () => {
439          this.pressAnimScale = 1;
440        })
441      }
442    })
443    .gesture(GestureGroup(GestureMode.Exclusive,
444      TapGesture().onAction((event: GestureEvent) => {
445        let ret: Boolean = focusControl.requestFocus('ImageGridFocus_' + this.item.uri);
446        if (ret !== true) {
447          Log.error(TAG, `requestFocus${'ImageGridFocus_' + this.item.uri}, ret:${ret}`);
448        }
449        let msg = {
450          'From': BigDataConstants.BY_CLICK,
451        }
452        ReportToBigDataUtil.report(BigDataConstants.ENTER_PHOTO_BROWSER_WAY, msg);
453        this.openPhotoBrowser();
454      }),
455      LongPressGesture().onAction((event: GestureEvent) => {
456        Log.info(TAG, `LongPressGesture ${event}`);
457        this.selectStateChange();
458        this.pressAnimScale = 1;
459      })
460    ))
461  }
462
463  private resetPressAnim(): void {
464    this.pressAnimScale = 1;
465    this.isEnteringPhoto = false;
466  }
467
468  private onShow(): void {
469    this.resetPressAnim();
470  }
471
472  private generateSampleSize(imageWidth: number, imageHeight: number): number {
473    let width = ScreenManager.getInstance().getWinWidth();
474    let height = ScreenManager.getInstance().getWinHeight();
475    width = width == 0 ? ScreenManager.DEFAULT_WIDTH : width;
476    height = height == 0 ? ScreenManager.DEFAULT_HEIGHT : height;
477    let maxNumOfPixels = width * height;
478    let minSide = Math.min(width, height);
479    return ImageUtil.computeSampleSize(imageWidth, imageHeight, minSide, maxNumOfPixels);
480  }
481
482  private changeTextResToPlural(action: Action): Resource {
483    let textStr: Resource = action.textRes;
484    if (Action.RECOVER.equals(action)) {
485      textStr = this.isSelected
486        ? $r('app.plural.action_recover_count', this.selectedCount, this.selectedCount)
487        : $r('app.string.action_recover');
488    } else if (Action.DELETE.equals(action)) {
489      textStr = this.isSelected
490        ? $r('app.plural.action_delete_count', this.selectedCount, this.selectedCount)
491        : $r('app.string.action_delete');
492    } else if (Action.MOVE.equals(action)) {
493      textStr = this.isSelected
494        ? $r('app.plural.move_to_album_count', this.selectedCount, this.selectedCount)
495        : $r('app.string.move_to_album');
496    } else if (Action.ADD.equals(action)) {
497      textStr = this.isSelected
498        ? $r('app.plural.add_to_album_count', this.selectedCount, this.selectedCount)
499        : $r('app.string.add_to_album');
500    }
501    return textStr;
502  }
503
504  // 获取最近删除中,待回收照片倒计天数
505  private calculateRecycleDays(): void {
506    let currentTimeSeconds: number = new Date().getTime() / 1000;
507    let trashedDay = DateUtil.convertSecondsToDays(currentTimeSeconds - this.item.dateTrashed);
508    Log.debug(TAG, `currentSec=${currentTimeSeconds}, trashedSec=${this.item.dateTrashed}, trashedDay=${trashedDay}`);
509    if (trashedDay > Constants.RECYCLE_DAYS_MAX) {
510      this.recycleDays = 0;
511    } else if (trashedDay <= 0) {
512      this.recycleDays = Constants.RECYCLE_DAYS_MAX - 1;
513    } else {
514      this.recycleDays = parseInt((Constants.RECYCLE_DAYS_MAX - trashedDay) + '');
515    }
516  }
517
518  private requestFocus(keyName: string): void {
519    if (AppStorage.Get('deviceType') == Constants.DEFAULT_DEVICE_TYPE) {
520      return;
521    }
522    let positionUri = AppStorage.Get('focusPosition');
523    let isUpdate = AppStorage.Get('focusUpdate');
524    if (isUpdate && positionUri === this.item.uri) {
525      let ret: Boolean = focusControl.requestFocus(keyName + this.item.uri);
526      if (ret !== true) {
527        Log.error(TAG, `requestFocus${keyName + this.item.uri}, ret:${ret}`);
528      }
529      AppStorage.SetOrCreate('focusUpdate', false);
530    }
531  }
532
533  private openPhotoBrowser(): void {
534    if (this.isSelectedMode) {
535      this.selectStateChange();
536    } else {
537      Log.info(TAG, 'item onClick loadBmp');
538      Log.info(TAG, 'onClick loadThumbnailUri' + this.imageThumbnail);
539      this.updateGeometryTapInfo();
540      if (this.isThird) {
541        this.broadCast.emit(BroadCastConstants.JUMP_THIRD_PHOTO_BROWSER,
542          [this.pageName, this.item, this.geometryTapIndex, this.geometryTransitionString]);
543      } else {
544        this.broadCast.emit(BroadCastConstants.JUMP_PHOTO_BROWSER,
545          [this.pageName, this.item, this.geometryTapIndex, this.geometryTransitionString]);
546      }
547      this.isEnteringPhoto = true;
548    }
549  }
550
551  private handleKeyEvent(event: KeyEvent): void {
552    if (KeyType.Up == event.type) {
553      switch (event.keyCode) {
554        case MultimodalInputManager.KEY_CODE_KEYBOARD_ENTER:
555          let msg = {
556            'From': BigDataConstants.BY_KEYBOARD,
557          }
558          ReportToBigDataUtil.report(BigDataConstants.ENTER_PHOTO_BROWSER_WAY, msg);
559          this.openPhotoBrowser();
560          break;
561        case MultimodalInputManager.KEY_CODE_KEYBOARD_ESC:
562          this.onMenuClicked && this.onMenuClicked(Action.BACK);
563          break;
564        default:
565          Log.info(TAG, `on key event Up, default`);
566          break;
567      }
568    }
569  }
570
571  private updateGeometryTapInfo(): void {
572    this.geometryTapIndex = this.getPosition();
573  }
574
575  private getPosition(): number {
576    return this.dataSource.getDataIndex(this.item) + this.dataSource.getGroupCountBeforeItem(this.item);
577  }
578}