recyclerlistview
Version:
The listview that you need and deserve. It was built for performance, uses cell recycling to achieve smooth scrolling.
347 lines • 16.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var RecycleItemPool_1 = require("../utils/RecycleItemPool");
var CustomError_1 = require("./exceptions/CustomError");
var RecyclerListViewExceptions_1 = require("./exceptions/RecyclerListViewExceptions");
var ViewabilityTracker_1 = require("./ViewabilityTracker");
var ts_object_utils_1 = require("ts-object-utils");
var TSCast_1 = require("../utils/TSCast");
var VirtualRenderer = /** @class */ (function () {
function VirtualRenderer(renderStackChanged, scrollOnNextUpdate, fetchStableId, isRecyclingEnabled) {
var _this = this;
this._layoutProvider = TSCast_1.default.cast(null); //TSI
this._recyclePool = TSCast_1.default.cast(null); //TSI
this._layoutManager = null;
this._viewabilityTracker = null;
this._optimizeForAnimations = false;
this._onVisibleItemsChanged = function (all, now, notNow) {
if (_this.onVisibleItemsChanged) {
_this.onVisibleItemsChanged(all, now, notNow);
}
};
this._onEngagedItemsChanged = function (all, now, notNow) {
var count = notNow.length;
var resolvedKey;
var disengagedIndex = 0;
if (_this._isRecyclingEnabled) {
for (var i = 0; i < count; i++) {
disengagedIndex = notNow[i];
delete _this._engagedIndexes[disengagedIndex];
if (_this._params && disengagedIndex < _this._params.itemCount) {
//All the items which are now not visible can go to the recycle pool, the pool only needs to maintain keys since
//react can link a view to a key automatically
resolvedKey = _this._stableIdToRenderKeyMap[_this._fetchStableId(disengagedIndex)];
if (!ts_object_utils_1.ObjectUtil.isNullOrUndefined(resolvedKey)) {
_this._recyclePool.putRecycledObject(_this._layoutProvider.getLayoutTypeForIndex(disengagedIndex), resolvedKey.key);
}
}
}
}
if (_this._updateRenderStack(now)) {
//Ask Recycler View to update itself
_this._renderStackChanged(_this._renderStack);
}
};
//Keeps track of items that need to be rendered in the next render cycle
this._renderStack = {};
this._fetchStableId = fetchStableId;
//Keeps track of keys of all the currently rendered indexes, can eventually replace renderStack as well if no new use cases come up
this._stableIdToRenderKeyMap = {};
this._engagedIndexes = {};
this._renderStackChanged = renderStackChanged;
this._scrollOnNextUpdate = scrollOnNextUpdate;
this._dimensions = null;
this._params = null;
this._isRecyclingEnabled = isRecyclingEnabled;
this._isViewTrackerRunning = false;
this._markDirty = false;
//Would be surprised if someone exceeds this
this._startKey = 0;
this.onVisibleItemsChanged = null;
}
VirtualRenderer.prototype.getLayoutDimension = function () {
if (this._layoutManager) {
return this._layoutManager.getContentDimension();
}
return { height: 0, width: 0 };
};
VirtualRenderer.prototype.setOptimizeForAnimations = function (shouldOptimize) {
this._optimizeForAnimations = shouldOptimize;
};
VirtualRenderer.prototype.hasPendingAnimationOptimization = function () {
return this._optimizeForAnimations;
};
VirtualRenderer.prototype.updateOffset = function (offsetX, offsetY, isActual, correction) {
if (this._viewabilityTracker) {
var offset = this._params && this._params.isHorizontal ? offsetX : offsetY;
if (!this._isViewTrackerRunning) {
if (isActual) {
this._viewabilityTracker.setActualOffset(offset);
}
this.startViewabilityTracker(correction);
}
this._viewabilityTracker.updateOffset(offset, isActual, correction);
}
};
VirtualRenderer.prototype.attachVisibleItemsListener = function (callback) {
this.onVisibleItemsChanged = callback;
};
VirtualRenderer.prototype.removeVisibleItemsListener = function () {
this.onVisibleItemsChanged = null;
if (this._viewabilityTracker) {
this._viewabilityTracker.onVisibleRowsChanged = null;
}
};
VirtualRenderer.prototype.getLayoutManager = function () {
return this._layoutManager;
};
VirtualRenderer.prototype.setParamsAndDimensions = function (params, dim) {
this._params = params;
this._dimensions = dim;
};
VirtualRenderer.prototype.setLayoutManager = function (layoutManager) {
this._layoutManager = layoutManager;
if (this._params) {
this._layoutManager.relayoutFromIndex(0, this._params.itemCount);
}
};
VirtualRenderer.prototype.setLayoutProvider = function (layoutProvider) {
this._layoutProvider = layoutProvider;
};
VirtualRenderer.prototype.getViewabilityTracker = function () {
return this._viewabilityTracker;
};
VirtualRenderer.prototype.refreshWithAnchor = function () {
if (this._viewabilityTracker) {
var firstVisibleIndex = this._viewabilityTracker.findFirstLogicallyVisibleIndex();
this._prepareViewabilityTracker();
var offset = 0;
if (this._layoutManager && this._params) {
firstVisibleIndex = Math.min(this._params.itemCount - 1, firstVisibleIndex);
var point = this._layoutManager.getOffsetForIndex(firstVisibleIndex);
this._scrollOnNextUpdate(point);
offset = this._params.isHorizontal ? point.x : point.y;
}
this._viewabilityTracker.forceRefreshWithOffset(offset);
}
};
VirtualRenderer.prototype.refresh = function () {
if (this._viewabilityTracker) {
this._prepareViewabilityTracker();
this._viewabilityTracker.forceRefresh();
}
};
VirtualRenderer.prototype.getInitialOffset = function () {
var offset = { x: 0, y: 0 };
if (this._params) {
var initialRenderIndex = ts_object_utils_1.Default.value(this._params.initialRenderIndex, 0);
if (initialRenderIndex > 0 && this._layoutManager) {
offset = this._layoutManager.getOffsetForIndex(initialRenderIndex);
this._params.initialOffset = this._params.isHorizontal ? offset.x : offset.y;
}
else {
if (this._params.isHorizontal) {
offset.x = ts_object_utils_1.Default.value(this._params.initialOffset, 0);
offset.y = 0;
}
else {
offset.y = ts_object_utils_1.Default.value(this._params.initialOffset, 0);
offset.x = 0;
}
}
}
return offset;
};
VirtualRenderer.prototype.init = function () {
this.getInitialOffset();
this._recyclePool = new RecycleItemPool_1.default();
if (this._params) {
this._viewabilityTracker = new ViewabilityTracker_1.default(ts_object_utils_1.Default.value(this._params.renderAheadOffset, 0), ts_object_utils_1.Default.value(this._params.initialOffset, 0));
}
else {
this._viewabilityTracker = new ViewabilityTracker_1.default(0, 0);
}
this._prepareViewabilityTracker();
};
VirtualRenderer.prototype.startViewabilityTracker = function (windowCorrection) {
if (this._viewabilityTracker) {
this._isViewTrackerRunning = true;
this._viewabilityTracker.init(windowCorrection);
}
};
VirtualRenderer.prototype.syncAndGetKey = function (index, overrideStableIdProvider, newRenderStack, keyToStableIdMap) {
var getStableId = overrideStableIdProvider ? overrideStableIdProvider : this._fetchStableId;
var renderStack = newRenderStack ? newRenderStack : this._renderStack;
var stableIdItem = this._stableIdToRenderKeyMap[getStableId(index)];
var key = stableIdItem ? stableIdItem.key : undefined;
if (ts_object_utils_1.ObjectUtil.isNullOrUndefined(key)) {
var type = this._layoutProvider.getLayoutTypeForIndex(index);
key = this._recyclePool.getRecycledObject(type);
if (!ts_object_utils_1.ObjectUtil.isNullOrUndefined(key)) {
var itemMeta = renderStack[key];
if (itemMeta) {
var oldIndex = itemMeta.dataIndex;
itemMeta.dataIndex = index;
if (!ts_object_utils_1.ObjectUtil.isNullOrUndefined(oldIndex) && oldIndex !== index) {
delete this._stableIdToRenderKeyMap[getStableId(oldIndex)];
}
}
else {
renderStack[key] = { dataIndex: index };
if (keyToStableIdMap && keyToStableIdMap[key]) {
delete this._stableIdToRenderKeyMap[keyToStableIdMap[key]];
}
}
}
else {
key = getStableId(index);
if (renderStack[key]) {
//Probable collision, warn and avoid
//TODO: Disabled incorrectly triggering in some cases
//console.warn("Possible stableId collision @", index); //tslint:disable-line
key = this._getCollisionAvoidingKey();
}
renderStack[key] = { dataIndex: index };
}
this._markDirty = true;
this._stableIdToRenderKeyMap[getStableId(index)] = { key: key, type: type };
}
if (!ts_object_utils_1.ObjectUtil.isNullOrUndefined(this._engagedIndexes[index])) {
this._recyclePool.removeFromPool(key);
}
var stackItem = renderStack[key];
if (stackItem && stackItem.dataIndex !== index) {
//Probable collision, warn
console.warn("Possible stableId collision @", index); //tslint:disable-line
}
return key;
};
//Further optimize in later revision, pretty fast for now considering this is a low frequency event
VirtualRenderer.prototype.handleDataSetChange = function (newDataProvider) {
var _this = this;
var getStableId = newDataProvider.getStableId;
var maxIndex = newDataProvider.getSize() - 1;
var activeStableIds = {};
var newRenderStack = {};
var keyToStableIdMap = {};
// Do not use recycle pool so that elements don't fly top to bottom or vice versa
// Doing this is expensive and can draw extra items
if (this._optimizeForAnimations && this._recyclePool) {
this._recyclePool.clearAll();
}
//Compute active stable ids and stale active keys and resync render stack
for (var key in this._renderStack) {
if (this._renderStack.hasOwnProperty(key)) {
var index = this._renderStack[key].dataIndex;
if (!ts_object_utils_1.ObjectUtil.isNullOrUndefined(index)) {
if (index <= maxIndex) {
var stableId = getStableId(index);
activeStableIds[stableId] = 1;
}
}
}
}
//Clean stable id to key map
var oldActiveStableIds = Object.keys(this._stableIdToRenderKeyMap);
var oldActiveStableIdsCount = oldActiveStableIds.length;
for (var i = 0; i < oldActiveStableIdsCount; i++) {
var key = oldActiveStableIds[i];
var stableIdItem = this._stableIdToRenderKeyMap[key];
if (stableIdItem) {
if (!activeStableIds[key]) {
if (!this._optimizeForAnimations && this._isRecyclingEnabled) {
this._recyclePool.putRecycledObject(stableIdItem.type, stableIdItem.key);
}
delete this._stableIdToRenderKeyMap[key];
var stackItem = this._renderStack[stableIdItem.key];
var dataIndex = stackItem ? stackItem.dataIndex : undefined;
if (!ts_object_utils_1.ObjectUtil.isNullOrUndefined(dataIndex) && dataIndex <= maxIndex && this._layoutManager) {
this._layoutManager.removeLayout(dataIndex);
}
}
else {
keyToStableIdMap[stableIdItem.key] = key;
}
}
}
var renderStackKeys = Object.keys(this._renderStack).sort(function (a, b) {
var firstItem = _this._renderStack[a];
var secondItem = _this._renderStack[b];
if (firstItem && firstItem.dataIndex && secondItem && secondItem.dataIndex) {
return firstItem.dataIndex - secondItem.dataIndex;
}
return 1;
});
var renderStackLength = renderStackKeys.length;
for (var i = 0; i < renderStackLength; i++) {
var key = renderStackKeys[i];
var index = this._renderStack[key].dataIndex;
if (!ts_object_utils_1.ObjectUtil.isNullOrUndefined(index)) {
if (index <= maxIndex) {
var newKey = this.syncAndGetKey(index, getStableId, newRenderStack, keyToStableIdMap);
var newStackItem = newRenderStack[newKey];
if (!newStackItem) {
newRenderStack[newKey] = { dataIndex: index };
}
else if (newStackItem.dataIndex !== index) {
var cllKey = this._getCollisionAvoidingKey();
newRenderStack[cllKey] = { dataIndex: index };
this._stableIdToRenderKeyMap[getStableId(index)] = {
key: cllKey, type: this._layoutProvider.getLayoutTypeForIndex(index),
};
}
}
}
delete this._renderStack[key];
}
Object.assign(this._renderStack, newRenderStack);
for (var key in this._renderStack) {
if (this._renderStack.hasOwnProperty(key)) {
var index = this._renderStack[key].dataIndex;
if (!ts_object_utils_1.ObjectUtil.isNullOrUndefined(index) && ts_object_utils_1.ObjectUtil.isNullOrUndefined(this._engagedIndexes[index])) {
var type = this._layoutProvider.getLayoutTypeForIndex(index);
this._recyclePool.putRecycledObject(type, key);
}
}
}
};
VirtualRenderer.prototype._getCollisionAvoidingKey = function () {
return "#" + this._startKey++ + "_rlv_c";
};
VirtualRenderer.prototype._prepareViewabilityTracker = function () {
if (this._viewabilityTracker && this._layoutManager && this._dimensions && this._params) {
this._viewabilityTracker.onEngagedRowsChanged = this._onEngagedItemsChanged;
if (this.onVisibleItemsChanged) {
this._viewabilityTracker.onVisibleRowsChanged = this._onVisibleItemsChanged;
}
this._viewabilityTracker.setLayouts(this._layoutManager.getLayouts(), this._params.isHorizontal ?
this._layoutManager.getContentDimension().width :
this._layoutManager.getContentDimension().height);
this._viewabilityTracker.setDimensions({
height: this._dimensions.height,
width: this._dimensions.width,
}, ts_object_utils_1.Default.value(this._params.isHorizontal, false));
}
else {
throw new CustomError_1.default(RecyclerListViewExceptions_1.default.initializationException);
}
};
//Updates render stack and reports whether anything has changed
VirtualRenderer.prototype._updateRenderStack = function (itemIndexes) {
this._markDirty = false;
var count = itemIndexes.length;
var index = 0;
var hasRenderStackChanged = false;
for (var i = 0; i < count; i++) {
index = itemIndexes[i];
this._engagedIndexes[index] = 1;
this.syncAndGetKey(index);
hasRenderStackChanged = this._markDirty;
}
this._markDirty = false;
return hasRenderStackChanged;
};
return VirtualRenderer;
}());
exports.default = VirtualRenderer;
//# sourceMappingURL=VirtualRenderer.js.map