@@ -1195,7 +1195,10 @@ class VirtualizedList extends StateSafePureComponent<
11951195 }
11961196
11971197 componentDidUpdate ( prevProps : VirtualizedListProps ) {
1198- const { data, extraData, getItemLayout} = this . props ;
1198+ const { data, extraData} = this . props ;
1199+ if ( data !== prevProps . data ) {
1200+ this . _dataChangeCount ++ ;
1201+ }
11991202 if ( data !== prevProps . data || extraData !== prevProps . extraData ) {
12001203 // clear the viewableIndices cache to also trigger
12011204 // the onViewableItemsChanged callback with the new data
@@ -1217,11 +1220,14 @@ class VirtualizedList extends StateSafePureComponent<
12171220 this . _hiPriInProgress = false ;
12181221 }
12191222
1220- // We only call `onEndReached` after we render the last cell, but when
1221- // getItemLayout is present, we can scroll past the last rendered cell, and
1222- // never trigger a new layout or bounds change, so we need to check again
1223- // after rendering more cells.
1224- if ( getItemLayout != null ) {
1223+ // We only call `onEndReached` after we render the last cell, but
1224+ // programmatic scrolling (e.g. scrollToItem, scrollToIndex) or using
1225+ // getItemLayout can scroll past the last rendered cell, and never
1226+ // trigger a new layout or bounds change, so we need to check again
1227+ // after rendering more cells. We gate on having received at least one
1228+ // scroll event (timestamp > 0) to avoid false onEndReached calls
1229+ // during initial render.
1230+ if ( this . props . getItemLayout != null || this . _scrollMetrics . timestamp > 0 ) {
12251231 this . _maybeCallOnEdgeReached ( ) ;
12261232 }
12271233 }
@@ -1264,8 +1270,16 @@ class VirtualizedList extends StateSafePureComponent<
12641270 zoomScale : 1 ,
12651271 } ;
12661272 _scrollRef: ?React . ElementRef < typeof ScrollView > = null ;
1267- _sentStartForContentLength = 0 ;
1268- _sentEndForContentLength = 0 ;
1273+ _contentLengthDataChangeCount: number = 0 ;
1274+ _dataChangeCount: number = 0 ;
1275+ _sentStartForFirstItemKey: ?string = null ;
1276+ _sentStartForItemCount: ?number = null ;
1277+ _sentStartForOffset: ?number = null ;
1278+ _sentEndForContentLength: ?number = null ;
1279+ _sentEndForDataChangeCount: ?number = null ;
1280+ _sentEndForItemCount: ?number = null ;
1281+ _sentEndForLastItemKey: ?string = null ;
1282+ _sentEndForOffset: ?number = null ;
12691283 _updateCellsToRenderTimeoutID: ?ReturnType < typeof setTimeout > = null ;
12701284 _viewabilityTuples: Array < ViewabilityHelperCallbackTuple > = [ ] ;
12711285
@@ -1557,9 +1571,10 @@ class VirtualizedList extends StateSafePureComponent<
15571571 } ;
15581572 }
15591573
1560- _maybeCallOnEdgeReached ( ) {
1574+ _maybeCallOnEdgeReached ( isScrollEvent : boolean = false ) {
15611575 const {
15621576 data,
1577+ getItem,
15631578 getItemCount,
15641579 onStartReached,
15651580 onStartReachedThreshold,
@@ -1581,9 +1596,9 @@ class VirtualizedList extends StateSafePureComponent<
15811596 }
15821597
15831598 const { visibleLength , offset } = this . _scrollMetrics ;
1599+ const contentLength = this . _listMetrics . getContentLength ( ) ;
15841600 let distanceFromStart = offset ;
1585- let distanceFromEnd =
1586- this . _listMetrics . getContentLength ( ) - visibleLength - offset ;
1601+ let distanceFromEnd = contentLength - visibleLength - offset ;
15871602
15881603 // Especially when oERT is zero it's necessary to 'floor' very small distance values to be 0
15891604 // since debouncing causes us to not fire this event for every single "pixel" we scroll and can thus
@@ -1610,39 +1625,102 @@ class VirtualizedList extends StateSafePureComponent<
16101625 const isWithinStartThreshold = distanceFromStart <= startThreshold ;
16111626 const isWithinEndThreshold = distanceFromEnd <= endThreshold ;
16121627
1613- // First check if the user just scrolled within the end threshold
1614- // and call onEndReached only once for a given content length,
1615- // and only if onStartReached is not being executed
1628+ const itemCount = getItemCount ( data ) ;
1629+
16161630 if (
16171631 onEndReached &&
1618- this . state . cellsAroundViewport . last === getItemCount ( data ) - 1 &&
1619- isWithinEndThreshold &&
1620- this . _listMetrics . getContentLength ( ) !== this . _sentEndForContentLength
1632+ this . state . cellsAroundViewport . last === itemCount - 1 &&
1633+ isWithinEndThreshold
16211634 ) {
1622- this . _sentEndForContentLength = this . _listMetrics . getContentLength ( ) ;
1623- onEndReached ( { distanceFromEnd} ) ;
1635+ const lastItemKey =
1636+ itemCount > 0
1637+ ? VirtualizedList . _keyExtractor (
1638+ getItem ( data , itemCount - 1 ) ,
1639+ itemCount - 1 ,
1640+ this . props ,
1641+ )
1642+ : null ;
1643+ // A changed edge may represent a new callback generation even if the
1644+ // item count is unchanged.
1645+ if (
1646+ itemCount !== this . _sentEndForItemCount ||
1647+ lastItemKey !== this . _sentEndForLastItemKey ||
1648+ ( this . _dataChangeCount !== this . _sentEndForDataChangeCount &&
1649+ this . _contentLengthDataChangeCount === this . _dataChangeCount &&
1650+ contentLength !== this . _sentEndForContentLength ) ||
1651+ ( isScrollEvent &&
1652+ this . _sentEndForContentLength != null &&
1653+ this . _sentEndForOffset != null &&
1654+ contentLength > this . _sentEndForContentLength &&
1655+ offset > this . _sentEndForOffset )
1656+ ) {
1657+ this . _sentEndForContentLength = contentLength ;
1658+ this . _sentEndForDataChangeCount = this . _dataChangeCount ;
1659+ this . _sentEndForItemCount = itemCount ;
1660+ this . _sentEndForLastItemKey = lastItemKey ;
1661+ this . _sentEndForOffset = offset ;
1662+ onEndReached ( { distanceFromEnd} ) ;
1663+ }
16241664 }
16251665
1626- // Next check if the user just scrolled within the start threshold
1627- // and call onStartReached only once for a given content length,
1628- // and only if onEndReached is not being executed
16291666 if (
16301667 onStartReached != null &&
16311668 this . state . cellsAroundViewport . first === 0 &&
1632- isWithinStartThreshold &&
1633- this . _listMetrics . getContentLength ( ) !== this . _sentStartForContentLength
1669+ isWithinStartThreshold
16341670 ) {
1635- this . _sentStartForContentLength = this . _listMetrics . getContentLength ( ) ;
1636- onStartReached ( { distanceFromStart} ) ;
1671+ const firstItemKey =
1672+ itemCount > 0
1673+ ? VirtualizedList . _keyExtractor ( getItem ( data , 0 ) , 0 , this . props )
1674+ : null ;
1675+ // The start edge changes only when its item identity or count changes.
1676+ if (
1677+ itemCount !== this . _sentStartForItemCount ||
1678+ firstItemKey !== this . _sentStartForFirstItemKey
1679+ ) {
1680+ this . _sentStartForFirstItemKey = firstItemKey ;
1681+ this . _sentStartForItemCount = itemCount ;
1682+ this . _sentStartForOffset = offset ;
1683+ onStartReached ( { distanceFromStart} ) ;
1684+ }
16371685 }
16381686
16391687 // If the user scrolls away from the start or end and back again,
16401688 // cause onStartReached or onEndReached to be triggered again
1641- if ( ! isWithinStartThreshold ) {
1642- this . _sentStartForContentLength = 0 ;
1689+ if ( isScrollEvent && this . _sentStartForOffset != null ) {
1690+ if ( offset < this . _sentStartForOffset ) {
1691+ this . _sentStartForOffset = offset ;
1692+ } else if ( ! isWithinStartThreshold && offset > this . _sentStartForOffset ) {
1693+ this . _sentStartForFirstItemKey = null ;
1694+ this . _sentStartForItemCount = null ;
1695+ this . _sentStartForOffset = null ;
1696+ }
16431697 }
1644- if ( ! isWithinEndThreshold ) {
1645- this . _sentEndForContentLength = 0 ;
1698+ if ( this . _sentEndForOffset != null ) {
1699+ if (
1700+ ! isScrollEvent &&
1701+ this . _dataChangeCount !== this . _sentEndForDataChangeCount &&
1702+ this . _contentLengthDataChangeCount === this . _dataChangeCount &&
1703+ contentLength !== this . _sentEndForContentLength &&
1704+ ! isWithinEndThreshold
1705+ ) {
1706+ this . _sentEndForContentLength = null ;
1707+ this . _sentEndForDataChangeCount = null ;
1708+ this . _sentEndForItemCount = null ;
1709+ this . _sentEndForLastItemKey = null ;
1710+ this . _sentEndForOffset = null ;
1711+ } else if ( isScrollEvent && offset > this . _sentEndForOffset ) {
1712+ this . _sentEndForOffset = offset ;
1713+ } else if (
1714+ isScrollEvent &&
1715+ ! isWithinEndThreshold &&
1716+ offset < this . _sentEndForOffset
1717+ ) {
1718+ this . _sentEndForContentLength = null ;
1719+ this . _sentEndForDataChangeCount = null ;
1720+ this . _sentEndForItemCount = null ;
1721+ this . _sentEndForLastItemKey = null ;
1722+ this . _sentEndForOffset = null ;
1723+ }
16461724 }
16471725 }
16481726
@@ -1651,6 +1729,7 @@ class VirtualizedList extends StateSafePureComponent<
16511729 layout : { width, height} ,
16521730 orientation : this . _orientation ( ) ,
16531731 } ) ;
1732+ this . _contentLengthDataChangeCount = this . _dataChangeCount ;
16541733
16551734 this . _maybeScrollToInitialScrollIndex ( width , height ) ;
16561735
@@ -1784,13 +1863,18 @@ class VirtualizedList extends StateSafePureComponent<
17841863 zoomScale,
17851864 } ;
17861865 if ( this . state . pendingScrollUpdateCount > 0 ) {
1787- this . setState < 'pendingScrollUpdateCount' > ( { pendingScrollUpdateCount : 0 } ) ;
1866+ this . setState < 'pendingScrollUpdateCount' > (
1867+ { pendingScrollUpdateCount : 0 } ,
1868+ ( ) => {
1869+ this . _maybeCallOnEdgeReached ( true ) ;
1870+ } ,
1871+ ) ;
17881872 }
17891873 this . _updateViewableItems ( this . props , this . state . cellsAroundViewport ) ;
17901874 if ( ! this . props ) {
17911875 return ;
17921876 }
1793- this . _maybeCallOnEdgeReached ( ) ;
1877+ this . _maybeCallOnEdgeReached ( true ) ;
17941878 if ( velocity !== 0 ) {
17951879 this . _fillRateHelper . activate ( ) ;
17961880 }
0 commit comments