From 52e50af56d601e63a0c83118c194149d3aed38ea Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Wed, 3 May 2017 10:18:27 -0700 Subject: [PATCH] Fix and optimize VirtualizedList update triggers Summary: - If the initial render doesn't extend past `onEndReachedThreshold` it is likely that onEndReached won't get called until scroll, which can be a bad experience if the `initialNumToRender` is very close to the viewport height. This happens because when `onContentSizeChange`, `onLayout` may not have fired yet so we don't know what the `visibleLength` is. Fix is to also call `maybeCallOnEndReached` in `_onLayout` as well. - We have an optimization that does hi-pri render window updates when scrolling quickly and the content reaches the edge of the viewport, but there is also an important case where the user has scrolled to the end of the content and is waiting for a network response. Once the new data comes in, we want to render it ASAP because the user is waiting for it. To solve this we refactor our scheduling code into a shared function that always checks if it should be a hi-pri update instead of just in `_onScroll`. Reviewed By: bvaughn Differential Revision: D4975314 fbshipit-source-id: 8d64832ecbcbdbac430a08a4018d7a32b2216a85 --- Libraries/Lists/VirtualizedList.js | 61 +++++++++++++++++------------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/Libraries/Lists/VirtualizedList.js b/Libraries/Lists/VirtualizedList.js index d1310a54205..bd955f4b2a9 100644 --- a/Libraries/Lists/VirtualizedList.js +++ b/Libraries/Lists/VirtualizedList.js @@ -347,7 +347,6 @@ class VirtualizedList extends React.PureComponent { if (data !== this.props.data || extraData !== this.props.extraData) { this._hasDataChangedSinceEndReached = true; } - this._updateCellsToRenderBatcher.schedule(); } _pushCells( @@ -513,7 +512,7 @@ class VirtualizedList extends React.PureComponent { } componentDidUpdate() { - this._updateCellsToRenderBatcher.schedule(); + this._scheduleCellsToRenderUpdate(); } _averageCellLength = 0; @@ -567,7 +566,7 @@ class VirtualizedList extends React.PureComponent { this._averageCellLength = this._totalCellLength / this._totalCellsMeasured; this._frames[cellKey] = next; this._highestMeasuredFrameIndex = Math.max(this._highestMeasuredFrameIndex, index); - this._updateCellsToRenderBatcher.schedule(); + this._scheduleCellsToRenderUpdate(); } else { this._frames[cellKey].inLayout = true; } @@ -584,7 +583,8 @@ class VirtualizedList extends React.PureComponent { _onLayout = (e: Object) => { this._scrollMetrics.visibleLength = this._selectLength(e.nativeEvent.layout); this.props.onLayout && this.props.onLayout(e); - this._updateCellsToRenderBatcher.schedule(); + this._scheduleCellsToRenderUpdate(); + this._maybeCallOnEndReached(); }; _onLayoutFooter = (e) => { @@ -671,7 +671,7 @@ class VirtualizedList extends React.PureComponent { this.props.onContentSizeChange(width, height); } this._scrollMetrics.contentLength = this._selectLength({height, width}); - this._updateCellsToRenderBatcher.schedule(); + this._scheduleCellsToRenderUpdate(); this._maybeCallOnEndReached(); }; @@ -697,37 +697,44 @@ class VirtualizedList extends React.PureComponent { const dOffset = offset - this._scrollMetrics.offset; const velocity = dOffset / dt; this._scrollMetrics = {contentLength, dt, dOffset, offset, timestamp, velocity, visibleLength}; - const {data, getItemCount, windowSize} = this.props; - - this._updateViewableItems(data); - if (!data) { + this._updateViewableItems(this.props); + if (!this.props) { return; } this._maybeCallOnEndReached(); - - const {first, last} = this.state; if (velocity !== 0) { this._fillRateHelper.activate(); } this._computeBlankness(); - const itemCount = getItemCount(data); - if ((first > 0 && velocity < 0) || (last < itemCount - 1 && velocity > 0)) { - const distanceToContentEdge = Math.min( - Math.abs(this._getFrameMetricsApprox(first).offset - offset), - Math.abs(this._getFrameMetricsApprox(last).offset - (offset + visibleLength)), - ); - const hiPri = distanceToContentEdge < (windowSize * visibleLength / 4); - if (hiPri) { - // Don't worry about interactions when scrolling quickly; focus on filling content as fast - // as possible. - this._updateCellsToRenderBatcher.dispose({abort: true}); - this._updateCellsToRender(); - return; - } - } - this._updateCellsToRenderBatcher.schedule(); + this._scheduleCellsToRenderUpdate(); }; + _scheduleCellsToRenderUpdate() { + const {first, last} = this.state; + const {offset, visibleLength, velocity} = this._scrollMetrics; + const itemCount = this.props.getItemCount(this.props.data); + let hiPri = false; + if (first > 0 || last < itemCount - 1) { + const distTop = offset - this._getFrameMetricsApprox(first).offset; + const distBottom = this._getFrameMetricsApprox(last).offset - (offset + visibleLength); + const scrollingThreshold = this.props.onEndReachedThreshold * visibleLength / 2; + hiPri = ( + Math.min(distTop, distBottom) < 0 || + (velocity < -2 && distTop < scrollingThreshold) || + (velocity > 2 && distBottom < scrollingThreshold) + ); + } + if (hiPri) { + // Don't worry about interactions when scrolling quickly; focus on filling content as fast + // as possible. + this._updateCellsToRenderBatcher.dispose({abort: true}); + this._updateCellsToRender(); + return; + } else { + this._updateCellsToRenderBatcher.schedule(); + } + } + _onScrollBeginDrag = (e): void => { this._viewabilityHelper.recordInteraction(); this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e);