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