UNPKG

@nstudio/ui-collectionview

Version:

Customized NativeScript CollectionView for high performance lists. Supports vertical and horizontal modes, templating, and more.

1,130 lines 69.5 kB
import { ChangeType, ContentView, Length, Observable, Property, ProxyViewContainer, Trace, Utils, View, booleanConverter, paddingBottomProperty, paddingLeftProperty, paddingRightProperty, paddingTopProperty, profile } from '@nativescript/core'; import { reorderLongPressEnabledProperty, reorderingEnabledProperty, reverseLayoutProperty, scrollBarIndicatorVisibleProperty } from '.'; import { CLog, CLogTypes, CollectionViewBase, ListViewViewTypes, isBounceEnabledProperty, isScrollEnabledProperty, itemTemplatesProperty, orientationProperty } from './common'; export * from './common'; const infinity = Utils.layout.makeMeasureSpec(0, Utils.layout.UNSPECIFIED); export var ContentInsetAdjustmentBehavior; (function (ContentInsetAdjustmentBehavior) { ContentInsetAdjustmentBehavior[ContentInsetAdjustmentBehavior["Always"] = 3] = "Always"; ContentInsetAdjustmentBehavior[ContentInsetAdjustmentBehavior["Automatic"] = 0] = "Automatic"; ContentInsetAdjustmentBehavior[ContentInsetAdjustmentBehavior["Never"] = 2] = "Never"; ContentInsetAdjustmentBehavior[ContentInsetAdjustmentBehavior["ScrollableAxes"] = 1] = "ScrollableAxes"; })(ContentInsetAdjustmentBehavior || (ContentInsetAdjustmentBehavior = {})); function parseContentInsetAdjustmentBehavior(value) { if (typeof value === 'string') { switch (value) { case 'always': return ContentInsetAdjustmentBehavior.Always; case 'never': return ContentInsetAdjustmentBehavior.Never; case 'scrollableAxes': return ContentInsetAdjustmentBehavior.ScrollableAxes; default: case 'automatic': return ContentInsetAdjustmentBehavior.Automatic; } } else { return value; } } export const contentInsetAdjustmentBehaviorProperty = new Property({ name: 'contentInsetAdjustmentBehavior', valueConverter: parseContentInsetAdjustmentBehavior, defaultValue: ContentInsetAdjustmentBehavior.Automatic, }); export const estimatedItemSizeProperty = new Property({ name: 'estimatedItemSize', defaultValue: true, valueConverter: booleanConverter, }); export const autoSizeProperty = new Property({ name: 'autoSize', defaultValue: false, valueConverter: booleanConverter, }); export var SnapPosition; (function (SnapPosition) { SnapPosition[SnapPosition["START"] = -1] = "START"; SnapPosition[SnapPosition["END"] = 1] = "END"; })(SnapPosition || (SnapPosition = {})); export class CollectionView extends CollectionViewBase { // dragDelegate: UICollectionViewDragDelegateImpl; // dropDelegate: UICollectionViewDropDelegateImpl; constructor() { super(); this._preparingCell = false; this.autoSize = false; this.reorderStartingRow = -1; this.reorderEndingRow = -1; this.manualDragging = false; this.scrollEnabledBeforeDragging = true; this.estimatedItemSize = true; this.needsScrollStartEvent = false; this.isScrolling = false; this._map = new Map(); // this._sizes = new Array<number[]>(); } createNativeView() { let layout; if (CollectionViewBase.layoutStyles[this.layoutStyle]) { layout = this._layout = CollectionViewBase.layoutStyles[this.layoutStyle].createLayout(this); } else { layout = this._layout = UICollectionViewFlowLayoutImpl.initWithOwner(this); // layout = this._layout = UICollectionViewFlowLayout.new(); } if (layout instanceof UICollectionViewFlowLayout) { layout.minimumLineSpacing = 0; layout.minimumInteritemSpacing = 0; } // const view = UICollectionViewImpl.initWithFrameCollectionViewLayout(CGRectMake(0, 0, 0, 0), layout) as UICollectionViewImpl; const view = UICollectionViewImpl.initWithOwner(this, layout); view.backgroundColor = UIColor.clearColor; this._itemTemplatesInternal.forEach((t) => { view.registerClassForCellWithReuseIdentifier(CollectionViewCell.class(), t.key.toLowerCase()); }); view.autoresizesSubviews = false; view.autoresizingMask = 0 /* UIViewAutoresizing.None */; this.lastContentOffset = view.contentOffset; return view; } onTemplateAdded(t) { super.onTemplateAdded(t); if (this.nativeViewProtected) { this.nativeViewProtected.registerClassForCellWithReuseIdentifier(CollectionViewCell.class(), t.key.toLowerCase()); } } initNativeView() { super.initNativeView(); const nativeView = this.nativeViewProtected; this._dataSource = CollectionViewDataSource.initWithOwner(this); nativeView.dataSource = this._dataSource; // this.dragDelegate = nativeView.dragDelegate = UICollectionViewDragDelegateImpl.initWithOwner(this); // this.dropDelegate = nativeView.dropDelegate = UICollectionViewDropDelegateImpl.initWithOwner(this); // delegate will be set in first onLayout because we need computed _effectiveColWidth and _effectiveRowHeight this._measureCellMap = new Map(); // waterfall requires the delegate to be set as soon as possible // but default delegates need _effectiveRowHeight and _effectiveColWidth // so we need to wait const layoutStyle = CollectionViewBase.layoutStyles[this.layoutStyle]; if (layoutStyle && layoutStyle.createDelegate) { this._delegate = layoutStyle.createDelegate(this); this.nativeViewProtected.delegate = this._delegate; } else if (this.autoSize) { this._delegate = UICollectionViewDelegateImpl.initWithOwner(this); this.nativeViewProtected.delegate = this._delegate; } this._setNativeClipToBounds(); } disposeNativeView() { if (Trace.isEnabled()) { CLog(CLogTypes.log, 'disposeNativeView'); } const nativeView = this.nativeViewProtected; nativeView.delegate = null; // nativeView.dragDelegate = null; // nativeView.dropDelegate = null; this._delegate = null; nativeView.dataSource = null; this._dataSource = null; this._layout = null; this.reorderLongPressHandler = null; this.reorderLongPressGesture = null; this.clearRealizedCells(); super.disposeNativeView(); } // _onSizeChanged() { // super._onSizeChanged(); // this.onSizeChanged(this.getMeasuredWidth(), this.getMeasuredHeight()); // } get _childrenCount() { return this._map.size; } onLoaded() { super.onLoaded(); // we need refreshVisibleItems // if some items were updated while unloaded they wont re layout // after this (because we are not a Layout view) this.refreshVisibleItems(); } eachChild(callback) { // used for css updates (like theme change) this._map.forEach((view) => { if (view.parent instanceof CollectionView) { callback(view); } else { // in some cases (like item is unloaded from another place (like angular) view.parent becomes undefined) if (view.parent) { callback(view.parent); } } }); // we need to call on measure cells too // otherwise they would not get notified of some css changes // like fontScale change this._measureCellMap?.forEach((view) => { callback(view.view); }); } async eachChildAsync(callback) { // used for css updates (like theme change) const children = [...this._map.values()]; for (let index = 0; index < children.length; index++) { const view = children[index]; if (view.parent instanceof CollectionView) { await callback(view); } else { // in some cases (like item is unloaded from another place (like angular) view.parent becomes undefined) if (view.parent) { await callback(view.parent); } } } } getViewForItemAtIndex(index) { let result; if (this.nativeViewProtected) { // when the collectionview is not loaded anymore, cellForItemAtIndexPath wont return // then we use our cached map to get the view const cell = this.nativeViewProtected.cellForItemAtIndexPath(NSIndexPath.indexPathForRowInSection(index, 0)); if (cell) { return cell?.view; } else { for (const [cell, view] of this._map) { if (cell.currentIndex === index) { return view; } } } } return result; } startDragging(index, pointer) { if (this.reorderEnabled && this.nativeViewProtected) { this.manualDragging = true; this.draggingStartDelta = null; if (pointer) { const view = this.getViewForItemAtIndex(index); if (view) { const size = view.nativeViewProtected.bounds.size; const point = pointer.ios.locationInView(view.nativeViewProtected); this.draggingStartDelta = [point.x - size.width / 2, point.y - size.height / 2]; } } this.nativeViewProtected.beginInteractiveMovementForItemAtIndexPath(NSIndexPath.indexPathForRowInSection(index, 0)); this.scrollEnabledBeforeDragging = this.isScrollEnabled; this.nativeViewProtected.scrollEnabled = false; } } onReorderingTouch(event) { if (!this.manualDragging) { return; } const collectionView = this.nativeViewProtected; const pointer = event.getActivePointers()[0]; switch (event.action) { case 'move': let x = pointer.getX(); let y = pointer.getY(); if (this.draggingStartDelta) { x -= this.draggingStartDelta[0]; y -= this.draggingStartDelta[1]; } collectionView.updateInteractiveMovementTargetPosition(CGPointMake(x, y)); break; case 'up': this.manualDragging = false; collectionView && collectionView.endInteractiveMovement(); this.nativeViewProtected.scrollEnabled = this.scrollEnabledBeforeDragging; this.handleReorderEnd(); break; case 'cancel': this.manualDragging = false; collectionView && collectionView.cancelInteractiveMovement(); this.nativeViewProtected.scrollEnabled = this.scrollEnabledBeforeDragging; this.handleReorderEnd(); break; } } handleReorderEnd() { // we call all events from here because the delegate // does not handle the case start dragging => cancel or // start dragging => end over the same item if (!this.reorderEndingRow) { this.reorderEndingRow = this.reorderStartingRow; } const item = this.getItemAtIndex(this.reorderStartingRow); this._callItemReorderedEvent(this.reorderStartingRow, this.reorderEndingRow, item); this.reorderEndingRow = -1; this.reorderEndingRow = -1; } onReorderLongPress(gesture) { const collectionView = this.nativeViewProtected; if (!collectionView) { return; } switch (gesture.state) { case 1 /* UIGestureRecognizerState.Began */: { let point = gesture.locationInView(collectionView); const selectedIndexPath = collectionView.indexPathForItemAtPoint(point); const view = this.getViewForItemAtIndex(selectedIndexPath.row); if (view) { const size = view.nativeViewProtected.bounds.size; point = gesture.locationInView(view.nativeViewProtected); this.draggingStartDelta = [point.x - size.width / 2, point.y - size.height / 2]; } collectionView.beginInteractiveMovementForItemAtIndexPath(selectedIndexPath); break; } case 2 /* UIGestureRecognizerState.Changed */: { const point = gesture.locationInView(collectionView); let x = point.x; let y = point.y; if (this.draggingStartDelta) { x -= this.draggingStartDelta[0]; y -= this.draggingStartDelta[1]; } collectionView.updateInteractiveMovementTargetPosition(CGPointMake(x, y)); break; } case 3 /* UIGestureRecognizerState.Ended */: collectionView.endInteractiveMovement(); this.handleReorderEnd(); break; default: collectionView.cancelInteractiveMovement(); this.handleReorderEnd(); break; } } // @ts-ignore [contentInsetAdjustmentBehaviorProperty.setNative](value) { this.nativeViewProtected.contentInsetAdjustmentBehavior = value; } // @ts-ignore [paddingTopProperty.setNative](value) { this._setPadding({ top: Utils.layout.toDeviceIndependentPixels(this.effectivePaddingTop) }); } // @ts-ignore [paddingRightProperty.setNative](value) { this._setPadding({ right: Utils.layout.toDeviceIndependentPixels(this.effectivePaddingRight) }); } // @ts-ignore [paddingBottomProperty.setNative](value) { this._setPadding({ bottom: Utils.layout.toDeviceIndependentPixels(this.effectivePaddingBottom) }); } // @ts-ignore [paddingLeftProperty.setNative](value) { this._setPadding({ left: Utils.layout.toDeviceIndependentPixels(this.effectivePaddingLeft) }); } // @ts-ignore [orientationProperty.setNative](value) { const layout = this._layout; if (layout instanceof UICollectionViewFlowLayout) { if (value === 'horizontal') { layout.scrollDirection = 1 /* UICollectionViewScrollDirection.Horizontal */; } else { layout.scrollDirection = 0 /* UICollectionViewScrollDirection.Vertical */; } } this.updateScrollBarVisibility(this.scrollBarIndicatorVisible); } // @ts-ignore [isScrollEnabledProperty.setNative](value) { this.nativeViewProtected.scrollEnabled = value; this.scrollEnabledBeforeDragging = value; } // @ts-ignore [isBounceEnabledProperty.setNative](value) { this.nativeViewProtected.bounces = value; // this.nativeViewProtected.alwaysBounceHorizontal = value; } // @ts-ignore [itemTemplatesProperty.getDefault]() { return null; } // @ts-ignore [reverseLayoutProperty.setNative](value) { this.nativeViewProtected.transform = value ? CGAffineTransformMakeRotation(-Math.PI) : null; } // @ts-ignore [reorderLongPressEnabledProperty.setNative](value) { if (value) { if (!this.reorderLongPressGesture) { this.reorderLongPressHandler = ReorderLongPressImpl.initWithOwner(new WeakRef(this)); this.reorderLongPressGesture = UILongPressGestureRecognizer.alloc().initWithTargetAction(this.reorderLongPressHandler, 'longPress'); this.nativeViewProtected.addGestureRecognizer(this.reorderLongPressGesture); } else { this.reorderLongPressGesture.enabled = true; } } else { if (this.reorderLongPressGesture) { this.reorderLongPressGesture.enabled = false; } } } // @ts-ignore [reorderingEnabledProperty.setNative](value) { if (value) { this.on('touch', this.onReorderingTouch, this); } else { this.off('touch', this.onReorderingTouch, this); } } // @ts-ignore [scrollBarIndicatorVisibleProperty.getDefault]() { return true; } // @ts-ignore [scrollBarIndicatorVisibleProperty.setNative](value) { this.updateScrollBarVisibility(value); } updateScrollBarVisibility(value) { if (!this.nativeViewProtected) { return; } if (this.orientation === 'horizontal') { this.nativeViewProtected.showsHorizontalScrollIndicator = value; } else { this.nativeViewProtected.showsVerticalScrollIndicator = value; } } eachChildView(callback) { this._map.forEach((view, key) => { callback(view); }); } onLayout(left, top, right, bottom) { super.onLayout(left, top, right, bottom); const layoutView = this.nativeViewProtected?.collectionViewLayout; if (!layoutView) { return; } const p = CollectionViewBase.plugins[this.layoutStyle]; if (p && p.onLayout) { p.onLayout(this, left, top, right, bottom); } this.plugins.forEach((k) => { const p = CollectionViewBase.plugins[k]; p.onLayout && p.onLayout(this, left, top, right, bottom); }); if (!this._delegate) { const layoutStyle = CollectionViewBase.layoutStyles[this.layoutStyle]; if (layoutStyle && layoutStyle.createDelegate) { this._delegate = layoutStyle.createDelegate(this); } else { // if we use fixed col and row size we want a delegate // without collectionViewLayoutSizeForItemAtIndexPath // because it is not needed and faster if (this._effectiveColWidth && this._effectiveRowHeight) { this._delegate = UICollectionViewDelegateFixedSizeImpl.initWithOwner(this); } else { this._delegate = UICollectionViewDelegateImpl.initWithOwner(this); } } // this._delegate._owner = new WeakRef(this); this.nativeViewProtected.delegate = this._delegate; } this.updateRowColSize(); // there is no need to call refresh if it was triggered before with same size. // this refresh is just to handle size change const layoutKey = this._innerWidth + '_' + this._innerHeight; if (this._lastLayoutKey !== layoutKey) { this.refresh(); } } updateRowColSize() { const layoutView = this.nativeViewProtected?.collectionViewLayout; if (layoutView instanceof UICollectionViewFlowLayout) { if (this._effectiveRowHeight && this._effectiveColWidth) { layoutView.itemSize = CGSizeMake(Utils.layout.toDeviceIndependentPixels(this._effectiveColWidth), Utils.layout.toDeviceIndependentPixels(this._effectiveRowHeight)); } else if (this.estimatedItemSize && !this.autoSize) { layoutView.estimatedItemSize = CGSizeMake(Utils.layout.toDeviceIndependentPixels(this._effectiveColWidth), Utils.layout.toDeviceIndependentPixels(this._effectiveRowHeight)); } layoutView.invalidateLayout(); } } _onRowHeightPropertyChanged(oldValue, newValue) { this.updateRowColSize(); this.refresh(); } _onColWidthPropertyChanged(oldValue, newValue) { this.updateRowColSize(); this.refresh(); } isHorizontal() { return this.orientation === 'horizontal'; } clearCachedSize(...indexes) { const sizes = this._delegate instanceof UICollectionViewDelegateImpl ? this._delegate.cachedSizes : null; if (sizes) { indexes.forEach((index) => { if (index < sizes.count) { sizes.replaceObjectAtIndexWithObject(index, NSValue.valueWithCGSize(CGSizeZero)); } }); } } layoutAttributesForElementsInRect(attributesArray, rect) { if (this.itemOverlap) { let currentDeltaX = 0; let currentDeltaY = 0; for (let index = 0; index < attributesArray.count; index++) { const attributes = attributesArray.objectAtIndex(index); if (attributes.representedElementCategory === 0 /* UICollectionElementCategory.Cell */) { const row = attributes.indexPath.row; if (this.itemOverlap) { attributes.zIndex = row; } const itemOverlap = this.itemOverlap(this.getItemAtIndex(row), row); currentDeltaX += Utils.layout.toDeviceIndependentPixels(Length.toDevicePixels(itemOverlap[1], 0) + Length.toDevicePixels(itemOverlap[3], 0)); currentDeltaY += Utils.layout.toDeviceIndependentPixels(Length.toDevicePixels(itemOverlap[0], 0) + Length.toDevicePixels(itemOverlap[2], 0)); attributes.center = CGPointMake(attributes.center.x + currentDeltaX, attributes.center.y + currentDeltaY); } } } } onSourceCollectionChanged(event) { const view = this.nativeViewProtected; if (!view || this._dataUpdatesSuspended || !this._lastLayoutKey) { return; } if (Trace.isEnabled()) { CLog(CLogTypes.log, 'onItemsChanged', ChangeType.Update, event.action, event.index, event.addedCount, event.removed && event.removed.length); } // we need to clear stored cell sizes and it wont be correct anymore // this.clearCellSize(); const sizes = this._delegate instanceof UICollectionViewDelegateImpl ? this._delegate.cachedSizes : null; const performBatchUpdatesCompletion = (c) => { // if we are not "presented" (viewController hidden) then performBatchUpdatesCompletion would crash const viewIsLoaded = !!this.page?.viewController ? !!this.page.viewController.view.window : true; if (viewIsLoaded) { view.performBatchUpdatesCompletion(c, null); } else { this.refresh(); } }; switch (event.action) { case ChangeType.Delete: { const indexes = NSMutableArray.new(); for (let index = 0; index < event.addedCount; index++) { indexes.addObject(NSIndexPath.indexPathForRowInSection(event.index + index, 0)); if (sizes) { sizes.removeObjectAtIndex(event.index); } } // this._sizes.splice(event.index, event.addedCount); this.unbindUnusedCells(event.removed); if (Trace.isEnabled()) { CLog(CLogTypes.info, 'deleteItemsAtIndexPaths', indexes.count); } performBatchUpdatesCompletion(() => { view.deleteItemsAtIndexPaths(indexes); }); return; } case ChangeType.Update: { const indexes = NSMutableArray.new(); indexes.addObject(NSIndexPath.indexPathForRowInSection(event.index, 0)); if (sizes && event.index < sizes.count) { sizes.replaceObjectAtIndexWithObject(event.index, NSValue.valueWithCGSize(CGSizeZero)); } // this._sizes[event.index] = null; if (Trace.isEnabled()) { CLog(CLogTypes.info, 'reloadItemsAtIndexPaths', event.index, indexes.count); } performBatchUpdatesCompletion(() => { view.reloadItemsAtIndexPaths(indexes); }); return; } case ChangeType.Add: { const indexes = NSMutableArray.new(); for (let index = 0; index < event.addedCount; index++) { indexes.addObject(NSIndexPath.indexPathForRowInSection(event.index + index, 0)); if (sizes) { sizes.insertObjectAtIndex(NSValue.valueWithCGSize(CGSizeZero), event.index); } // this._sizes.splice(index, 0, null); } if (Trace.isEnabled()) { CLog(CLogTypes.info, 'insertItemsAtIndexPaths', indexes.count); } performBatchUpdatesCompletion(() => { view.insertItemsAtIndexPaths(indexes); }); return; } case ChangeType.Splice: { performBatchUpdatesCompletion(() => { const added = event.addedCount; const removed = (event.removed && event.removed.length) || 0; if (added > 0 && added === removed) { const indexes = NSMutableArray.new(); for (let index = 0; index < added; index++) { indexes.addObject(NSIndexPath.indexPathForRowInSection(event.index + index, 0)); if (sizes && event.index + index < sizes.count) { sizes.replaceObjectAtIndexWithObject(event.index + index, NSValue.valueWithCGSize(CGSizeZero)); } // this._sizes[event.index + index] = null; } view.reloadItemsAtIndexPaths(indexes); } else { if (event.removed && event.removed.length > 0) { const indexes = NSMutableArray.new(); for (let index = 0; index < event.removed.length; index++) { indexes.addObject(NSIndexPath.indexPathForItemInSection(event.index + index, 0)); if (sizes) { sizes.removeObjectAtIndex(event.index); } } // this._sizes.splice(event.index, event.removed.length); this.unbindUnusedCells(event.removed); if (Trace.isEnabled()) { CLog(CLogTypes.info, 'deleteItemsAtIndexPaths', indexes.count); } view.deleteItemsAtIndexPaths(indexes); } if (event.addedCount > 0) { const indexes = NSMutableArray.alloc().init(); for (let index = 0; index < event.addedCount; index++) { indexes.addObject(NSIndexPath.indexPathForItemInSection(event.index + index, 0)); if (sizes) { sizes.insertObjectAtIndex(NSValue.valueWithCGSize(CGSizeZero), event.index); } // this._sizes.splice(event.index, 0, null); } if (Trace.isEnabled()) { CLog(CLogTypes.info, 'insertItemsAtIndexPaths', indexes.count); } view.insertItemsAtIndexPaths(indexes); } } // view.collectionViewLayout.invalidateLayout(); }); return; } } this.refresh(); } onItemTemplatesChanged(oldValue, newValue) { super.onItemTemplatesChanged(oldValue, newValue); if (!this.nativeViewProtected) { return; } const view = this.nativeViewProtected; this._itemTemplatesInternal.forEach((t) => { view.registerClassForCellWithReuseIdentifier(CollectionViewCell.class(), t.key.toLowerCase()); }); } unbindUnusedCells(removedDataItems) { this._map.forEach((view, nativeView, map) => { if (!view || !view.bindingContext) { return; } if (removedDataItems.indexOf(view.bindingContext) !== -1) { view.bindingContext = undefined; } }, this); } refreshVisibleItems() { const view = this.nativeViewProtected; if (!view) { return; } const sizes = this._delegate instanceof UICollectionViewDelegateImpl ? this._delegate.cachedSizes : null; const visibles = view.indexPathsForVisibleItems; if (sizes) { const indexes = Array.from(visibles); indexes.forEach((value) => { if (value.row < sizes.count) { sizes.replaceObjectAtIndexWithObject(value.row, NSValue.valueWithCGSize(CGSizeZero)); } }); } UIView.performWithoutAnimation(() => { view.performBatchUpdatesCompletion(() => { view.reloadItemsAtIndexPaths(visibles); }, null); }); } isItemAtIndexVisible(itemIndex) { const view = this.nativeViewProtected; if (!view) { return false; } const indexes = Array.from(view.indexPathsForVisibleItems); return indexes.some((visIndex) => visIndex.row === itemIndex); } findFirstVisibleItemIndex() { const view = this.nativeViewProtected; if (!view) { return -1; } return this.getRowIndexPath(view)[0] ?? -1; } findLastVisibleItemIndex() { const view = this.nativeViewProtected; if (!view) { return -1; } return this.getRowIndexPath(view).at(-1) ?? -1; } getRowIndexPath(view) { return Array.from(view.indexPathsForVisibleItems) .map((e) => e.row) .sort((a, b) => a - b); } refresh() { if (!this.isLoaded || !this.nativeView) { this._isDataDirty = true; return; } this._isDataDirty = false; this._lastLayoutKey = this._innerWidth + '_' + this._innerHeight; if (Trace.isEnabled()) { CLog(CLogTypes.info, 'refresh'); } // we need to clear stored cell sizes and it wont be correct anymore // this.clearCellSize(); const sizes = this._delegate instanceof UICollectionViewDelegateImpl ? this._delegate.cachedSizes : null; if (sizes) { sizes.removeAllObjects(); } // clear bindingContext when it is not observable because otherwise bindings to items won't reevaluate this._map.forEach((view, nativeView, map) => { if (!(view.bindingContext instanceof Observable)) { view.bindingContext = null; } }); // TODO: this is ugly look here: https://github.com/nativescript-vue/nativescript-vue/issues/525 // this.clearRealizedCells(); // dispatch_async(main_queue, () => { this.nativeViewProtected.reloadData(); // }); this.notify({ eventName: CollectionViewBase.dataPopulatedEvent }); } //@ts-ignore get scrollOffset() { const view = this.nativeViewProtected; return (this.isHorizontal() ? view?.contentOffset.x : view?.contentOffset.y) || 0; } get verticalOffsetX() { return this.nativeViewProtected?.contentOffset.x || 0; } get verticalOffsetY() { return this.nativeViewProtected?.contentOffset.y || 0; } scrollToIndex(index, animated = true, snap = SnapPosition.START) { const nativeView = this.nativeViewProtected; if (!nativeView) { return; } const nbItems = nativeView.numberOfItemsInSection(0); if (nbItems > 0 && index < nbItems) { let scrollPosition = 1 /* UICollectionViewScrollPosition.Top */; if (this.orientation === 'vertical') { scrollPosition = snap === SnapPosition.START ? 1 /* UICollectionViewScrollPosition.Top */ : 4 /* UICollectionViewScrollPosition.Bottom */; } else { scrollPosition = snap === SnapPosition.START ? 8 /* UICollectionViewScrollPosition.Left */ : 32 /* UICollectionViewScrollPosition.Right */; } nativeView.scrollToItemAtIndexPathAtScrollPositionAnimated(NSIndexPath.indexPathForItemInSection(index, 0), scrollPosition, animated); } } scrollToOffset(value, animated) { const view = this.nativeViewProtected; if (view && this.isScrollEnabled) { const { width, height } = view.bounds.size; let rect; if (this.orientation === 'vertical') { rect = CGRectMake(0, value, width, height); } else { rect = CGRectMake(value, 0, width, height); } if (rect) { view.scrollRectToVisibleAnimated(rect, animated); } } } requestLayout() { // When preparing cell don't call super - no need to invalidate our measure when cell desiredSize is changed. if (!this._preparingCell) { super.requestLayout(); } } _setNativeClipToBounds() { this.nativeView.clipsToBounds = true; } notifyForItemAtIndex(eventName, view, index, bindingContext, native) { const args = { eventName, object: this, index, view, ios: native, bindingContext }; this.notify(args); return args; } _getItemTemplateType(indexPath) { const selector = this._itemTemplateSelector; let type = this._defaultTemplate.key; if (selector) { type = selector.call(this, this.getItemAtIndex(indexPath.item), indexPath.item, this.items); } return type.toLowerCase(); } getItemTemplateContent(index, templateType) { return this.getViewForViewType(ListViewViewTypes.ItemView, templateType); } _prepareCell(cell, indexPath, templateType, notForCellSizeComp = true) { let cellSize; try { this._preparingCell = true; const firstRender = !cell.view; let view = cell.view; const index = indexPath.row; if (!view) { view = this.getItemTemplateContent(index, templateType); } const bindingContext = this._prepareItem(view, index); if (Trace.isEnabled()) { CLog(CLogTypes.log, '_prepareCell', index, templateType, !!cell.view, !!view, cell.view !== view, notForCellSizeComp); } const args = this.notifyForItemAtIndex(CollectionViewBase.itemLoadingEvent, view, indexPath.row, bindingContext, cell); view = args.view; if (firstRender) { view['iosIgnoreSafeArea'] = true; } view.bindingContext = bindingContext; if (view instanceof ProxyViewContainer) { const sp = new ContentView(); sp.content = view; view = sp; } if (!cell.view) { cell.owner = new WeakRef(view); } else if (cell.view !== view) { this._removeContainer(cell); if (cell.view.nativeViewProtected) { cell.view.nativeViewProtected.removeFromSuperview(); } cell.owner = new WeakRef(view); } cell.currentIndex = indexPath.row; if (notForCellSizeComp) { this._map.set(cell, view); } if (view && !view.parent) { this._addView(view); const innerView = NSCellView.new(); innerView.autoresizingMask = 2 /* UIViewAutoresizing.FlexibleWidth */ | 16 /* UIViewAutoresizing.FlexibleHeight */; innerView.view = new WeakRef(view); if (notForCellSizeComp && this.autoReloadItemOnLayout) { // for a cell to update correctly on cell layout change we need // to do it ourself instead of "propagating it" view['performLayout'] = () => { if (!this._preparingCell && !view['inPerformLayout']) { view['inPerformLayout'] = true; const index = cell.currentIndex; const nativeView = this.nativeViewProtected; const sizes = this._delegate instanceof UICollectionViewDelegateImpl ? this._delegate.cachedSizes : null; if (sizes && index < sizes.count) { sizes.replaceObjectAtIndexWithObject(index, NSValue.valueWithCGSize(CGSizeZero)); } nativeView.performBatchUpdatesCompletion(() => { this.notifyForItemAtIndex(CollectionViewBase.itemLoadingEvent, view, indexPath.row, view.bindingContext, cell); // the order is important because measureCell will set layout as requested and notifyForItemAtIndex would call requestLayout => endless loop this.measureCell(cell, view, index); cell.contentView.subviews.objectAtIndex(0)?.layoutSubviews(); // nativeView.collectionViewLayout.invalidateLayout(); }, null); view['inPerformLayout'] = false; } }; } innerView.addSubview(view.nativeViewProtected); cell.contentView.addSubview(innerView); } cellSize = this.measureCell(cell, view, indexPath.row); if (notForCellSizeComp) { view.notify({ eventName: CollectionViewBase.bindedEvent }); } if (Trace.isEnabled()) { CLog(CLogTypes.log, '_prepareCell done', index, cellSize); } } finally { this._preparingCell = false; } return cellSize; } getCellSize(index) { // let result = this._sizes[index]; let result; // CLog(CLogTypes.log, 'getCellSize', index, result, this._effectiveColWidth, this._effectiveRowHeight, this.getMeasuredWidth(), this.getMeasuredHeight()); if (!result) { let width = this._effectiveColWidth; let height = this._effectiveRowHeight; if (this.spanSize) { const dataItem = this.getItemAtIndex(index); const spanSize = this.spanSize(dataItem, index); const horizontal = this.isHorizontal(); if (horizontal) { height *= spanSize; } else { width *= spanSize; } } if (width && height) { result = [width, height]; } else if (height && this.orientation === 'vertical') { result = [this.getMeasuredWidth(), height]; } else if (width && this.orientation === 'horizontal') { result = [width, this.getMeasuredHeight()]; } } // return undefined; return result; } // public storeCellSize(index: number, value) { // this._sizes[index] = value; // } // public clearCellSize() { // this._sizes = new Array<number[]>(); // } measureCell(cell, cellView, position) { if (cellView) { let width = this._effectiveColWidth; let height = this._effectiveRowHeight; const horizontal = this.isHorizontal(); if (this.spanSize) { const dataItem = this.getItemAtIndex(position); const spanSize = this.spanSize(dataItem, position); if (horizontal) { height *= spanSize; } else { width *= spanSize; } } const widthMeasureSpec = width ? Utils.layout.makeMeasureSpec(width, Utils.layout.EXACTLY) : horizontal ? infinity : Utils.layout.makeMeasureSpec(this._innerWidth, Utils.layout.EXACTLY); const heightMeasureSpec = height ? Utils.layout.makeMeasureSpec(height, Utils.layout.EXACTLY) : horizontal ? Utils.layout.makeMeasureSpec(this._innerHeight, Utils.layout.EXACTLY) : infinity; if (Trace.isEnabled()) { CLog(CLogTypes.log, 'measureCell', position, width, height, this._innerWidth, this._innerHeight, widthMeasureSpec, heightMeasureSpec); } const measuredSize = View.measureChild(this, cellView, widthMeasureSpec, heightMeasureSpec); const result = [measuredSize.measuredWidth, measuredSize.measuredHeight]; // this.storeCellSize(index.row, result); return result; } return undefined; } layoutCell(index, cell, cellView) { // const cellSize = this.getCellSize(index); const size = cell.bounds.size; View.layoutChild(this, cellView, 0, 0, Utils.layout.toDevicePixels(size.width), Utils.layout.toDevicePixels(size.height)); if (Trace.isEnabled()) { CLog(CLogTypes.log, 'layoutCell', index, cellView.getMeasuredWidth(), cellView.getMeasuredHeight()); } } clearRealizedCells() { this._map.forEach((value, key) => { this._removeContainer(key); this._clearCellViews(key); }); this._map.clear(); } _clearCellViews(cell) { if (cell && cell.view) { if (cell.view.nativeViewProtected) { cell.view.nativeViewProtected.removeFromSuperview(); } cell.owner = undefined; } } _removeContainer(cell) { const view = cell.view; this.notifyForItemAtIndex(CollectionViewBase.itemDisposingEvent, view, cell.currentIndex, view.bindingContext, cell); // This is to clear the StackLayout that is used to wrap ProxyViewContainer instances. if (!(view.parent instanceof CollectionView)) { this._removeView(view.parent); } // No need to request layout when we are removing cells. const preparing = this._preparingCell; this._preparingCell = true; view.parent._removeView(view); this._preparingCell = preparing; this._map.delete(cell); } _setPadding(newPadding) { const layout = this._layout; // Need to guard sectionInset when using custom layouts if (layout && layout['sectionInset']) { const padding = { top: layout['sectionInset'].top, right: layout['sectionInset'].right, bottom: layout['sectionInset'].bottom, left: layout['sectionInset'].left, }; // tslint:disable-next-line:prefer-object-spread const newValue = Object.assign(padding, newPadding); layout['sectionInset'] = newValue; } } numberOfSectionsInCollectionView(collectionView) { if (!this._lastLayoutKey) { return 0; } return 1; } collectionViewNumberOfItemsInSection(collectionView, section) { return this.items?.length || 0; } collectionViewCellForItemAtIndexPath(collectionView, indexPath) { const templateType = this._getItemTemplateType(indexPath); let cell = collectionView.dequeueReusableCellWithReuseIdentifierForIndexPath(templateType, indexPath); if (!cell) { cell = CollectionViewCell.new(); } if (this.itemOverlap) { // should we force clipsToBounds? not doing so allows more complex layouts like overlapping // we set zPosition to allow overlap. Should we make it an option? cell.layer.zPosition = indexPath.row; } cell.clipsToBounds = true; const firstRender = !cell.view; if (Trace.isEnabled()) { CLog(CLogTypes.log, 'collectionViewCellForItemAtIndexPath', indexPath.row, templateType, !!cell.view, cell); } this._prepareCell(cell, indexPath, templateType); // the cell layout will be called from NSCellView layoutSubviews const cellView = cell.view; if (!firstRender && cellView['isLayoutRequired']) { this.layoutCell(indexPath.row, cell, cellView); } // if the cell view is a canvas we need to ensure redraw is called (cellView.content || cellView).nativeViewProtected.setNeedsDisplay(); return cell; } collectionViewWillDisplayCellForItemAtIndexPath(collectionView, cell, indexPath) { if (this.reverseLayout) { cell.transform = CGAffineTransformMakeRotation(-Math.PI); } if (this.items) { const loadMoreItemIndex = this.items.length - this.loadMoreThreshold; if (indexPath.row === loadMoreItemIndex && this.hasListeners(CollectionViewBase.loadMoreItemsEvent)) { this.notify({ eventName: CollectionViewBase.loadMoreItemsEvent, object: this, }); } } if (this.hasListeners(CollectionViewBase.displayItemEvent)) { this.notify({ eventName: CollectionViewBase.displayItemEvent, index: indexPath.row, object: this, }); } if (cell.preservesSuperviewLayoutMargins) { cell.preservesSuperviewLayoutMargins = false; } if (cell.layoutMargins) { cell.layoutMargins = UIEdgeInsetsZero; } } collectionViewDidSelectItemAtIndexPath(collectionView, indexPath) { const cell = collectionView.cellForItemAtIndexPath(indexPath); const position = indexPath.row; this.notify({ eventName: CollectionViewBase.itemTapEvent, object: this, item: this.getItemAtIndex(position), index: position, view: cell.view, }); cell.highlighted = false; return indexPath; } collectionViewDidHighlightItemAtIndexPath(collectionView, indexPath) { const cell = collectionView.cellForItemAtIndexPath(indexPath); const position = indexPath?.row; this.notify({ eventName: CollectionViewBase.itemHighlightEvent, object: this, item: this.getItemAtIndex(position), index: position, view: cell?.view, }); } collectionViewDidUnhighlightItemAtIndexPath(collectionView, indexPath) { const cell = collectionView.cellForItemAtIndexPath(indexPath); const position = indexPath?.row; this.notify({ eventName: CollectionViewBase.itemHighlightEndEvent, object: this, item: this.getItemAtIndex(position), index: position, view: cell?.view, }); } collectionViewLayoutSizeForItemAtIndexPath(collectionView, collectionViewLayout, indexPath) { const row = indexPath.row; let measuredSize = this.getCellSize(row); if (!measuredSize) { if (Trace.isEnabled()) { CLog(CLogTypes.log, 'collectionViewLayoutSizeForItemAtIndexPath', row); } const templateType = this._getItemTemplateType(indexPath); if (templateType) { const measureData = this._measureCellMap.get(templateType); let cell = measureData && measureData.cell; let needsSet = false; if (!cell) { cell = CollectionViewCell.new(); needsSet = true; } else if (!cell.view) { cell.owner = new WeakRef(measureData.view); needsSet = true; } measuredSize = this._prepareCell(cell, indexPath, templateType, false); if (needsSet) { this._measureCellMap.set(templateType, { cell, view: cell.view }); } } } if (Trace.isEnabled()) { CLog(CLogTypes.log, 'collectionViewLayoutSizeForItemAtIndexPath', row, measuredSize); } if (measuredSize) { return CGSizeMake(Utils.layout.toDeviceIndependentPixels(measuredSize[0]), Utils.layout.toDeviceIndependentPixels(measuredSize[1])); } return CGSizeZero; } computeScrollEventData(scrollView, eventName, dx, dy) { const horizontal = this.isHorizontal(); const safeAreaInsetsTop = this.iosIgnoreSafeArea ? 0 : scrollView.safeAreaInsets.top; const offset = horizontal ? scrollView.contentOffset.x : scrollView.contentOffset.y + safeAreaInsetsTop; const size = horizontal ? scrollView.contentSize.width - scrollView.bounds.size.width : scrollView.contentSize.height - scrollView.bounds.size.height + safeAreaInsetsTop; return { object: this, eventName, scrollOffset: offset, scrol