Skip to content

Commit 71246a7

Browse files
committed
Add onStartReached support for VirtualizedList
1 parent 805f28a commit 71246a7

File tree

1 file changed

+96
-21
lines changed
  • packages/react-native-web/src/vendor/react-native/VirtualizedList

1 file changed

+96
-21
lines changed

packages/react-native-web/src/vendor/react-native/VirtualizedList/index.js

Lines changed: 96 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -212,15 +212,27 @@ type OptionalProps = {|
212212
* interfere with responding to button taps or other interactions.
213213
*/
214214
maxToRenderPerBatch?: ?number,
215+
/**
216+
* Called once when the scroll position gets within `onStartReachedThreshold` of the rendered
217+
* content.
218+
*/
219+
onStartReached?: ?(info: {distanceFromStart: number, ...}) => void,
220+
/**
221+
* How far from the start (in units of visible length of the list) the leading edge of the
222+
* list must be from the start of the content to trigger the `onStartReached` callback.
223+
* Thus, a value of 0.5 will trigger `onStartReached` when the start of the content is
224+
* within half the visible length of the list.
225+
*/
226+
onStartReachedThreshold?: ?number,
215227
/**
216228
* Called once when the scroll position gets within `onEndReachedThreshold` of the rendered
217229
* content.
218230
*/
219231
onEndReached?: ?(info: {distanceFromEnd: number, ...}) => void,
220232
/**
221-
* How far from the end (in units of visible length of the list) the bottom edge of the
233+
* How far from the end (in units of visible length of the list) the trailing edge of the
222234
* list must be from the end of the content to trigger the `onEndReached` callback.
223-
* Thus a value of 0.5 will trigger `onEndReached` when the end of the content is
235+
* Thus, a value of 0.5 will trigger `onEndReached` when the end of the content is
224236
* within half the visible length of the list.
225237
*/
226238
onEndReachedThreshold?: ?number,
@@ -336,11 +348,21 @@ function maxToRenderPerBatchOrDefault(maxToRenderPerBatch: ?number) {
336348
return maxToRenderPerBatch ?? 10;
337349
}
338350

351+
// onStartReachedThresholdOrDefault(this.props.onStartReachedThreshold)
352+
function onStartReachedThresholdOrDefault(onStartReachedThreshold: ?number) {
353+
return onStartReachedThreshold ?? 2;
354+
}
355+
339356
// onEndReachedThresholdOrDefault(this.props.onEndReachedThreshold)
340357
function onEndReachedThresholdOrDefault(onEndReachedThreshold: ?number) {
341358
return onEndReachedThreshold ?? 2;
342359
}
343360

361+
// getScrollingThreshold(visibleLength, onEndReachedThreshold)
362+
function getScrollingThreshold(threshold: number, visibleLength: number) {
363+
return (threshold * visibleLength) / 2;
364+
}
365+
344366
// scrollEventThrottleOrDefault(this.props.scrollEventThrottle)
345367
function scrollEventThrottleOrDefault(scrollEventThrottle: ?number) {
346368
return scrollEventThrottle ?? 50;
@@ -1269,6 +1291,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
12691291
visibleLength: 0,
12701292
};
12711293
_scrollRef: ?React.ElementRef<any> = null;
1294+
_sentStartForContentLength = 0;
12721295
_sentEndForContentLength = 0;
12731296
_totalCellLength = 0;
12741297
_totalCellsMeasured = 0;
@@ -1464,7 +1487,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
14641487
}
14651488
this.props.onLayout && this.props.onLayout(e);
14661489
this._scheduleCellsToRenderUpdate();
1467-
this._maybeCallOnEndReached();
1490+
this._maybeCallOnEdgeReached();
14681491
};
14691492

