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