UNPKG

@nstudio/ui-collectionview

Version:

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

1,164 lines 57.5 kB
var _a, _b; /* eslint-disable no-redeclare */ import { ChangeType, ContentView, CoreTypes, Length, Property, ProxyViewContainer, Trace, Utils, View, booleanConverter, paddingBottomProperty, paddingLeftProperty, paddingRightProperty, paddingTopProperty, profile } from '@nativescript/core'; import { itemOverlapProperty, reorderLongPressEnabledProperty, reorderingEnabledProperty, reverseLayoutProperty, scrollBarIndicatorVisibleProperty } from '.'; import { CLog, CLogTypes, CollectionViewBase, ListViewViewTypes, isScrollEnabledProperty, orientationProperty } from './common'; import { createArrayBuffer } from '@nativescript-community/arraybuffers'; export * from './common'; let CollectionViewCellHolder; let CollectionViewRecyclerView; var SimpleCallback = /** @class */ (function (_super) { __extends(SimpleCallback, _super); function SimpleCallback(param1, param2) { var _this = _super.call(this, param1, param2) || this; _this.startPosition = -1; _this.endPosition = -1; return global.__native(_this); } SimpleCallback.prototype.onMove = function (recyclerview, viewHolder, target) { var _a; var startPosition = viewHolder.getAdapterPosition(); var endPosition = target.getAdapterPosition(); var owner = (_a = this.owner) === null || _a === void 0 ? void 0 : _a.get(); if (owner && !owner._canReorderToPosition(startPosition, endPosition, owner.getItemAtIndex(endPosition))) { return false; } if (this.startPosition === -1) { this.startPosition = startPosition; } this.endPosition = endPosition; if (owner) { owner._reorderItemInSource(startPosition, endPosition); return true; } return false; }; SimpleCallback.prototype.onSelectedChanged = function (viewHolder, state) { var _a; if (viewHolder) { if (this.startPosition === -1) { this.startPosition = viewHolder.getAdapterPosition(); } } if (!viewHolder) { // this is where we identify the end of the drag and call the end event var owner = (_a = this.owner) === null || _a === void 0 ? void 0 : _a.get(); if (this.endPosition === -1) { this.endPosition = this.startPosition; } if (owner) { var item = owner.getItemAtIndex(this.startPosition); owner._callItemReorderedEvent(this.startPosition, this.endPosition, item); } this.startPosition = -1; this.endPosition = -1; owner.isDragging = false; } }; SimpleCallback.prototype.onSwiped = function (viewHolder, direction) { }; SimpleCallback.prototype.isItemViewSwipeEnabled = function () { // disabled for now return false; }; SimpleCallback.prototype.isLongPressDragEnabled = function () { // we use our custom longpress gesture handler return false; }; return SimpleCallback; }(androidx.recyclerview.widget.ItemTouchHelper.SimpleCallback)); var LongPressGestureListenerImpl = /** @class */ (function (_super) { __extends(LongPressGestureListenerImpl, _super); function LongPressGestureListenerImpl(_owner) { var _this = _super.call(this) || this; _this._owner = _owner; return global.__native(_this); } LongPressGestureListenerImpl.prototype.onLongPress = function (motionEvent) { var _a; var owner = (_a = this._owner) === null || _a === void 0 ? void 0 : _a.get(); if (owner) { owner.onReorderLongPress(motionEvent); } }; return LongPressGestureListenerImpl; }(android.view.GestureDetector.SimpleOnGestureListener)); let LayoutParams; const extraLayoutSpaceProperty = new Property({ name: 'extraLayoutSpace', }); const itemViewCacheSizeProperty = new Property({ name: 'itemViewCacheSize', }); const nestedScrollingEnabledProperty = new Property({ name: 'nestedScrollingEnabled', defaultValue: true, valueConverter: booleanConverter, }); export var SnapPosition; (function (SnapPosition) { SnapPosition[SnapPosition["START"] = 0] = "START"; SnapPosition[SnapPosition["END"] = 1] = "END"; })(SnapPosition || (SnapPosition = {})); export class CollectionView extends CollectionViewBase { constructor() { super(...arguments); this.templateTypeNumberString = new Map(); this.templateStringTypeNumber = new Map(); this._currentNativeItemType = 0; this.currentSpanCount = 1; // used to store viewHolder and thus their corresponding Views // used to "destroy" cells when possible this._viewHolders = new Set(); this._scrollOrLoadMoreChangeCount = 0; this.scrolling = false; this.needsScrollStartEvent = false; this.animateItemUpdate = false; this.mInPropertiesSet = false; this.mShouldUpdateInnerSize = false; this.mShouldUpdateSpanCount = false; this.mShouldRefresh = false; this.defaultPoolSize = 10; this.desiredPoolSize = new Map(); this.isDragging = false; this._layedOut = false; this.bindedViewHolders = new Set(); } onResumeNativeUpdates() { // {N} suspends properties update on `_suspendNativeUpdates`. So we only need to do this in onResumeNativeUpdates this.mInPropertiesSet = true; super.onResumeNativeUpdates(); this.mInPropertiesSet = false; if (this.mShouldUpdateInnerSize) { this.updateInnerSize(); } if (this.mShouldUpdateSpanCount) { this.updateSpanCount(); } if (this.mShouldRefresh) { this.refresh(); } } createNativeView() { // storing the class in a property for reuse in the future cause a materializing which is pretty slow! if (!CollectionViewRecyclerView) { CollectionViewRecyclerView = com.nativescript.collectionview.RecyclerView; } const recyclerView = CollectionViewRecyclerView.createRecyclerView(this._context); // const expMgr = new RecyclerViewExpandableItemManager(null); // adapter.setDisplayHeadersAtStartUp(true).setStickyHeaders(true); //Make headers sticky // Endless scroll with 1 item threshold // .setLoadingMoreAtStartUp(true) // .setEndlessScrollListener(this, new ProgressItem()) // .setEndlessScrollThreshold(1); //Default=1 // const fastScroller = new com.l4digital.fastscroll.FastScroller(this._context); // fastScroller.setSectionIndexer(adapter); // fastScroller.attachRecyclerView(recyclerView); return recyclerView; } initNativeView() { super.initNativeView(); const nativeView = this.nativeViewProtected; if (!this.recycledViewPool) { this.recycledViewPool = new com.nativescript.collectionview.RecycledViewPool(); this.recycledViewPoolDisposeListener = new com.nativescript.collectionview.RecycledViewPool.ViewPoolListener({ onViewHolderDisposed: (holder) => { if (Trace.isEnabled()) { CLog(CLogTypes.log, 'onViewHolderDisposed', holder); } if (this._viewHolders) { this._viewHolders.delete(holder); } const isNonSync = holder['defaultItemView'] === true; const view = isNonSync ? holder.view.content : holder.view; this.notifyForItemAtIndex(CollectionViewBase.itemDisposingEvent, view, holder.getAdapterPosition(), view.bindingContext, holder); if (view && view.isLoaded) { view.callUnloaded(); } view._isAddedToNativeVisualTree = false; //@ts-ignore view.parent = null; view._tearDownUI(); }, }); this.recycledViewPool.mListener = this.recycledViewPoolDisposeListener; } if (Trace.isEnabled() || this.hasListeners(CollectionViewBase.itemRecyclingEvent)) { const recyclerListener = (this.recyclerListener = new androidx.recyclerview.widget.RecyclerView.RecyclerListener({ onViewRecycled: (holder) => { if (Trace.isEnabled()) { CLog(CLogTypes.log, 'onViewRecycled', this, nativeView, holder); } const isNonSync = holder['defaultItemView'] === true; const view = isNonSync ? holder.view.content : holder.view; this.notifyForItemAtIndex(CollectionViewBase.itemRecyclingEvent, view, holder.getAdapterPosition(), view.bindingContext, holder); }, })); nativeView.setRecyclerListener(recyclerListener); } nativeView.setRecycledViewPool(this.recycledViewPool); let layoutManager; if (CollectionViewBase.layoutStyles[this.layoutStyle]) { layoutManager = CollectionViewBase.layoutStyles[this.layoutStyle].createLayout(this); } else { layoutManager = new com.nativescript.collectionview.PreCachingGridLayoutManager(this._context, 1); if (this.hasListeners(CollectionView.layoutCompletedEvent)) { layoutManager.layoutCompletedListener = new com.nativescript.collectionview.GridLayoutManager.LayoutCompletedListener({ onLayoutCompleted: () => { this.notify({ eventName: CollectionView.layoutCompletedEvent }); }, }); } // layoutManager = new PreCachingGridLayoutManager(this._context, 1); // (layoutManager as any).owner = new WeakRef(this); } // this.spanSize nativeView.setLayoutManager(layoutManager); nativeView.layoutManager = layoutManager; nativeView.sizeChangedListener = new com.nativescript.collectionview.SizeChangedListener({ onLayout: (changed, left, top, right, bottom) => changed && this.onLayout(left, top, right, bottom), onMeasure: (widthMeasureSpec, heightMeasureSpec) => this.onMeasure(widthMeasureSpec, heightMeasureSpec), }); this.spanSize = this._getSpanSize; // const animator = new jp.wasabeef.recyclerview.animators.FadeInAnimator(); // // animator.setInterpolator(new android.view.animation.OvershootInterpolator()); // animator.setMoveDuration(200); // Change animations are enabled by default since support-v7-recyclerview v22. // Need to disable them when using animation indicator. // animator.setSupportsChangeAnimations(false); // nativeView.setItemAnimator(animator); // enforce the first refresh for the collectionview to be ready as soon as possible this.refresh(true); } disposeNativeView() { // this.eachChildView((view) => { // view.parent._removeView(view); // return true; // }); // this._realizedItems.clear(); const nativeView = this.nativeViewProtected; nativeView.setRecyclerListener(null); nativeView.setRecycledViewPool(null); this.recycledViewPoolDisposeListener = null; this.recycledViewPool = null; this.detachScrollListenerIfNecessary(true); nativeView.sizeChangedListener = null; nativeView.layoutManager = null; this._listViewAdapter = null; this._itemTouchHelper = null; this._simpleItemTouchCallback = null; this.disposeViewHolderViews(); this.clearTemplateTypes(); super.disposeNativeView(); } onLoaded() { super.onLoaded(); this.attachScrollListenerIfNecessary(); } // onUnloaded() { // super.onUnloaded(); // this.detachScrollListenerIfNecessary(); // } getViewForItemAtIndex(index) { return this.enumerateViewHolders((v) => (v.getAdapterPosition() === index ? v.view : undefined)); } //@ts-ignore set spanSize(inter) { if (!(typeof inter === 'function')) { return; } this._getSpanSize = inter; const layoutManager = this.layoutManager; if (layoutManager && layoutManager['setSpanSizeLookup']) { if (inter) { layoutManager['setSpanSizeLookup'](new com.nativescript.collectionview.SpanSizeLookup(new com.nativescript.collectionview.SpanSizeLookup.Interface({ getSpanSize: (position) => { const dataItem = this.getItemAtIndex(position); return Math.min(inter(dataItem, position), this.currentSpanCount); }, }))); } else { layoutManager['setSpanSizeLookup'](null); } } } get spanSize() { return this._getSpanSize; } nativeGetItemOverlap(position) { const item = this.getItemAtIndex(position); const result = this.itemOverlap(item, position); this.decoratorBuffer[0] = Length.toDevicePixels(result[0], 0); this.decoratorBuffer[1] = Length.toDevicePixels(result[1], 0); this.decoratorBuffer[2] = Length.toDevicePixels(result[2], 0); this.decoratorBuffer[3] = Length.toDevicePixels(result[3], 0); return this.decoratorBuffer; } [itemOverlapProperty.setNative](value) { if (typeof value === 'function') { if (!this.decorator) { this.decoratorListener = new com.nativescript.collectionview.OverlapDecoration.OverlapDecorationListener({ getItemOverlap: this.nativeGetItemOverlap.bind(this), }); this.decorator = new com.nativescript.collectionview.OverlapDecoration(this.decoratorListener); this.nativeViewProtected.addItemDecoration(this.decorator); } if (!this.decoratorBuffer) { this.decoratorBuffer = createArrayBuffer(4, false, true); } } else { if (this.decorator) { this.nativeViewProtected.removeItemDecoration(this.decorator); this.decorator = null; } this.decoratorListener = null; } } attachScrollListenerIfNecessary() { const nativeView = this.nativeViewProtected; if (this._scrollOrLoadMoreChangeCount > 0 && nativeView && !nativeView.scrollListener) { this._nScrollListener = new com.nativescript.collectionview.OnScrollListener.Listener({ onScrollStateChanged: this.onScrollStateChanged.bind(this), onScrolled: this.onScrolled.bind(this), }); const scrollListener = new com.nativescript.collectionview.OnScrollListener(this._nScrollListener); nativeView.addOnScrollListener(scrollListener); nativeView.scrollListener = scrollListener; } } detachScrollListenerIfNecessary(force = false) { const nativeView = this.nativeViewProtected; if (force || (this._scrollOrLoadMoreChangeCount === 0 && nativeView)) { if (nativeView.scrollListener) { this.nativeView.removeOnScrollListener(nativeView.scrollListener); nativeView.scrollListener = null; } this._nScrollListener = null; } } computeScrollEventData(view, eventName, dx, dy) { const horizontal = this.isHorizontal(); const offset = horizontal ? view.computeHorizontalScrollOffset() : view.computeVerticalScrollOffset(); const range = horizontal ? view.computeHorizontalScrollRange() : view.computeVerticalScrollRange(); const extent = horizontal ? view.computeHorizontalScrollExtent() : view.computeVerticalScrollExtent(); return { object: this, eventName, scrollOffset: offset / Utils.layout.getDisplayDensity(), scrollSize: (range - extent) / Utils.layout.getDisplayDensity(), scrollOffsetPercentage: offset / (range - extent), dx, dy, }; } onScrolled(view, dx, dy) { if (!this || !this.scrolling) { return; } if (this.needsScrollStartEvent) { this.needsScrollStartEvent = false; if (this.hasListeners(CollectionViewBase.scrollStartEvent)) { this.notify(this.computeScrollEventData(view, CollectionViewBase.scrollStartEvent, dx, dy)); } } if (this.hasListeners(CollectionViewBase.scrollEvent)) { this.notify(this.computeScrollEventData(view, CollectionViewBase.scrollEvent, dx, dy)); } if (this.hasListeners(CollectionViewBase.loadMoreItemsEvent) && this.items) { const layoutManager = view.getLayoutManager(); if (layoutManager['findLastCompletelyVisibleItemPosition']) { const lastVisibleItemPos = layoutManager['findLastCompletelyVisibleItemPosition'](); const loadMoreItemIndex = this.items.length - this.loadMoreThreshold; if (lastVisibleItemPos === loadMoreItemIndex) { this.notify({ eventName: CollectionViewBase.loadMoreItemsEvent }); } } else if (layoutManager['findLastCompletelyVisibleItemPositions'] && layoutManager['getSpanCount']) { let positions = Array.create('int', layoutManager['getSpanCount']()); positions = layoutManager['findLastCompletelyVisibleItemPositions'](positions); let lastVisibleItemPos = 0; for (let i = 0; i < positions.length; i++) { if (positions[i] > lastVisibleItemPos) { lastVisibleItemPos = positions[i]; } } const loadMoreItemIndex = this.items.length - this.loadMoreThreshold; if (lastVisibleItemPos >= loadMoreItemIndex) { this.notify({ eventName: CollectionViewBase.loadMoreItemsEvent }); } } } } onScrollStateChanged(view, newState) { if (this.scrolling && newState === 0) { // SCROLL_STATE_IDLE this.scrolling = false; if (this.hasListeners(CollectionViewBase.scrollEndEvent)) { this.notify(this.computeScrollEventData(view, CollectionViewBase.scrollEndEvent)); } } else if (!this.scrolling && newState === 1) { //SCROLL_STATE_DRAGGING this.needsScrollStartEvent = true; this.scrolling = true; } } addEventListener(arg, callback, thisArg) { super.addEventListener(arg, callback, thisArg); if (arg === CollectionViewBase.scrollEvent || arg === CollectionViewBase.scrollStartEvent || arg === CollectionViewBase.scrollEndEvent || arg === CollectionViewBase.loadMoreItemsEvent) { this._scrollOrLoadMoreChangeCount++; this.attachScrollListenerIfNecessary(); } } removeEventListener(arg, callback, thisArg) { super.removeEventListener(arg, callback, thisArg); if (arg === CollectionViewBase.scrollEvent || arg === CollectionViewBase.scrollStartEvent || arg === CollectionViewBase.scrollEndEvent || arg === CollectionViewBase.loadMoreItemsEvent) { this._scrollOrLoadMoreChangeCount--; this.detachScrollListenerIfNecessary(); } } //@ts-ignore get android() { return this.nativeView; } get layoutManager() { return this.nativeViewProtected && this.nativeViewProtected.layoutManager; } _getViewLayoutParams() { if (this.isHorizontal()) { if (!this._hlayoutParams) { LayoutParams = LayoutParams || android.view.ViewGroup.LayoutParams; this._hlayoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); } return this._hlayoutParams; } else { if (!this._vlayoutParams) { LayoutParams = LayoutParams || android.view.ViewGroup.LayoutParams; this._vlayoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); } return this._vlayoutParams; } } setNativePoolSize(key, nativeIndex) { if (this.desiredPoolSize.has(key)) { this.nativeViewProtected.getRecycledViewPool().setMaxRecycledViews(nativeIndex, this.desiredPoolSize.get(key)); } else { if (this.defaultPoolSize >= 0) { this.nativeViewProtected.getRecycledViewPool().setMaxRecycledViews(nativeIndex, this.defaultPoolSize); } } } setPoolSizes() { if (!this.nativeViewProtected || !this.templateTypeNumberString) { return; } this.desiredPoolSize.forEach((v, k) => { if (this.templateTypeNumberString.has(k)) { this.nativeViewProtected.getRecycledViewPool().setMaxRecycledViews(this.templateTypeNumberString.get(k), v); } }); } setPoolSize(key, size) { this.desiredPoolSize.set(key, size); this.setPoolSizes(); } [paddingTopProperty.getDefault]() { return { value: this._defaultPaddingTop, unit: 'px' }; } [paddingTopProperty.setNative](value) { this._setPadding({ top: this.effectivePaddingTop }); } [paddingRightProperty.getDefault]() { return { value: this._defaultPaddingRight, unit: 'px' }; } [paddingRightProperty.setNative](value) { this._setPadding({ right: this.effectivePaddingRight }); } [paddingBottomProperty.getDefault]() { return { value: this._defaultPaddingBottom, unit: 'px' }; } [paddingBottomProperty.setNative](value) { this._setPadding({ bottom: this.effectivePaddingBottom }); } [paddingLeftProperty.getDefault]() { return { value: this._defaultPaddingLeft, unit: 'px' }; } [paddingLeftProperty.setNative](value) { this._setPadding({ left: this.effectivePaddingLeft }); } [orientationProperty.getDefault]() { return 'vertical'; } [orientationProperty.setNative](value) { const layoutManager = this.layoutManager; if (!layoutManager || !layoutManager['setOrientation']) { return; } if (this.isHorizontal()) { layoutManager['setOrientation'](0); } else { layoutManager['setOrientation'](1); } this.updateScrollBarVisibility(this.scrollBarIndicatorVisible); } [isScrollEnabledProperty.setNative](value) { const layoutManager = this.layoutManager; if (layoutManager && layoutManager.setScrollEnabled) { layoutManager.setScrollEnabled(value); } } [reverseLayoutProperty.setNative](value) { const layoutManager = this.layoutManager; if (layoutManager && layoutManager.setReverseLayout) { layoutManager.setReverseLayout(value); // layoutManager['setStackFromEnd'](value); } } [nestedScrollingEnabledProperty.setNative](value) { this.nativeViewProtected.setNestedScrollingEnabled(value); } [extraLayoutSpaceProperty.setNative](value) { const layoutManager = this.layoutManager; if (layoutManager && layoutManager['setExtraLayoutSpace']) { layoutManager['setExtraLayoutSpace'](value); } } [itemViewCacheSizeProperty.setNative](value) { this.nativeViewProtected.setItemViewCacheSize(value); } [scrollBarIndicatorVisibleProperty.getDefault]() { return true; } [scrollBarIndicatorVisibleProperty.setNative](value) { this.updateScrollBarVisibility(value); } updateScrollBarVisibility(value) { if (!this.nativeViewProtected) { return; } if (this.orientation === 'horizontal') { this.nativeViewProtected.setHorizontalScrollBarEnabled(value); } else { this.nativeViewProtected.setVerticalScrollBarEnabled(value); } } enumerateViewHolders(cb) { let result, v; for (let it = this._viewHolders.values(), cellItemView = null; (cellItemView = it.next().value);) { if (cellItemView['position'] === undefined) { continue; } result = cb(cellItemView); if (result) { return result; } } return result; } async enumerateViewHoldersAsync(cb) { let result, v; for (let it = this._viewHolders.values(), cellItemView = null; (cellItemView = it.next().value);) { if (cellItemView['position'] === undefined) { continue; } result = await cb(cellItemView); if (result) { return result; } } return result; } startDragging(index) { if (this.reorderEnabled && this._itemTouchHelper) { // let viewHolder: CollectionViewCellHolder; const viewHolder = this.enumerateViewHolders((v) => (v.getAdapterPosition() === index ? v : undefined)); if (viewHolder) { this.startViewHolderDragging(index, viewHolder); } } } startViewHolderDragging(index, viewHolder) { // isDragging is to prevent longPress from triggering and starting a new drag // when triggered manually if (!this.isDragging && this.shouldMoveItemAtIndex(index)) { this.isDragging = true; this._itemTouchHelper.startDrag(viewHolder); } } onReorderLongPress(motionEvent) { const collectionView = this.nativeViewProtected; if (!collectionView) { return; } const view = collectionView.findChildViewUnder(motionEvent.getX(), motionEvent.getY()); const viewHolder = view != null ? collectionView.getChildViewHolder(view) : null; if (viewHolder) { this.startViewHolderDragging(viewHolder.getAdapterPosition(), viewHolder); } } _reorderItemInSource(oldPosition, newPosition) { const adapter = this._listViewAdapter; // 3. Tell adapter to render the model update. adapter.notifyItemMoved(oldPosition, newPosition); // on android _reorderItemInSource is call on every "move" and needs to update the adapter/items // we will call events only at then end super._reorderItemInSource(oldPosition, newPosition, false); } [_a = reorderLongPressEnabledProperty.setNative](value) { if (value) { if (!this._longPressGesture) { this._longPressGesture = new androidx.core.view.GestureDetectorCompat(this._context, new LongPressGestureListenerImpl(new WeakRef(this))); this._itemTouchListerner = new androidx.recyclerview.widget.RecyclerView.OnItemTouchListener({ onInterceptTouchEvent: (view, event) => { if (this.reorderEnabled && this._longPressGesture) { this._longPressGesture.onTouchEvent(event); } return false; }, onTouchEvent: (param0, param1) => { }, onRequestDisallowInterceptTouchEvent: (disallowIntercept) => { }, }); } this.nativeViewProtected.addOnItemTouchListener(this._itemTouchListerner); } else { if (this._itemTouchListerner) { this.nativeViewProtected.removeOnItemTouchListener(this._itemTouchListerner); } } } [_b = reorderingEnabledProperty.setNative](value) { if (value) { if (!this._simpleItemTouchCallback) { const ItemTouchHelper = androidx.recyclerview.widget.ItemTouchHelper; this._simpleItemTouchCallback = new SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.START | ItemTouchHelper.END, 0); this._simpleItemTouchCallback.owner = new WeakRef(this); this._itemTouchHelper = new androidx.recyclerview.widget.ItemTouchHelper(this._simpleItemTouchCallback); this._itemTouchHelper.attachToRecyclerView(this.nativeViewProtected); } } } onItemViewLoaderChanged() { if (this.itemViewLoader) { this.refresh(); } } onItemTemplateSelectorChanged(oldValue, newValue) { super.onItemTemplateSelectorChanged(oldValue, newValue); this.clearTemplateTypes(); this.refresh(); } onItemTemplateChanged(oldValue, newValue) { super.onItemTemplateChanged(oldValue, newValue); // TODO: update current template with the new one this.refresh(); } onItemTemplatesChanged(oldValue, newValue) { super.onItemTemplatesChanged(oldValue, newValue); // TODO: update current template with the new one this.refresh(); } updateSpanCount(requestLayout = true) { if (this.mInPropertiesSet) { this.mShouldUpdateSpanCount = true; return false; } this.mShouldUpdateSpanCount = false; const layoutManager = this.layoutManager; if (layoutManager && layoutManager['setSpanCount']) { const newValue = (this.currentSpanCount = this.computeSpanCount()); if (newValue !== layoutManager['getSpanCount']()) { layoutManager['setSpanCount'](newValue); if (requestLayout) { layoutManager.requestLayout(); } return true; } } return false; } updateInnerSize() { if (this.mInPropertiesSet) { this.mShouldUpdateInnerSize = true; return false; } this.mShouldUpdateInnerSize = false; const result = super.updateInnerSize(); if (result) { this.updateSpanCount(); } // 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._isDataDirty || (this._lastLayoutKey && this._lastLayoutKey !== layoutKey)) { // setTimeout(() => this.refresh(false), 0); } this._lastLayoutKey = layoutKey; return result; } _onColWidthPropertyChanged(oldValue, newValue) { this.updateSpanCount(); super._onColWidthPropertyChanged(oldValue, newValue); } _onRowHeightPropertyChanged(oldValue, newValue) { this.updateSpanCount(); super._onRowHeightPropertyChanged(oldValue, newValue); } onLayout(left, top, right, bottom) { this._layedOut = true; // super.onLayout(left, top, right, bottom); 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); }); } onMeasure(widthMeasureSpec, heightMeasureSpec) { const lastLayoutKey = this._lastLayoutKey; this.updateInnerSize(); if (lastLayoutKey !== this._lastLayoutKey) { //we need to refresh visible cells so that they measure to the new size // for some reason it gets animated even with setSupportsChangeAnimations(false) // so we clear until it is done const nativeView = this.nativeViewProtected; const animator = nativeView.getItemAnimator(); nativeView.setItemAnimator(null); this.refreshVisibleItems(); setTimeout(() => { nativeView.setItemAnimator(animator); }, 0); } const p = CollectionViewBase.plugins[this.layoutStyle]; if (p && p.onMeasure) { p.onMeasure(this, widthMeasureSpec, heightMeasureSpec); } this.plugins.forEach((k) => { const p = CollectionViewBase.plugins[k]; p.onMeasure && p.onMeasure(this, widthMeasureSpec, heightMeasureSpec); }); } onSourceCollectionChanged(event) { if (!this._listViewAdapter || this._dataUpdatesSuspended) { return; } if (Trace.isEnabled()) { CLog(CLogTypes.log, 'onItemsChanged', event.action, event.index, event.addedCount, event.removed, event.removed && event.removed.length); } switch (event.action) { case ChangeType.Delete: { this._listViewAdapter.notifyItemRangeRemoved(event.index, event.removed.length); return; } case ChangeType.Add: { if (event.addedCount > 0) { this._listViewAdapter.notifyItemRangeInserted(event.index, event.addedCount); } // Reload the items to avoid duplicate Load on Demand indicators: return; } case ChangeType.Update: { if (event.addedCount > 0) { this._listViewAdapter.notifyItemRangeChanged(event.index, event.addedCount); } return; } case ChangeType.Splice: { const added = event.addedCount; const removed = (event.removed && event.removed.length) || 0; if (added > 0 && added === removed) { // notifyItemRangeChanged wont create a fade effect if (!this.animateItemUpdate) { this._listViewAdapter.notifyItemRangeChanged(event.index, added); } else { this._listViewAdapter.notifyItemRangeRemoved(event.index, added); this._listViewAdapter.notifyItemRangeInserted(event.index, added); } } else { if (!this.animateItemUpdate) { if (added > removed) { if (removed > 0) { this._listViewAdapter.notifyItemRangeChanged(event.index, removed); } this._listViewAdapter.notifyItemRangeInserted(event.index + removed, added - removed); } else { if (added > 0) { this._listViewAdapter.notifyItemRangeChanged(event.index, added); } this._listViewAdapter.notifyItemRangeRemoved(event.index + added, removed - added); } } else { if (event.removed && event.removed.length > 0) { this._listViewAdapter.notifyItemRangeRemoved(event.index, event.removed.length); } if (event.addedCount > 0) { this._listViewAdapter.notifyItemRangeInserted(event.index, event.addedCount); } } } return; } } this._listViewAdapter.notifyDataSetChanged(); } eachChild(callback) { // used for css updates (like theme change) this.enumerateViewHolders((v) => { const view = v.view; if (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); } } } }); } async eachChildAsync(callback) { return this.enumerateViewHoldersAsync(async (v) => { const view = v.view; if (view) { 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); } } } }); } refreshVisibleItems() { const view = this.nativeViewProtected; if (!view) { return; } const ids = Array.from(this.bindedViewHolders).sort((a, b) => a - b); this._listViewAdapter.notifyItemRangeChanged(ids[0], ids[ids.length - 1] - ids[0] + 1); } isItemAtIndexVisible(index) { const view = this.nativeViewProtected; if (!view) { return false; } const layoutManager = this.layoutManager; if (layoutManager['findFirstVisibleItemPosition']) { const first = layoutManager.findFirstVisibleItemPosition(); const last = layoutManager.findLastVisibleItemPosition(); return index >= first && index <= last; } return false; } findFirstVisibleItemIndex() { const view = this.nativeViewProtected; if (!view) { return -1; } const layoutManager = this.layoutManager; if (layoutManager['findFirstVisibleItemPosition']) { return layoutManager.findFirstVisibleItemPosition(); } return -1; } findLastVisibleItemIndex() { const view = this.nativeViewProtected; if (!view) { return -1; } const layoutManager = this.layoutManager; if (layoutManager['findLastVisibleItemPosition']) { return layoutManager.findLastVisibleItemPosition(); } return -1; } refresh(forceRefresh = false, updateSpanCountRequestsLayout = false) { if (this.mInPropertiesSet) { this.mShouldRefresh = true; return; } this.mShouldRefresh = false; const view = this.nativeViewProtected; if (!view) { return; } // seems like we refresh sooner // not sure why it was needed before and not now. if (!forceRefresh && (!this.isLoaded || !this.nativeView)) { this._isDataDirty = true; return; } this._isDataDirty = false; this._lastLayoutKey = this._innerWidth + '_' + this._innerHeight; let adapter = this._listViewAdapter; if (!adapter) { adapter = this._listViewAdapter = this.createComposedAdapter(view); adapter.setHasStableIds(!!this._itemIdGenerator); view.setAdapter(adapter); } else if (!view.getAdapter()) { view.setAdapter(adapter); } this.updateSpanCount(updateSpanCountRequestsLayout); adapter.notifyDataSetChanged(); this.notify({ eventName: CollectionViewBase.dataPopulatedEvent }); } //@ts-ignore get scrollOffset() { const view = this.nativeViewProtected; if (!view) { return 0; } return (this.isHorizontal() ? view.computeHorizontalScrollOffset() : view.computeVerticalScrollOffset()) / Utils.layout.getDisplayDensity(); } get verticalOffsetX() { const view = this.nativeViewProtected; if (!view) { return 0; } return view.computeHorizontalScrollOffset() / Utils.layout.getDisplayDensity(); } get verticalOffsetY() { const view = this.nativeViewProtected; if (!view) { return 0; } return view.computeVerticalScrollOffset() / Utils.layout.getDisplayDensity(); } scrollToIndex(index, animated = true, snap = SnapPosition.START) { const view = this.nativeViewProtected; if (!view) { return; } if (animated) { view.smoothScrollToPosition(index, snap); } else { view.scrollToPosition(index); } } scrollToOffset(offSetValue) { const view = this.nativeViewProtected; if (view && this.isScrollEnabled) { if (this.orientation === 'horizontal') { view.scrollBy(offSetValue, 0); } else { view.scrollBy(0, offSetValue); } } } _setPadding(newPadding) { const nativeView = this.nativeViewProtected; const padding = { top: nativeView.getPaddingTop(), right: nativeView.getPaddingRight(), bottom: nativeView.getPaddingBottom(), left: nativeView.getPaddingLeft(), }; // tslint:disable-next-line:prefer-object-spread const newValue = Object.assign(padding, newPadding); nativeView.setClipToPadding(false); nativeView.setPadding(newValue.left, newValue.top, newValue.right, newValue.bottom); this.updateInnerSize(); } createComposedAdapter(recyclerView) { const adapter = new com.nativescript.collectionview.Adapter(); adapter.adapterInterface = new com.nativescript.collectionview.AdapterInterface({ getItemId: this.getItemId.bind(this), getItemViewType: this.getItemViewType.bind(this), getItemCount: this.getItemCount.bind(this), onCreateViewHolder: this.onCreateViewHolder.bind(this), onBindViewHolder: this.onBindViewHolder.bind(this), onViewRecycled: this.onViewRecycled.bind(this), }); // const composedAdapter = new com.h6ah4i.android.widget.advrecyclerview.composedadapter.ComposedAdapter(); // composedAdapter.addAdapter(new CollectionViewAdapter(new WeakRef(this))); return adapter; } getItemCount() { return this.items ? this.items.length : 0; } getItem(i) { if (this.items && i < this.items.length) { return this.getItemAtIndex(i); } return null; } getItemId(i) { let id = -1; if (this._itemIdGenerator && this.items) { const item = this.getItemAtIndex(i); id = this._itemIdGenerator(item, i, this.items); } return long(id); } onItemIdGeneratorChanged(oldValue, newValue) { super.onItemIdGeneratorChanged(oldValue, newValue); if (this._listViewAdapter) { this._listViewAdapter.setHasStableIds(!!newValue); } } clearTemplateTypes() { this._currentNativeItemType = 0; this.templateTypeNumberString.clear(); this.templateStringTypeNumber.clear(); } getItemViewType(position) { let selectorType = 'default'; if (this._itemTemplateSelector) { const selector = this._itemTemplateSelector; const dataItem = this.getItemAtIndex(position); if (dataItem) { selectorType = selector.call(this, dataItem, position, this.items); } } return this.templateKeyToNativeItem(selectorType); // if (!this.templateTypeNumberString.has(selectorType)) { // resultType = this._currentNativeItemType; // this.templateTypeNumberString.set(selectorType, resultType); // this.templateStringTypeNumber.set(resultType, selectorType); // this._currentNativeItemType++; // } else { // resultType = this.templateTypeNumberString.get(selectorType); // } // return resultType; } templateKeyToNativeItem(key) { if (!this.templateTypeNumberString) { this.templateTypeNumberString = new Map(); this._currentNativeItemType = 0; this._itemTemplatesInternal.forEach((v, i) => { this.templateTypeNumberString.set(v.key, this._currentNativeItemType); this.templateStringTypeNumber.set(this._currentNativeItemType, v.key); this.setNativePoolSize(v.key, this._currentNativeItemType); this._currentNativeItemType++; }); this._currentNativeItemType = Math.max(this._itemTemplatesInternal.size, 100); // templates will be numbered 0,1,2,3... for named templates // default/unnamed templates will be numbered 100, 101, 102, 103... } if (!this.templateTypeNumberString.has(key)) { this.templateTypeNumberString.set(key, this._currentNativeItemType); this.templateStringTypeNumber.set(this._currentNativeItemType, key); this.setNativePoolSize(key, this._currentNativeItemType); this._currentNativeItemType++; } return this.templateTypeNumberString.get(key); } // public nativeItemToTemplateKey(item: number): string { // let result: string; // this.templateTypeNumberString?.forEach((value, key, map) => { // if (value === item) { // result = key; // } // }, this); // return result; // } disposeViewHolderViews() { this.enumerateViewHolders((v) => { const view = v.view; if (view) { this.notifyForItemAtIndex(CollectionViewBase.itemDisposingEvent, view, v.getAdapterPosition(), view.bindingContext, v); if (view.isLoaded) { view.callUnloaded(); } view._isAddedToNativeVisualTree = false; view._tearDownUI(); } v.view = null; v.clickListener = null; }); this._viewHolders = new Set(); } getKeyByValue(viewType) { return this.templateStringTypeNumber.get(viewType); } onCreateViewHolder(parent, viewType) { const start = Date.now(); let view = this.getViewForViewType(ListViewViewTypes.ItemView, this.getKeyByValue(viewType)); const isNonSync = view === undefined; // dont create unecessary StackLayout if template.createView returns. Will happend when not using Vue or angular if (isNonSync || view instanceof ProxyViewContainer) { const parentView = new ContentView(); parentView.id = 'collectionViewHolder'; view = parentView; } //@ts-ignore view.parent = this; view._setupAsRootView(this._context); view._isAddedToNativeVisualTree = true; view.callLoaded(); if (!CollectionViewCellHolder) { CollectionViewCellHolder = com.nativescript.collectionview.CollectionViewCellHolder; } const holder = new CollectionViewCellHolder(view.nativeView); const collectionView = this; const clickListener = new android.view.View.OnClickListener({ onClick: () => { const position = holder.getAdapterPosition(); collectionView.notify({ eventName: CollectionViewBase.itemTapEvent, object: collectionView, index: position, item: collectionView.getItem(position), view: holder.view, }); }, }); view.nativeView.setOnClickListener(clickListener); holder.clickListener = clickListener; holder.view = view; const layoutParams = this._getViewLayoutParams(); view.nativeView.setLayoutParams(layoutParams); if (isNonSync) { holder['defaultItemView'] = true; } this._viewHolders.add(holder); if (Trace.isEnabled()) { CLog(CLogTypes.log, 'onCreateViewHolder', this, this.nativeView, viewType, this.getKeyByValue(viewType), holder, Date.now() - start, 'ms'); } return holder; } notifyForItemAtIndex(eventName, view, index, bindingContext, native) { const args = { eventName, object: this, index, view, ios: native, bindingContext }; this.notify(args); return args; } onBindViewHolder(holder, position) { const start = Date.now(); if (Trace.isEnabled()) { CLog(CLogTypes.log, 'onBindViewHolder', this, this.nativeView, position, holder, holder.view); } let view = holder.view; const isNonSync = holder['defaultItemView'] === true; view = isNonSync ? view.content : view; const bindingContext = this._prepareItem(view, position); if (holder['position'] !== undefined) { this.bindedViewHolders.delete(holder['position']); } holder['position'] = position; this.bindedViewHolders.add(holder['position']); const args = this.notifyForItemAtIndex(CollectionViewBase.itemLoadingEvent, view, position, bindingContext, holder); if (isNonSync && args.view !== view) { view = args.view; // the view has been changed on the event handler holder.view.content = args.view; } view.bindingContext = bindingContext; view.n