@@ -212,15 +212,27 @@ type OptionalProps = {|
212
212
* interfere with responding to button taps or other interactions.
213
213
*/
214
214
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 ,
215
227
/**
216
228
* Called once when the scroll position gets within `onEndReachedThreshold` of the rendered
217
229
* content.
218
230
*/
219
231
onEndReached ?: ?( info : { distanceFromEnd : number , ...} ) => void ,
220
232
/**
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
222
234
* 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
224
236
* within half the visible length of the list.
225
237
*/
226
238
onEndReachedThreshold ?: ?number ,
@@ -336,11 +348,21 @@ function maxToRenderPerBatchOrDefault(maxToRenderPerBatch: ?number) {
336
348
return maxToRenderPerBatch ?? 10 ;
337
349
}
338
350
351
+ // onStartReachedThresholdOrDefault(this.props.onStartReachedThreshold)
352
+ function onStartReachedThresholdOrDefault ( onStartReachedThreshold : ?number ) {
353
+ return onStartReachedThreshold ?? 2 ;
354
+ }
355
+
339
356
// onEndReachedThresholdOrDefault(this.props.onEndReachedThreshold)
340
357
function onEndReachedThresholdOrDefault ( onEndReachedThreshold : ?number ) {
341
358
return onEndReachedThreshold ?? 2 ;
342
359
}
343
360
361
+ // getScrollingThreshold(visibleLength, onEndReachedThreshold)
362
+ function getScrollingThreshold ( threshold : number , visibleLength : number ) {
363
+ return ( threshold * visibleLength ) / 2 ;
364
+ }
365
+
344
366
// scrollEventThrottleOrDefault(this.props.scrollEventThrottle)
345
367
function scrollEventThrottleOrDefault ( scrollEventThrottle : ?number ) {
346
368
return scrollEventThrottle ?? 50 ;
@@ -1269,6 +1291,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
1269
1291
visibleLength : 0 ,
1270
1292
} ;
1271
1293
_scrollRef : ?React . ElementRef < any > = null ;
1294
+ _sentStartForContentLength = 0 ;
1272
1295
_sentEndForContentLength = 0 ;
1273
1296
_totalCellLength = 0 ;
1274
1297
_totalCellsMeasured = 0 ;
@@ -1464,7 +1487,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
1464
1487
}
1465
1488
this.props.onLayout && this . props . onLayout ( e ) ;
1466
1489
this . _scheduleCellsToRenderUpdate ( ) ;
1467
- this . _maybeCallOnEndReached ( ) ;
1490
+ this . _maybeCallOnEdgeReached ( ) ;
1468
1491
} ;
1469
1492
1470
1493
_onLayoutEmpty = e => {
@@ -1566,26 +1589,69 @@ class VirtualizedList extends React.PureComponent<Props, State> {
1566
1589
return ! horizontalOrDefault ( this . props . horizontal ) ? metrics . y : metrics . x ;
1567
1590
}
1568
1591
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 ;
1572
1602
const { contentLength, visibleLength, offset} = this . _scrollMetrics ;
1603
+ const distanceFromStart = offset ;
1573
1604
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
1576
1618
if (
1577
1619
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
1581
1623
) {
1582
- // Only call onEndReached once for a given content length
1583
1624
this . _sentEndForContentLength = this . _scrollMetrics . contentLength ;
1584
1625
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 ;
1589
1655
}
1590
1656
}
1591
1657
@@ -1610,7 +1676,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
1610
1676
}
1611
1677
this._scrollMetrics.contentLength = this._selectLength({ height , width } );
1612
1678
this._scheduleCellsToRenderUpdate();
1613
- this._maybeCallOnEndReached ();
1679
+ this._maybeCallOnEdgeReached ();
1614
1680
} ;
1615
1681
1616
1682
/* Translates metrics from a scroll event in a parent VirtualizedList into
@@ -1694,7 +1760,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
1694
1760
if (!this.props) {
1695
1761
return ;
1696
1762
}
1697
- this._maybeCallOnEndReached ();
1763
+ this._maybeCallOnEdgeReached ();
1698
1764
if (velocity !== 0) {
1699
1765
this . _fillRateHelper . activate ( ) ;
1700
1766
}
@@ -1707,16 +1773,23 @@ class VirtualizedList extends React.PureComponent<Props, State> {
1707
1773
const { offset , visibleLength , velocity } = this._scrollMetrics;
1708
1774
const itemCount = this.props.getItemCount(this.props.data);
1709
1775
let hiPri = false;
1776
+ const onStartReachedThreshold = onStartReachedThresholdOrDefault(
1777
+ this.props.onStartReachedThreshold,
1778
+ );
1710
1779
const onEndReachedThreshold = onEndReachedThresholdOrDefault(
1711
1780
this.props.onEndReachedThreshold,
1712
1781
);
1713
1782
const scrollingThreshold = (onEndReachedThreshold * visibleLength) / 2;
1714
1783
// Mark as high priority if we're close to the start of the first item
1715
1784
// But only if there are items before the first rendered item
1716
1785
if (first > 0 ) {
1717
- const distTop = offset - this . _getFrameMetricsApprox ( first ) . offset ;
1786
+ const distStart = offset - this . __getFrameMetricsApprox ( first ) . offset ;
1718
1787
hiPri =
1719
- hiPri || distTop < 0 || ( velocity < - 2 && distTop < scrollingThreshold ) ;
1788
+ hiPri ||
1789
+ distStart < 0 ||
1790
+ ( velocity < - 2 &&
1791
+ distStart <
1792
+ getScrollingThreshold ( onStartReachedThreshold , visibleLength ) ) ;
1720
1793
}
1721
1794
// Mark as high priority if we're close to the end of the last item
1722
1795
// But only if there are items after the last rendered item
@@ -1726,7 +1799,9 @@ class VirtualizedList extends React.PureComponent<Props, State> {
1726
1799
hiPri =
1727
1800
hiPri ||
1728
1801
distBottom < 0 ||
1729
- ( velocity > 2 && distBottom < scrollingThreshold ) ;
1802
+ ( velocity > 2 &&
1803
+ distBottom <
1804
+ getScrollingThreshold ( onEndReachedThreshold , visibleLength ) ) ;
1730
1805
}
1731
1806
// Only trigger high-priority updates if we've actually rendered cells,
1732
1807
// and with that size estimate, accurately compute how many cells we should render.
0 commit comments