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}