@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
JavaScript
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