Skip to content

Commit

Permalink
Remove the initial empty render of the scrollview if a layoutSize is …
Browse files Browse the repository at this point in the history
…passed (#450)

* Adding a layout size prop to take initial size of RLV and use this render to remove initial shift by making opacity 0

* Removing visibility related changes

* Moving componentDidMount logic into constructor

* Renaming layoutSize to scrollViewSize prop

* Fixing initial offset

* Handling the case when initial offset is provided and preventing force render when the onLayout is called for the first time

* Version bump

* Version bump to beta before publishing

* Moved willMountCompat logic to constructor and removed acceptableLayoutDelta check from gridlayoutmanager

* Undo acceptable relayout delta changes

* Renaming getContextFromProvider method

* version bump

* fix: removed extra changes

* Renaming prop to scrollViewSize and using cachedLayouts in forceRender scenario

* Renaming prop to layoutSize and moving forceFullRender to separate block

Co-authored-by: Talha Naqvi <naqvitalha@gmail.com>
  • Loading branch information
arunreddy10 and naqvitalha authored May 4, 2020
1 parent b7c23d2 commit e12e568
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 60 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ not be as fast.
| optimizeForInsertDeleteAnimations | No | boolean | Enables you to utilize layout animations better by unmounting removed items |
| style | No | object | To pass down style to inner ScrollView |
| scrollViewProps | No | object | For all props that need to be proxied to inner/external scrollview. Put them in an object and they'll be spread and passed down. |
| layoutSize | No | Dimension | Will prevent the initial empty render required to compute the size of the listview and use these dimensions to render list items in the first render itself. This is useful for cases such as server side rendering. The prop canChangeSize has to be set to true if the size can be changed after rendering. Note that this is not the scroll view size and is used solely for layouting. |

For full feature set have a look at prop definitions of [RecyclerListView](https://github.com/Flipkart/recyclerlistview/blob/21049cc89ad606ec9fe8ea045dc73732ff29eac9/src/core/RecyclerListView.tsx#L540-L634)
(bottom of the file). All `ScrollView` features like `RefreshControl` also work out of the box.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "recyclerlistview",
"version": "3.0.0",
"version": "3.0.1",
"description": "The listview that you need and deserve. It was built for performance, uses cell recycling to achieve smooth scrolling.",
"main": "dist/reactnative/index.js",
"types": "dist/reactnative/index.d.ts",
Expand Down
168 changes: 109 additions & 59 deletions src/core/RecyclerListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export interface RecyclerListViewProps {
onVisibleIndicesChanged?: TOnItemStatusChanged;
renderFooter?: () => JSX.Element | JSX.Element[] | null;
externalScrollView?: { new(props: ScrollViewDefaultProps): BaseScrollView };
layoutSize?: Dimension;
initialOffset?: number;
initialRenderIndex?: number;
scrollThrottle?: number;
Expand Down Expand Up @@ -164,14 +165,21 @@ export default class RecyclerListView<P extends RecyclerListViewProps, S extends
return this.props.dataProvider.getStableId(index);
}, !props.disableRecycling);

this.state = {
internalSnapshot: {},
renderStack: {},
} as S;

this._windowCorrection = {
startCorrection: 0, endCorrection: 0, windowShift: 0,
};
this._getContextFromContextProvider(props);
if (props.layoutSize) {
this._layout.height = props.layoutSize.height;
this._layout.width = props.layoutSize.width;
this._initComplete = true;
this._initTrackers(props);
} else {
this.state = {
internalSnapshot: {},
renderStack: {},
} as S;
}
}

public componentWillReceivePropsCompat(newProps: RecyclerListViewProps): void {
Expand All @@ -189,25 +197,21 @@ export default class RecyclerListView<P extends RecyclerListViewProps, S extends
}

public componentDidUpdate(): void {
if (this._pendingScrollToOffset) {
const offset = this._pendingScrollToOffset;
this._pendingScrollToOffset = null;
if (this.props.isHorizontal) {
offset.y = 0;
} else {
offset.x = 0;
}
setTimeout(() => {
this.scrollToOffset(offset.x, offset.y, false);
}, 0);
}
this._processInitialOffset();
this._processOnEndReached();
this._checkAndChangeLayouts(this.props);
if (this.props.dataProvider.getSize() === 0) {
console.warn(Messages.WARN_NO_DATA); //tslint:disable-line
}
}

public componentDidMount(): void {
if (this._initComplete) {
this._processInitialOffset();
this._processOnEndReached();
}
}

public componentWillUnmount(): void {
if (this.props.contextProvider) {
const uniqueKey = this.props.contextProvider.getUniqueKey();
Expand All @@ -227,29 +231,6 @@ export default class RecyclerListView<P extends RecyclerListViewProps, S extends
}
}