14701493
_onLayoutEmpty = e => {
@@ -1566,26 +1589,69 @@ class VirtualizedList extends React.PureComponent<Props, State> {
15661589
return !horizontalOrDefault(this.props.horizontal) ? metrics.y : metrics.x;
15671590
}
15681591

1569-
_maybeCallOnEndReached() {
1570-
const {data, getItemCount, onEndReached, onEndReachedThreshold} =
1571-
this.props;
1592+
_maybeCallOnEdgeReached() {
1593+
const {
1594+
data,
1595+
getItemCount,
1596+
onStartReached,
1597+
onStartReachedThreshold,
1598+
onEndReached,
1599+
onEndReachedThreshold,
1600+
initialScrollIndex,
1601+
} = this.props;
15721602
const {contentLength, visibleLength, offset} = this._scrollMetrics;
1603+
const distanceFromStart = offset;
15731604
const distanceFromEnd = contentLength - visibleLength - offset;
1574-
const threshold =
1575-
onEndReachedThreshold != null ? onEndReachedThreshold * visibleLength : 2;
1605+
const startThreshold =
1606+
onStartReachedThresholdOrDefault(onStartReachedThreshold) * visibleLength;
1607+
const endThreshold =
1608+
onEndReachedThresholdOrDefault(onEndReachedThreshold) * visibleLength;
1609+
const isWithinStartThreshold = distanceFromStart <= startThreshold;
1610+
const isWithinEndThreshold = distanceFromEnd <= endThreshold;
1611+
const shouldExecuteNewCallback =
1612+
this._scrollMetrics.contentLength !== this._sentStartForContentLength &&
1613+
this._scrollMetrics.contentLength !== this._sentEndForContentLength;
1614+
1615+
// First check if the user just scrolled within the end threshold
1616+
// and call onEndReached only once for a given content length,
1617+
// and only if onStartReached is not being executed
15761618
if (
15771619
onEndReached &&
1578-
this.state.last === getItemCount(data) - 1 &&
1579-
distanceFromEnd < threshold &&
1580-
this._scrollMetrics.contentLength !== this._sentEndForContentLength
1620+
isWithinEndThreshold &&
1621+
shouldExecuteNewCallback &&
1622+
this.state.last === getItemCount(data) - 1
15811623
) {
1582-
// Only call onEndReached once for a given content length
15831624
this._sentEndForContentLength = this._scrollMetrics.contentLength;
15841625
onEndReached({distanceFromEnd});
1585-
} else if (distanceFromEnd > threshold) {
1586-
// If the user scrolls away from the end and back again cause
1587-
// an onEndReached to be triggered again
1588-
this._sentEndForContentLength = 0;
1626+
}
1627+
1628+
// Next check if the user just scrolled within the start threshold
1629+
// and call onStartReached only once for a given content length,
1630+
// and only if onEndReached is not being executed
1631+
else if (
1632+
onStartReached &&
1633+
isWithinStartThreshold &&
1634+
shouldExecuteNewCallback &&
1635+
this.state.first === 0 &&
1636+
// On initial mount when using initialScrollIndex the offset will be 0 initially
1637+
// and will trigger an unexpected onStartReached. To avoid this we can use
1638+
// timestamp to differentiate between the initial scroll metrics and when we actually
1639+
// received the first scroll event.
1640+
(!initialScrollIndex || this._scrollMetrics.timestamp !== 0)
1641+
) {
1642+
this._sentStartForContentLength = this._scrollMetrics.contentLength;
1643+
onStartReached({distanceFromStart});
1644+
}
1645+
1646+
// If the user scrolls away from the start or end and back again,
1647+
// cause onStartReached or onEndReached to be triggered again
1648+
else {
1649+
this._sentStartForContentLength = isWithinStartThreshold
1650+
? this._sentStartForContentLength
1651+
: 0;
1652+
this._sentEndForContentLength = isWithinEndThreshold
1653+
? this._sentEndForContentLength
1654+
: 0;
15891655
}
15901656
}
15911657

@@ -1610,7 +1676,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
16101676
}
16111677
this._scrollMetrics.contentLength = this._selectLength({height, width});
16121678
this._scheduleCellsToRenderUpdate();
1613-
this._maybeCallOnEndReached();
1679+
this._maybeCallOnEdgeReached();
16141680
};
16151681

16161682
/* Translates metrics from a scroll event in a parent VirtualizedList into
@@ -1694,7 +1760,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
16941760
if (!this.props) {
16951761
return;
16961762
}
1697-
this._maybeCallOnEndReached();
1763+
this._maybeCallOnEdgeReached();
16981764
if (velocity !== 0) {
16991765
this._fillRateHelper.activate();
17001766
}
@@ -1707,16 +1773,23 @@ class VirtualizedList extends React.PureComponent<Props, State> {
17071773
const {offset, visibleLength, velocity} = this._scrollMetrics;
17081774
const itemCount = this.props.getItemCount(this.props.data);
17091775
let hiPri = false;
1776+
const onStartReachedThreshold = onStartReachedThresholdOrDefault(
1777+
this.props.onStartReachedThreshold,
1778+
);
17101779
const onEndReachedThreshold = onEndReachedThresholdOrDefault(
17111780
this.props.onEndReachedThreshold,
17121781
);
17131782
const scrollingThreshold = (onEndReachedThreshold * visibleLength) / 2;
17141783
// Mark as high priority if we're close to the start of the first item
17151784
// But only if there are items before the first rendered item
17161785
if (first > 0) {
1717-
const distTop = offset - this._getFrameMetricsApprox(first).offset;
1786+
const distStart = offset - this.__getFrameMetricsApprox(first).offset;
17181787
hiPri =
1719-
hiPri || distTop < 0 || (velocity < -2 && distTop < scrollingThreshold);
1788+
hiPri ||
1789+
distStart < 0 ||
1790+
(velocity < -2 &&
1791+
distStart <
1792+
getScrollingThreshold(onStartReachedThreshold, visibleLength));
17201793
}
17211794
// Mark as high priority if we're close to the end of the last item
17221795
// But only if there are items after the last rendered item
@@ -1726,7 +1799,9 @@ class VirtualizedList extends React.PureComponent<Props, State> {
17261799
hiPri =
17271800
hiPri ||
17281801
distBottom < 0 ||
1729-
(velocity > 2 && distBottom < scrollingThreshold);
1802+
(velocity > 2 &&
1803+
distBottom <
1804+
getScrollingThreshold(onEndReachedThreshold, visibleLength));
17301805
}
17311806
// Only trigger high-priority updates if we've actually rendered cells,
17321807
// and with that size estimate, accurately compute how many cells we should render.

0 commit comments

Comments
 (0)