@@ -50,7 +50,7 @@ import * as React from 'react';
50
50
51
51
export type { RenderItemProps , RenderItemType , Separators } ;
52
52
53
- const ON_END_REACHED_EPSILON = 0.001 ;
53
+ const ON_EDGE_REACHED_EPSILON = 0.001 ;
54
54
55
55
let _usedIndexForKey = false ;
56
56
let _keylessItemComponentName : string = '' ;
@@ -90,11 +90,21 @@ function maxToRenderPerBatchOrDefault(maxToRenderPerBatch: ?number) {
90
90
return maxToRenderPerBatch ?? 10 ;
91
91
}
92
92
93
+ // onStartReachedThresholdOrDefault(this.props.onStartReachedThreshold)
94
+ function onStartReachedThresholdOrDefault ( onStartReachedThreshold : ?number ) {
95
+ return onStartReachedThreshold ?? 2 ;
96
+ }
97
+
93
98
// onEndReachedThresholdOrDefault(this.props.onEndReachedThreshold)
94
99
function onEndReachedThresholdOrDefault ( onEndReachedThreshold : ?number ) {
95
100
return onEndReachedThreshold ?? 2 ;
96
101
}
97
102
103
+ // getScrollingThreshold(visibleLength, onEndReachedThreshold)
104
+ function getScrollingThreshold ( threshold : number , visibleLength : number ) {
105
+ return ( threshold * visibleLength ) / 2 ;
106
+ }
107
+
98
108
// scrollEventThrottleOrDefault(this.props.scrollEventThrottle)
99
109
function scrollEventThrottleOrDefault ( scrollEventThrottle : ?number ) {
100
110
return scrollEventThrottle ?? 50 ;
@@ -1114,6 +1124,7 @@ export default class VirtualizedList extends StateSafePureComponent<
1114
1124
zoomScale : 1 ,
1115
1125
} ;
1116
1126
_scrollRef : ?React . ElementRef < any > = null ;
1127
+ _sentStartForContentLength = 0 ;
1117
1128
_sentEndForContentLength = 0 ;
1118
1129
_totalCellLength = 0 ;
1119
1130
_totalCellsMeasured = 0 ;
@@ -1301,7 +1312,7 @@ export default class VirtualizedList extends StateSafePureComponent<
1301
1312
}
1302
1313
this.props.onLayout && this . props . onLayout ( e ) ;
1303
1314
this . _scheduleCellsToRenderUpdate ( ) ;
1304
- this . _maybeCallOnEndReached ( ) ;
1315
+ this . _maybeCallOnEdgeReached ( ) ;
1305
1316
} ;
1306
1317
1307
1318
_onLayoutEmpty = ( e : LayoutEvent ) => {
@@ -1410,35 +1421,86 @@ export default class VirtualizedList extends StateSafePureComponent<
1410
1421
return ! horizontalOrDefault ( this . props . horizontal ) ? metrics . y : metrics . x ;
1411
1422
}
1412
1423
1413
- _maybeCallOnEndReached() {
1414
- const { data , getItemCount , onEndReached , onEndReachedThreshold } =
1415
- this.props;
1424
+ _maybeCallOnEdgeReached() {
1425
+ const {
1426
+ data,
1427
+ getItemCount,
1428
+ onStartReached,
1429
+ onStartReachedThreshold,
1430
+ onEndReached,
1431
+ onEndReachedThreshold,
1432
+ initialScrollIndex,
1433
+ } = this . props ;
1416
1434
const { contentLength, visibleLength, offset} = this . _scrollMetrics ;
1435
+ let distanceFromStart = offset ;
1417
1436
let distanceFromEnd = contentLength - visibleLength - offset ;
1418
1437
1419
- // Especially when oERT is zero it's necessary to 'floor' very small distanceFromEnd values to be 0
1438
+ // Especially when oERT is zero it's necessary to 'floor' very small distance values to be 0
1420
1439
// since debouncing causes us to not fire this event for every single "pixel" we scroll and can thus
1421
- // be at the "end" of the list with a distanceFromEnd approximating 0 but not quite there.
1422
- if (distanceFromEnd < ON_END_REACHED_EPSILON ) {
1440
+ // be at the edge of the list with a distance approximating 0 but not quite there.
1441
+ if ( distanceFromStart < ON_EDGE_REACHED_EPSILON ) {
1442
+ distanceFromStart = 0 ;
1443
+ }
1444
+ if (distanceFromEnd < ON_EDGE_REACHED_EPSILON ) {
1423
1445
distanceFromEnd = 0 ;
1424
1446
}
1425
1447
1426
- // TODO: T121172172 Look into why we're "defaulting" to a threshold of 2 when oERT is not present
1427
- const threshold =
1428
- onEndReachedThreshold != null ? onEndReachedThreshold * visibleLength : 2 ;
1448
+ // TODO: T121172172 Look into why we're "defaulting" to a threshold of 2px
1449
+ // when oERT is not present (different from 2 viewports used elsewhere)
1450
+ const DEFAULT_THRESHOLD_PX = 2 ;
1451
+
1452
+ const startThreshold =
1453
+ onStartReachedThreshold != null
1454
+ ? onStartReachedThreshold * visibleLength
1455
+ : DEFAULT_THRESHOLD_PX ;
1456
+ const endThreshold =
1457
+ onEndReachedThreshold != null
1458
+ ? onEndReachedThreshold * visibleLength
1459
+ : DEFAULT_THRESHOLD_PX ;
1460
+ const isWithinStartThreshold = distanceFromStart <= startThreshold ;
1461
+ const isWithinEndThreshold = distanceFromEnd <= endThreshold ;
1462
+
1463
+ // First check if the user just scrolled within the end threshold
1464
+ // and call onEndReached only once for a given content length,
1465
+ // and only if onStartReached is not being executed
1429
1466
if (
1430
1467
onEndReached &&
1431
1468
this . state . cellsAroundViewport . last = = = getItemCount ( data ) - 1 &&
1432
- distanceFromEnd <= threshold &&
1469
+ isWithinEndThreshold &&
1433
1470
this . _scrollMetrics . contentLength !== this . _sentEndForContentLength
1434
1471
) {
1435
- // Only call onEndReached once for a given content length
1436
1472
this . _sentEndForContentLength = this . _scrollMetrics . contentLength ;
1437
1473
onEndReached ( { distanceFromEnd} ) ;
1438
- } else if ( distanceFromEnd > threshold) {
1439
- // If the user scrolls away from the end and back again cause
1440
- // an onEndReached to be triggered again
1441
- this . _sentEndForContentLength = 0 ;
1474
+ }
1475
+
1476
+ // Next check if the user just scrolled within the start threshold
1477
+ // and call onStartReached only once for a given content length,
1478
+ // and only if onEndReached is not being executed
1479
+ else if (
1480
+ onStartReached != null &&
1481
+ this . state . cellsAroundViewport . first = = = 0 &&
1482
+ isWithinStartThreshold &&
1483
+ this . _scrollMetrics . contentLength !== this . _sentStartForContentLength
1484
+ ) {
1485
+ // On initial mount when using initialScrollIndex the offset will be 0 initially
1486
+ // and will trigger an unexpected onStartReached. To avoid this we can use
1487
+ // timestamp to differentiate between the initial scroll metrics and when we actually
1488
+ // received the first scroll event.
1489
+ if ( ! initialScrollIndex || this . _scrollMetrics . timestamp !== 0 ) {
1490
+ this . _sentStartForContentLength = this . _scrollMetrics . contentLength ;
1491
+ onStartReached ( { distanceFromStart} ) ;
1492
+ }
1493
+ }
1494
+
1495
+ // If the user scrolls away from the start or end and back again,
1496
+ // cause onStartReached or onEndReached to be triggered again
1497
+ else {
1498
+ this . _sentStartForContentLength = isWithinStartThreshold
1499
+ ? this . _sentStartForContentLength
1500
+ : 0 ;
1501
+ this . _sentEndForContentLength = isWithinEndThreshold
1502
+ ? this . _sentEndForContentLength
1503
+ : 0 ;
1442
1504
}
1443
1505
}
1444
1506
@@ -1463,7 +1525,7 @@ export default class VirtualizedList extends StateSafePureComponent<
1463
1525
}
1464
1526
this . _scrollMetrics . contentLength = this . _selectLength ( { height , width } ) ;
1465
1527
this . _scheduleCellsToRenderUpdate ( ) ;
1466
- this._maybeCallOnEndReached ();
1528
+ this . _maybeCallOnEdgeReached ( ) ;
1467
1529
} ;
1468
1530
1469
1531
/* Translates metrics from a scroll event in a parent VirtualizedList into
@@ -1551,7 +1613,7 @@ export default class VirtualizedList extends StateSafePureComponent<
1551
1613
if ( ! this . props ) {
1552
1614
return;
1553
1615
}
1554
- this . _maybeCallOnEndReached ( ) ;
1616
+ this . _maybeCallOnEdgeReached ( ) ;
1555
1617
if ( velocity !== 0 ) {
1556
1618
this . _fillRateHelper . activate ( ) ;
1557
1619
}
@@ -1564,28 +1626,34 @@ export default class VirtualizedList extends StateSafePureComponent<
1564
1626
const { offset, visibleLength, velocity} = this . _scrollMetrics ;
1565
1627
const itemCount = this . props . getItemCount ( this . props . data ) ;
1566
1628
let hiPri = false ;
1629
+ const onStartReachedThreshold = onStartReachedThresholdOrDefault (
1630
+ this . props . onStartReachedThreshold ,
1631
+ ) ;
1567
1632
const onEndReachedThreshold = onEndReachedThresholdOrDefault (
1568
1633
this . props . onEndReachedThreshold ,
1569
1634
) ;
1570
- const scrollingThreshold = ( onEndReachedThreshold * visibleLength ) / 2 ;
1571
1635
// Mark as high priority if we're close to the start of the first item
1572
1636
// But only if there are items before the first rendered item
1573
1637
if ( first > 0 ) {
1574
1638
const distTop =
1575
1639
offset - this . __getFrameMetricsApprox ( first , this . props ) . offset ;
1576
1640
hiPri =
1577
- hiPri || distTop < 0 || ( velocity < - 2 && distTop < scrollingThreshold ) ;
1641
+ distTop < 0 ||
1642
+ ( velocity < - 2 &&
1643
+ distTop <
1644
+ getScrollingThreshold ( onStartReachedThreshold , visibleLength ) ) ;
1578
1645
}
1579
1646
// Mark as high priority if we're close to the end of the last item
1580
1647
// But only if there are items after the last rendered item
1581
- if ( last >= 0 && last < itemCount - 1 ) {
1648
+ if ( ! hiPri && last >= 0 && last < itemCount - 1 ) {
1582
1649
const distBottom =
1583
1650
this . __getFrameMetricsApprox ( last , this . props ) . offset -
1584
1651
( offset + visibleLength ) ;
1585
1652
hiPri =
1586
- hiPri ||
1587
1653
distBottom < 0 ||
1588
- ( velocity > 2 && distBottom < scrollingThreshold ) ;
1654
+ ( velocity > 2 &&
1655
+ distBottom <
1656
+ getScrollingThreshold ( onEndReachedThreshold , visibleLength ) ) ;
1589
1657
}
1590
1658
// Only trigger high-priority updates if we've actually rendered cells,
1591
1659
// and with that size estimate, accurately compute how many cells we should render.
0 commit comments