public componentWillMountCompat(): void {
if (this.props.contextProvider) {
const uniqueKey = this.props.contextProvider.getUniqueKey();
if (uniqueKey) {
const offset = this.props.contextProvider.get(uniqueKey + Constants.CONTEXT_PROVIDER_OFFSET_KEY_SUFFIX);
if (typeof offset === "number" && offset > 0) {
this._initialOffset = offset;
if (this.props.onRecreate) {
this.props.onRecreate({ lastOffset: this._initialOffset });
}
this.props.contextProvider.remove(uniqueKey + Constants.CONTEXT_PROVIDER_OFFSET_KEY_SUFFIX);
}
if (this.props.forceNonDeterministicRendering) {
const cachedLayouts = this.props.contextProvider.get(uniqueKey + Constants.CONTEXT_PROVIDER_LAYOUT_KEY_SUFFIX) as string;
if (cachedLayouts && typeof cachedLayouts === "string") {
this._cachedLayouts = JSON.parse(cachedLayouts).layoutArray;
this.props.contextProvider.remove(uniqueKey + Constants.CONTEXT_PROVIDER_LAYOUT_KEY_SUFFIX);
}
}
}
}
}

public scrollToIndex(index: number, animate?: boolean): void {
const layoutManager = this._virtualRenderer.getLayoutManager();
if (layoutManager) {
Expand Down Expand Up @@ -381,6 +362,44 @@ export default class RecyclerListView<P extends RecyclerListViewProps, S extends
return this._virtualRenderer;
}

private _processInitialOffset(): void {
if (this._pendingScrollToOffset) {
const offset = this._pendingScrollToOffset;
this._pendingScrollToOffset = null;
if (this.props.isHorizontal) {
offset.y = 0;
} else {
offset.x = 0;
}
setTimeout(() => {
this.scrollToOffset(offset.x, offset.y, false);
}, 0);
}
}

private _getContextFromContextProvider(props: RecyclerListViewProps): void {
if (props.contextProvider) {
const uniqueKey = props.contextProvider.getUniqueKey();
if (uniqueKey) {
const offset = props.contextProvider.get(uniqueKey + Constants.CONTEXT_PROVIDER_OFFSET_KEY_SUFFIX);
if (typeof offset === "number" && offset > 0) {
this._initialOffset = offset;
if (props.onRecreate) {
props.onRecreate({ lastOffset: this._initialOffset });
}
props.contextProvider.remove(uniqueKey + Constants.CONTEXT_PROVIDER_OFFSET_KEY_SUFFIX);
}
if (props.forceNonDeterministicRendering) {
const cachedLayouts = props.contextProvider.get(uniqueKey + Constants.CONTEXT_PROVIDER_LAYOUT_KEY_SUFFIX) as string;
if (cachedLayouts && typeof cachedLayouts === "string") {
this._cachedLayouts = JSON.parse(cachedLayouts).layoutArray;
props.contextProvider.remove(uniqueKey + Constants.CONTEXT_PROVIDER_LAYOUT_KEY_SUFFIX);
}
}
}
}
}

private _checkAndChangeLayouts(newProps: RecyclerListViewProps, forceFullRender?: boolean): void {
this._params.isHorizontal = newProps.isHorizontal;
this._params.itemCount = newProps.dataProvider.getSize();
Expand All @@ -389,7 +408,7 @@ export default class RecyclerListView<P extends RecyclerListViewProps, S extends
if (newProps.dataProvider.hasStableIds() && this.props.dataProvider !== newProps.dataProvider && newProps.dataProvider.requiresDataChangeHandling()) {
this._virtualRenderer.handleDataSetChange(newProps.dataProvider, this.props.optimizeForInsertDeleteAnimations);
}
if (forceFullRender || this.props.layoutProvider !== newProps.layoutProvider || this.props.isHorizontal !== newProps.isHorizontal) {
if (this.props.layoutProvider !== newProps.layoutProvider || this.props.isHorizontal !== newProps.isHorizontal) {
//TODO:Talha use old layout manager
this._virtualRenderer.setLayoutManager(newProps.layoutProvider.newLayoutManager(this._layout, newProps.isHorizontal));
if (newProps.layoutProvider.shouldRefreshWithAnchoring) {
Expand All @@ -407,6 +426,13 @@ export default class RecyclerListView<P extends RecyclerListViewProps, S extends
layoutManager.relayoutFromIndex(newProps.dataProvider.getFirstIndexToProcessInternal(), newProps.dataProvider.getSize());
this._virtualRenderer.refresh();
}
} else if (forceFullRender) {
const layoutManager = this._virtualRenderer.getLayoutManager();
if (layoutManager) {
const cachedLayouts = layoutManager.getLayouts();
this._virtualRenderer.setLayoutManager(newProps.layoutProvider.newLayoutManager(this._layout, newProps.isHorizontal, cachedLayouts));
this._refreshViewability();
}
} else if (this._relayoutReqIndex >= 0) {
const layoutManager = this._virtualRenderer.getLayoutManager();
if (layoutManager) {
Expand All @@ -433,6 +459,9 @@ export default class RecyclerListView<P extends RecyclerListViewProps, S extends
}

private _onSizeChanged = (layout: Dimension): void => {
if (!this.props.canChangeSize && this.props.layoutSize) {
return;
}
const hasHeightChanged = this._layout.height !== layout.height;
const hasWidthChanged = this._layout.width !== layout.width;
this._layout.height = layout.height;
Expand All @@ -442,7 +471,7 @@ export default class RecyclerListView<P extends RecyclerListViewProps, S extends
}
if (!this._initComplete) {
this._initComplete = true;
this._initTrackers();
this._initTrackers(this.props);
this._processOnEndReached();
} else {
if ((hasHeightChanged && hasWidthChanged) ||
Expand All @@ -455,40 +484,55 @@ export default class RecyclerListView<P extends RecyclerListViewProps, S extends
}
}

private _initStateIfRequired(stack?: RenderStack): boolean {
if (!this.state) {
this.state = {
internalSnapshot: {},
renderStack: stack,
} as S;
return true;
}
return false;
}

private _renderStackWhenReady = (stack: RenderStack): void => {
this.setState(() => {
return { renderStack: stack };
});
if (!this._initStateIfRequired(stack)) {
this.setState(() => {
return { renderStack: stack };
});
}
}

private _initTrackers(): void {
this._assertDependencyPresence(this.props);
if (this.props.onVisibleIndexesChanged) {
private _initTrackers(props: RecyclerListViewProps): void {
this._assertDependencyPresence(props);
if (props.onVisibleIndexesChanged) {
throw new CustomError(RecyclerListViewExceptions.usingOldVisibleIndexesChangedParam);
}
if (this.props.onVisibleIndicesChanged) {
this._virtualRenderer.attachVisibleItemsListener(this.props.onVisibleIndicesChanged!);
if (props.onVisibleIndicesChanged) {
this._virtualRenderer.attachVisibleItemsListener(props.onVisibleIndicesChanged!);
}
this._params = {
initialOffset: this._initialOffset ? this._initialOffset : this.props.initialOffset,
initialRenderIndex: this.props.initialRenderIndex,
isHorizontal: this.props.isHorizontal,
itemCount: this.props.dataProvider.getSize(),
renderAheadOffset: this.props.renderAheadOffset,
initialOffset: this._initialOffset ? this._initialOffset : props.initialOffset,
initialRenderIndex: props.initialRenderIndex,
isHorizontal: props.isHorizontal,
itemCount: props.dataProvider.getSize(),
renderAheadOffset: props.renderAheadOffset,
};
this._virtualRenderer.setParamsAndDimensions(this._params, this._layout);
const layoutManager = this.props.layoutProvider.newLayoutManager(this._layout, this.props.isHorizontal, this._cachedLayouts);
const layoutManager = props.layoutProvider.newLayoutManager(this._layout, props.isHorizontal, this._cachedLayouts);
this._virtualRenderer.setLayoutManager(layoutManager);
this._virtualRenderer.setLayoutProvider(this.props.layoutProvider);
this._virtualRenderer.setLayoutProvider(props.layoutProvider);
this._virtualRenderer.init();
const offset = this._virtualRenderer.getInitialOffset();
const contentDimension = layoutManager.getContentDimension();
if ((offset.y > 0 && contentDimension.height > this._layout.height) ||
(offset.x > 0 && contentDimension.width > this._layout.width)) {
this._pendingScrollToOffset = offset;
this.setState({});
if (!this._initStateIfRequired()) {
this.setState({});
}
} else {
this._virtualRenderer.startViewabilityTracker(this._getWindowCorrection(offset.x, offset.y, this.props));
this._virtualRenderer.startViewabilityTracker(this._getWindowCorrection(offset.x, offset.y, props));
}
}

Expand Down Expand Up @@ -675,6 +719,12 @@ RecyclerListView.propTypes = {
//Specify the initial item index you want rendering to start from. Preferred over initialOffset if both are specified.
initialRenderIndex: PropTypes.number,

//Specify the estimated size of the recyclerlistview to render the list items in the first pass. If provided, recyclerlistview will
//use these dimensions to fill in the items in the first render. If not provided, recyclerlistview will first render with no items
//and then fill in the items based on the size given by its onLayout event. canChangeSize can be set to true to relayout items when
//the size changes.
layoutSize: PropTypes.object,

//iOS only. Scroll throttle duration.
scrollThrottle: PropTypes.number,

Expand Down

0 comments on commit e12e568

Please sign in to comment.