@react-native/virtualized-lists 0.73.0 → 0.73.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,15 +16,17 @@ import type {
16
16
  } from 'react-native/Libraries/Types/CoreEventTypes';
17
17
  import type {ViewToken} from './ViewabilityHelper';
18
18
  import type {
19
- FrameMetricProps,
20
19
  Item,
21
20
  Props,
22
21
  RenderItemProps,
23
22
  RenderItemType,
24
23
  Separators,
25
24
  } from './VirtualizedListProps';
25
+ import type {CellMetricProps, ListOrientation} from './ListMetricsAggregator';
26
26
 
27
27
  import {
28
+ I18nManager,
29
+ Platform,
28
30
  RefreshControl,
29
31
  ScrollView,
30
32
  View,
@@ -37,6 +39,7 @@ import infoLog from '../Utilities/infoLog';
37
39
  import {CellRenderMask} from './CellRenderMask';
38
40
  import ChildListCollection from './ChildListCollection';
39
41
  import FillRateHelper from './FillRateHelper';
42
+ import ListMetricsAggregator from './ListMetricsAggregator';
40
43
  import StateSafePureComponent from './StateSafePureComponent';
41
44
  import ViewabilityHelper from './ViewabilityHelper';
42
45
  import CellRenderer from './VirtualizedListCellRenderer';
@@ -53,6 +56,15 @@ import invariant from 'invariant';
53
56
  import nullthrows from 'nullthrows';
54
57
  import * as React from 'react';
55
58
 
59
+ import {
60
+ horizontalOrDefault,
61
+ initialNumToRenderOrDefault,
62
+ maxToRenderPerBatchOrDefault,
63
+ onStartReachedThresholdOrDefault,
64
+ onEndReachedThresholdOrDefault,
65
+ windowSizeOrDefault,
66
+ } from './VirtualizedListProps';
67
+
56
68
  export type {RenderItemProps, RenderItemType, Separators};
57
69
 
58
70
  const ON_EDGE_REACHED_EPSILON = 0.001;
@@ -73,53 +85,12 @@ type ViewabilityHelperCallbackTuple = {
73
85
  type State = {
74
86
  renderMask: CellRenderMask,
75
87
  cellsAroundViewport: {first: number, last: number},
88
+ // Used to track items added at the start of the list for maintainVisibleContentPosition.
89
+ firstVisibleItemKey: ?string,
90
+ // When > 0 the scroll position available in JS is considered stale and should not be used.
91
+ pendingScrollUpdateCount: number,
76
92
  };
77
93
 
78
- /**
79
- * Default Props Helper Functions
80
- * Use the following helper functions for default values
81
- */
82
-
83
- // horizontalOrDefault(this.props.horizontal)
84
- function horizontalOrDefault(horizontal: ?boolean) {
85
- return horizontal ?? false;
86
- }
87
-
88
- // initialNumToRenderOrDefault(this.props.initialNumToRender)
89
- function initialNumToRenderOrDefault(initialNumToRender: ?number) {
90
- return initialNumToRender ?? 10;
91
- }
92
-
93
- // maxToRenderPerBatchOrDefault(this.props.maxToRenderPerBatch)
94
- function maxToRenderPerBatchOrDefault(maxToRenderPerBatch: ?number) {
95
- return maxToRenderPerBatch ?? 10;
96
- }
97
-
98
- // onStartReachedThresholdOrDefault(this.props.onStartReachedThreshold)
99
- function onStartReachedThresholdOrDefault(onStartReachedThreshold: ?number) {
100
- return onStartReachedThreshold ?? 2;
101
- }
102
-
103
- // onEndReachedThresholdOrDefault(this.props.onEndReachedThreshold)
104
- function onEndReachedThresholdOrDefault(onEndReachedThreshold: ?number) {
105
- return onEndReachedThreshold ?? 2;
106
- }
107
-
108
- // getScrollingThreshold(visibleLength, onEndReachedThreshold)
109
- function getScrollingThreshold(threshold: number, visibleLength: number) {
110
- return (threshold * visibleLength) / 2;
111
- }
112
-
113
- // scrollEventThrottleOrDefault(this.props.scrollEventThrottle)
114
- function scrollEventThrottleOrDefault(scrollEventThrottle: ?number) {
115
- return scrollEventThrottle ?? 50;
116
- }
117
-
118
- // windowSizeOrDefault(this.props.windowSize)
119
- function windowSizeOrDefault(windowSize: ?number) {
120
- return windowSize ?? 21;
121
- }
122
-
123
94
  function findLastWhere<T>(
124
95
  arr: $ReadOnlyArray<T>,
125
96
  predicate: (element: T) => boolean,
@@ -133,6 +104,10 @@ function findLastWhere<T>(
133
104
  return null;
134
105
  }
135
106
 
107
+ function getScrollingThreshold(threshold: number, visibleLength: number) {
108
+ return (threshold * visibleLength) / 2;
109
+ }
110
+
136
111
  /**
137
112
  * Base implementation for the more convenient [`<FlatList>`](https://reactnative.dev/docs/flatlist)
138
113
  * and [`<SectionList>`](https://reactnative.dev/docs/sectionlist) components, which are also better
@@ -172,7 +147,7 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
172
147
  if (veryLast < 0) {
173
148
  return;
174
149
  }
175
- const frame = this.__getFrameMetricsApprox(veryLast, this.props);
150
+ const frame = this._listMetrics.getCellMetricsApprox(veryLast, this.props);
176
151
  const offset = Math.max(
177
152
  0,
178
153
  frame.offset +
@@ -181,24 +156,8 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
181
156
  this._scrollMetrics.visibleLength,
182
157
  );
183
158
 
184
- if (this._scrollRef == null) {
185
- return;
186
- }
187
-
188
- if (this._scrollRef.scrollTo == null) {
189
- console.warn(
190
- 'No scrollTo method provided. This may be because you have two nested ' +
191
- 'VirtualizedLists with the same orientation, or because you are ' +
192
- 'using a custom component that does not implement scrollTo.',
193
- );
194
- return;
195
- }
196
-
197
- this._scrollRef.scrollTo(
198
- horizontalOrDefault(this.props.horizontal)
199
- ? {x: offset, animated}
200
- : {y: offset, animated},
201
- );
159
+ // TODO: consider using `ref.scrollToEnd` directly
160
+ this.scrollToOffset({animated, offset});
202
161
  }
203
162
 
204
163
  // scrollToIndex may be janky without getItemLayout prop
@@ -209,13 +168,8 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
209
168
  viewPosition?: number,
210
169
  ...
211
170
  }): $FlowFixMe {
212
- const {
213
- data,
214
- horizontal,
215
- getItemCount,
216
- getItemLayout,
217
- onScrollToIndexFailed,
218
- } = this.props;
171
+ const {data, getItemCount, getItemLayout, onScrollToIndexFailed} =
172
+ this.props;
219
173
  const {animated, index, viewOffset, viewPosition} = params;
220
174
  invariant(
221
175
  index >= 0,
@@ -233,44 +187,36 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
233
187
  getItemCount(data) - 1
234
188
  }`,
235
189
  );
236
- if (!getItemLayout && index > this._highestMeasuredFrameIndex) {
190
+ if (
191
+ !getItemLayout &&
192
+ index > this._listMetrics.getHighestMeasuredCellIndex()
193
+ ) {
237
194
  invariant(
238
195
  !!onScrollToIndexFailed,
239
196
  'scrollToIndex should be used in conjunction with getItemLayout or onScrollToIndexFailed, ' +
240
197
  'otherwise there is no way to know the location of offscreen indices or handle failures.',
241
198
  );
242
199
  onScrollToIndexFailed({
243
- averageItemLength: this._averageCellLength,
244
- highestMeasuredFrameIndex: this._highestMeasuredFrameIndex,
200
+ averageItemLength: this._listMetrics.getAverageCellLength(),
201
+ highestMeasuredFrameIndex:
202
+ this._listMetrics.getHighestMeasuredCellIndex(),
245
203
  index,
246
204
  });
247
205
  return;
248
206
  }
249
- const frame = this.__getFrameMetricsApprox(Math.floor(index), this.props);
207
+ const frame = this._listMetrics.getCellMetricsApprox(
208
+ Math.floor(index),
209
+ this.props,
210
+ );
250
211
  const offset =
251
212
  Math.max(
252
213
  0,
253
- this._getOffsetApprox(index, this.props) -
214
+ this._listMetrics.getCellOffsetApprox(index, this.props) -
254
215
  (viewPosition || 0) *
255
216
  (this._scrollMetrics.visibleLength - frame.length),
256
217
  ) - (viewOffset || 0);
257
218
 
258
- if (this._scrollRef == null) {
259
- return;
260
- }
261
-
262
- if (this._scrollRef.scrollTo == null) {
263
- console.warn(
264
- 'No scrollTo method provided. This may be because you have two nested ' +
265
- 'VirtualizedLists with the same orientation, or because you are ' +
266
- 'using a custom component that does not implement scrollTo.',
267
- );
268
- return;
269
- }
270
-
271
- this._scrollRef.scrollTo(
272
- horizontal ? {x: offset, animated} : {y: offset, animated},
273
- );
219
+ this.scrollToOffset({offset, animated});
274
220
  }
275
221
 
276
222
  // scrollToItem may be janky without getItemLayout prop. Required linear scan through items -
@@ -305,12 +251,13 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
305
251
  */
306
252
  scrollToOffset(params: {animated?: ?boolean, offset: number, ...}) {
307
253
  const {animated, offset} = params;
254
+ const scrollRef = this._scrollRef;
308
255
 
309
- if (this._scrollRef == null) {
256
+ if (scrollRef == null) {
310
257
  return;
311
258
  }
312
259
 
313
- if (this._scrollRef.scrollTo == null) {
260
+ if (scrollRef.scrollTo == null) {
314
261
  console.warn(
315
262
  'No scrollTo method provided. This may be because you have two nested ' +
316
263
  'VirtualizedLists with the same orientation, or because you are ' +
@@ -319,11 +266,31 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
319
266
  return;
320
267
  }
321
268
 
322
- this._scrollRef.scrollTo(
323
- horizontalOrDefault(this.props.horizontal)
324
- ? {x: offset, animated}
325
- : {y: offset, animated},
326
- );
269
+ const {horizontal, rtl} = this._orientation();
270
+ if (horizontal && rtl && !this._listMetrics.hasContentLength()) {
271
+ console.warn(
272
+ 'scrollToOffset may not be called in RTL before content is laid out',
273
+ );
274
+ return;
275
+ }
276
+
277
+ scrollRef.scrollTo({
278
+ animated,
279
+ ...this._scrollToParamsFromOffset(offset),
280
+ });
281
+ }
282
+
283
+ _scrollToParamsFromOffset(offset: number): {x?: number, y?: number} {
284
+ const {horizontal, rtl} = this._orientation();
285
+ if (horizontal && rtl) {
286
+ // Add the visible length of the scrollview so that the offset is right-aligned
287
+ const cartOffset = this._listMetrics.cartesianOffset(
288
+ offset + this._scrollMetrics.visibleLength,
289
+ );
290
+ return horizontal ? {x: cartOffset} : {y: cartOffset};
291
+ } else {
292
+ return horizontal ? {x: offset} : {y: offset};
293
+ }
327
294
  }
328
295
 
329
296
  recordInteraction() {
@@ -423,7 +390,7 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
423
390
  super(props);
424
391
  this._checkProps(props);
425
392
 
426
- this._fillRateHelper = new FillRateHelper(this._getFrameMetrics);
393
+ this._fillRateHelper = new FillRateHelper(this._listMetrics);
427
394
  this._updateCellsToRenderBatcher = new Batchinator(
428
395
  this._updateCellsToRender,
429
396
  this.props.updateCellsBatchingPeriod ?? 50,
@@ -448,9 +415,24 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
448
415
 
449
416
  const initialRenderRegion = VirtualizedList._initialRenderRegion(props);
450
417
 
418
+ const minIndexForVisible =
419
+ this.props.maintainVisibleContentPosition?.minIndexForVisible ?? 0;
420
+
451
421
  this.state = {
452
422
  cellsAroundViewport: initialRenderRegion,
453
423
  renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion),
424
+ firstVisibleItemKey:
425
+ this.props.getItemCount(this.props.data) > minIndexForVisible
426
+ ? VirtualizedList._getItemKey(this.props, minIndexForVisible)
427
+ : null,
428
+ // When we have a non-zero initialScrollIndex, we will receive a
429
+ // scroll event later so this will prevent the window from updating
430
+ // until we get a valid offset.
431
+ pendingScrollUpdateCount:
432
+ this.props.initialScrollIndex != null &&
433
+ this.props.initialScrollIndex > 0
434
+ ? 1
435
+ : 0,
454
436
  };
455
437
  }
456
438
 
@@ -502,6 +484,40 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
502
484
  }
503
485
  }
504
486
 
487
+ static _findItemIndexWithKey(
488
+ props: Props,
489
+ key: string,
490
+ hint: ?number,
491
+ ): ?number {
492
+ const itemCount = props.getItemCount(props.data);
493
+ if (hint != null && hint >= 0 && hint < itemCount) {
494
+ const curKey = VirtualizedList._getItemKey(props, hint);
495
+ if (curKey === key) {
496
+ return hint;
497
+ }
498
+ }
499
+ for (let ii = 0; ii < itemCount; ii++) {
500
+ const curKey = VirtualizedList._getItemKey(props, ii);
501
+ if (curKey === key) {
502
+ return ii;
503
+ }
504
+ }
505
+ return null;
506
+ }
507
+
508
+ static _getItemKey(
509
+ props: {
510
+ data: Props['data'],
511
+ getItem: Props['getItem'],
512
+ keyExtractor: Props['keyExtractor'],
513
+ ...
514
+ },
515
+ index: number,
516
+ ): string {
517
+ const item = props.getItem(props.data, index);
518
+ return VirtualizedList._keyExtractor(item, index, props);
519
+ }
520
+
505
521
  static _createRenderMask(
506
522
  props: Props,
507
523
  cellsAroundViewport: {first: number, last: number},
@@ -585,12 +601,14 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
585
601
  _adjustCellsAroundViewport(
586
602
  props: Props,
587
603
  cellsAroundViewport: {first: number, last: number},
604
+ pendingScrollUpdateCount: number,
588
605
  ): {first: number, last: number} {
589
606
  const {data, getItemCount} = props;
590
607
  const onEndReachedThreshold = onEndReachedThresholdOrDefault(
591
608
  props.onEndReachedThreshold,
592
609
  );
593
- const {contentLength, offset, visibleLength} = this._scrollMetrics;
610
+ const {offset, visibleLength} = this._scrollMetrics;
611
+ const contentLength = this._listMetrics.getContentLength();
594
612
  const distanceFromEnd = contentLength - visibleLength - offset;
595
613
 
596
614
  // Wait until the scroll view metrics have been set up. And until then,
@@ -616,21 +634,9 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
616
634
  ),
617
635
  };
618
636
  } else {
619
- // If we have a non-zero initialScrollIndex and run this before we've scrolled,
620
- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex.
621
- // So let's wait until we've scrolled the view to the right place. And until then,
622
- // we will trust the initialScrollIndex suggestion.
623
-
624
- // Thus, we want to recalculate the windowed render limits if any of the following hold:
625
- // - initialScrollIndex is undefined or is 0
626
- // - initialScrollIndex > 0 AND scrolling is complete
627
- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case
628
- // where the list is shorter than the visible area)
629
- if (
630
- props.initialScrollIndex &&
631
- !this._scrollMetrics.offset &&
632
- Math.abs(distanceFromEnd) >= Number.EPSILON
633
- ) {
637
+ // If we have a pending scroll update, we should not adjust the render window as it
638
+ // might override the correct window.
639
+ if (pendingScrollUpdateCount > 0) {
634
640
  return cellsAroundViewport.last >= getItemCount(data)
635
641
  ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props)
636
642
  : cellsAroundViewport;
@@ -641,7 +647,7 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
641
647
  maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch),
642
648
  windowSizeOrDefault(props.windowSize),
643
649
  cellsAroundViewport,
644
- this.__getFrameMetricsApprox,
650
+ this._listMetrics,
645
651
  this._scrollMetrics,
646
652
  );
647
653
  invariant(
@@ -712,14 +718,59 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
712
718
  return prevState;
713
719
  }
714
720
 
721
+ let maintainVisibleContentPositionAdjustment: ?number = null;
722
+ const prevFirstVisibleItemKey = prevState.firstVisibleItemKey;
723
+ const minIndexForVisible =
724
+ newProps.maintainVisibleContentPosition?.minIndexForVisible ?? 0;
725
+ const newFirstVisibleItemKey =
726
+ newProps.getItemCount(newProps.data) > minIndexForVisible
727
+ ? VirtualizedList._getItemKey(newProps, minIndexForVisible)
728
+ : null;
729
+ if (
730
+ newProps.maintainVisibleContentPosition != null &&
731
+ prevFirstVisibleItemKey != null &&
732
+ newFirstVisibleItemKey != null
733
+ ) {
734
+ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) {
735
+ // Fast path if items were added at the start of the list.
736
+ const hint =
737
+ itemCount - prevState.renderMask.numCells() + minIndexForVisible;
738
+ const firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey(
739
+ newProps,
740
+ prevFirstVisibleItemKey,
741
+ hint,
742
+ );
743
+ maintainVisibleContentPositionAdjustment =
744
+ firstVisibleItemIndex != null
745
+ ? firstVisibleItemIndex - minIndexForVisible
746
+ : null;
747
+ } else {
748
+ maintainVisibleContentPositionAdjustment = null;
749
+ }
750
+ }
751
+
715
752
  const constrainedCells = VirtualizedList._constrainToItemCount(
716
- prevState.cellsAroundViewport,
753
+ maintainVisibleContentPositionAdjustment != null
754
+ ? {
755
+ first:
756
+ prevState.cellsAroundViewport.first +
757
+ maintainVisibleContentPositionAdjustment,
758
+ last:
759
+ prevState.cellsAroundViewport.last +
760
+ maintainVisibleContentPositionAdjustment,
761
+ }
762
+ : prevState.cellsAroundViewport,
717
763
  newProps,
718
764
  );
719
765
 
720
766
  return {
721
767
  cellsAroundViewport: constrainedCells,
722
768
  renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells),
769
+ firstVisibleItemKey: newFirstVisibleItemKey,
770
+ pendingScrollUpdateCount:
771
+ maintainVisibleContentPositionAdjustment != null
772
+ ? prevState.pendingScrollUpdateCount + 1
773
+ : prevState.pendingScrollUpdateCount,
723
774
  };
724
775
  }
725
776
 
@@ -751,7 +802,7 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
751
802
 
752
803
  for (let ii = first; ii <= last; ii++) {
753
804
  const item = getItem(data, ii);
754
- const key = this._keyExtractor(item, ii, this.props);
805
+ const key = VirtualizedList._keyExtractor(item, ii, this.props);
755
806
 
756
807
  this._indicesToKeys.set(ii, key);
757
808
  if (stickyIndicesFromProps.has(ii + stickyOffset)) {
@@ -794,15 +845,19 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
794
845
  props: Props,
795
846
  ): {first: number, last: number} {
796
847
  const itemCount = props.getItemCount(props.data);
797
- const last = Math.min(itemCount - 1, cells.last);
848
+ const lastPossibleCellIndex = itemCount - 1;
798
849
 
850
+ // Constraining `last` may significantly shrink the window. Adjust `first`
851
+ // to expand the window if the new `last` results in a new window smaller
852
+ // than the number of cells rendered per batch.
799
853
  const maxToRenderPerBatch = maxToRenderPerBatchOrDefault(
800
854
  props.maxToRenderPerBatch,
801
855
  );
856
+ const maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch);
802
857
 
803
858
  return {
804
- first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first),
805
- last,
859
+ first: clamp(0, cells.first, maxFirst),
860
+ last: Math.min(lastPossibleCellIndex, cells.last),
806
861
  };
807
862
  }
808
863
 
@@ -824,15 +879,14 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
824
879
  _getSpacerKey = (isVertical: boolean): string =>
825
880
  isVertical ? 'height' : 'width';
826
881
 
827
- _keyExtractor(
882
+ static _keyExtractor(
828
883
  item: Item,
829
884
  index: number,
830
885
  props: {
831
886
  keyExtractor?: ?(item: Item, index: number) => string,
832
887
  ...
833
888
  },
834
- // $FlowFixMe[missing-local-annot]
835
- ) {
889
+ ): string {
836
890
  if (props.keyExtractor != null) {
837
891
  return props.keyExtractor(item, index);
838
892
  }
@@ -878,6 +932,10 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
878
932
  cellKey={this._getCellKey() + '-header'}
879
933
  key="$header">
880
934
  <View
935
+ // We expect that header component will be a single native view so make it
936
+ // not collapsable to avoid this view being flattened and make this assumption
937
+ // no longer true.
938
+ collapsable={false}
881
939
  onLayout={this._onLayoutHeader}
882
940
  style={StyleSheet.compose(
883
941
  inversionStyle,
@@ -947,15 +1005,18 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
947
1005
  ? clamp(
948
1006
  section.first - 1,
949
1007
  section.last,
950
- this._highestMeasuredFrameIndex,
1008
+ this._listMetrics.getHighestMeasuredCellIndex(),
951
1009
  )
952
1010
  : section.last;
953
1011
 
954
- const firstMetrics = this.__getFrameMetricsApprox(
1012
+ const firstMetrics = this._listMetrics.getCellMetricsApprox(
955
1013
  section.first,
956
1014
  this.props,
957
1015
  );
958
- const lastMetrics = this.__getFrameMetricsApprox(last, this.props);
1016
+ const lastMetrics = this._listMetrics.getCellMetricsApprox(
1017
+ last,
1018
+ this.props,
1019
+ );
959
1020
  const spacerSize =
960
1021
  lastMetrics.offset + lastMetrics.length - firstMetrics.offset;
961
1022
  cells.push(
@@ -1024,9 +1085,9 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1024
1085
  onScrollEndDrag: this._onScrollEndDrag,
1025
1086
  onMomentumScrollBegin: this._onMomentumScrollBegin,
1026
1087
  onMomentumScrollEnd: this._onMomentumScrollEnd,
1027
- scrollEventThrottle: scrollEventThrottleOrDefault(
1028
- this.props.scrollEventThrottle,
1029
- ), // TODO: Android support
1088
+ // iOS/macOS requires a non-zero scrollEventThrottle to fire more than a
1089
+ // single notification while scrolling. This will otherwise no-op.
1090
+ scrollEventThrottle: this.props.scrollEventThrottle ?? 0.0001,
1030
1091
  invertStickyHeaders:
1031
1092
  this.props.invertStickyHeaders !== undefined
1032
1093
  ? this.props.invertStickyHeaders
@@ -1035,6 +1096,17 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1035
1096
  style: inversionStyle
1036
1097
  ? [inversionStyle, this.props.style]
1037
1098
  : this.props.style,
1099
+ isInvertedVirtualizedList: this.props.inverted,
1100
+ maintainVisibleContentPosition:
1101
+ this.props.maintainVisibleContentPosition != null
1102
+ ? {
1103
+ ...this.props.maintainVisibleContentPosition,
1104
+ // Adjust index to account for ListHeaderComponent.
1105
+ minIndexForVisible:
1106
+ this.props.maintainVisibleContentPosition.minIndexForVisible +
1107
+ (this.props.ListHeaderComponent ? 1 : 0),
1108
+ }
1109
+ : undefined,
1038
1110
  };
1039
1111
 
1040
1112
  this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1;
@@ -1123,17 +1195,9 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1123
1195
  }
1124
1196
  }
1125
1197
 
1126
- _averageCellLength = 0;
1127
1198
  _cellRefs: {[string]: null | CellRenderer<any>} = {};
1128
1199
  _fillRateHelper: FillRateHelper;
1129
- _frames: {
1130
- [string]: {
1131
- inLayout?: boolean,
1132
- index: number,
1133
- length: number,
1134
- offset: number,
1135
- },
1136
- } = {};
1200
+ _listMetrics: ListMetricsAggregator = new ListMetricsAggregator();
1137
1201
  _footerLength = 0;
1138
1202
  // Used for preventing scrollToIndex from being called multiple times for initialScrollIndex
1139
1203
  _hasTriggeredInitialScrollToIndex = false;
@@ -1142,16 +1206,22 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1142
1206
  _hasWarned: {[string]: boolean} = {};
1143
1207
  _headerLength = 0;
1144
1208
  _hiPriInProgress: boolean = false; // flag to prevent infinite hiPri cell limit update
1145
- _highestMeasuredFrameIndex = 0;
1146
1209
  _indicesToKeys: Map<number, string> = new Map();
1147
1210
  _lastFocusedCellKey: ?string = null;
1148
1211
  _nestedChildLists: ChildListCollection<VirtualizedList> =
1149
1212
  new ChildListCollection();
1150
1213
  _offsetFromParentVirtualizedList: number = 0;
1214
+ _pendingViewabilityUpdate: boolean = false;
1151
1215
  _prevParentOffset: number = 0;
1152
- // $FlowFixMe[missing-local-annot]
1153
- _scrollMetrics = {
1154
- contentLength: 0,
1216
+ _scrollMetrics: {
1217
+ dOffset: number,
1218
+ dt: number,
1219
+ offset: number,
1220
+ timestamp: number,
1221
+ velocity: number,
1222
+ visibleLength: number,
1223
+ zoomScale: number,
1224
+ } = {
1155
1225
  dOffset: 0,
1156
1226
  dt: 10,
1157
1227
  offset: 0,
@@ -1163,8 +1233,6 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1163
1233
  _scrollRef: ?React.ElementRef<any> = null;
1164
1234
  _sentStartForContentLength = 0;
1165
1235
  _sentEndForContentLength = 0;
1166
- _totalCellLength = 0;
1167
- _totalCellsMeasured = 0;
1168
1236
  _updateCellsToRenderBatcher: Batchinator;
1169
1237
  _viewabilityTuples: Array<ViewabilityHelperCallbackTuple> = [];
1170
1238
 
@@ -1222,37 +1290,23 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1222
1290
  }
1223
1291
  };
1224
1292
 
1225
- _onCellLayout = (e: LayoutEvent, cellKey: string, index: number): void => {
1226
- const layout = e.nativeEvent.layout;
1227
- const next = {
1228
- offset: this._selectOffset(layout),
1229
- length: this._selectLength(layout),
1230
- index,
1231
- inLayout: true,
1232
- };
1233
- const curr = this._frames[cellKey];
1234
- if (
1235
- !curr ||
1236
- next.offset !== curr.offset ||
1237
- next.length !== curr.length ||
1238
- index !== curr.index
1239
- ) {
1240
- this._totalCellLength += next.length - (curr ? curr.length : 0);
1241
- this._totalCellsMeasured += curr ? 0 : 1;
1242
- this._averageCellLength =
1243
- this._totalCellLength / this._totalCellsMeasured;
1244
- this._frames[cellKey] = next;
1245
- this._highestMeasuredFrameIndex = Math.max(
1246
- this._highestMeasuredFrameIndex,
1247
- index,
1248
- );
1293
+ _onCellLayout = (
1294
+ e: LayoutEvent,
1295
+ cellKey: string,
1296
+ cellIndex: number,
1297
+ ): void => {
1298
+ const layoutHasChanged = this._listMetrics.notifyCellLayout({
1299
+ cellIndex,
1300
+ cellKey,
1301
+ layout: e.nativeEvent.layout,
1302
+ orientation: this._orientation(),
1303
+ });
1304
+
1305
+ if (layoutHasChanged) {
1249
1306
  this._scheduleCellsToRenderUpdate();
1250
- } else {
1251
- this._frames[cellKey].inLayout = true;
1252
1307
  }
1253
1308
 
1254
1309
  this._triggerRemeasureForChildListsInCell(cellKey);
1255
-
1256
1310
  this._computeBlankness();
1257
1311
  this._updateViewableItems(this.props, this.state.cellsAroundViewport);
1258
1312
  };
@@ -1264,10 +1318,7 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1264
1318
 
1265
1319
  _onCellUnmount = (cellKey: string) => {
1266
1320
  delete this._cellRefs[cellKey];
1267
- const curr = this._frames[cellKey];
1268
- if (curr) {
1269
- this._frames[cellKey] = {...curr, inLayout: false};
1270
- }
1321
+ this._listMetrics.notifyCellUnmounted(cellKey);
1271
1322
  };
1272
1323
 
1273
1324
  _triggerRemeasureForChildListsInCell(cellKey: string): void {
@@ -1289,9 +1340,9 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1289
1340
  this.context.getOutermostParentListRef().getScrollRef(),
1290
1341
  (x, y, width, height) => {
1291
1342
  this._offsetFromParentVirtualizedList = this._selectOffset({x, y});
1292
- this._scrollMetrics.contentLength = this._selectLength({
1293
- width,
1294
- height,
1343
+ this._listMetrics.notifyListContentLayout({
1344
+ layout: {width, height},
1345
+ orientation: this._orientation(),
1295
1346
  });
1296
1347
  const scrollMetrics = this._convertParentScrollMetrics(
1297
1348
  this.context.getScrollMetrics(),
@@ -1363,23 +1414,20 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1363
1414
  _renderDebugOverlay() {
1364
1415
  const normalize =
1365
1416
  this._scrollMetrics.visibleLength /
1366
- (this._scrollMetrics.contentLength || 1);
1417
+ (this._listMetrics.getContentLength() || 1);
1367
1418
  const framesInLayout = [];
1368
1419
  const itemCount = this.props.getItemCount(this.props.data);
1369
1420
  for (let ii = 0; ii < itemCount; ii++) {
1370
- const frame = this.__getFrameMetricsApprox(ii, this.props);
1371
- /* $FlowFixMe[prop-missing] (>=0.68.0 site=react_native_fb) This comment
1372
- * suppresses an error found when Flow v0.68 was deployed. To see the
1373
- * error delete this comment and run Flow. */
1374
- if (frame.inLayout) {
1421
+ const frame = this._listMetrics.getCellMetricsApprox(ii, this.props);
1422
+ if (frame.isMounted) {
1375
1423
  framesInLayout.push(frame);
1376
1424
  }
1377
1425
  }
1378
- const windowTop = this.__getFrameMetricsApprox(
1426
+ const windowTop = this._listMetrics.getCellMetricsApprox(
1379
1427
  this.state.cellsAroundViewport.first,
1380
1428
  this.props,
1381
1429
  ).offset;
1382
- const frameLast = this.__getFrameMetricsApprox(
1430
+ const frameLast = this._listMetrics.getCellMetricsApprox(
1383
1431
  this.state.cellsAroundViewport.last,
1384
1432
  this.props,
1385
1433
  );
@@ -1438,14 +1486,15 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1438
1486
  : metrics.width;
1439
1487
  }
1440
1488
 
1441
- _selectOffset(
1442
- metrics: $ReadOnly<{
1443
- x: number,
1444
- y: number,
1445
- ...
1446
- }>,
1447
- ): number {
1448
- return !horizontalOrDefault(this.props.horizontal) ? metrics.y : metrics.x;
1489
+ _selectOffset({x, y}: $ReadOnly<{x: number, y: number, ...}>): number {
1490
+ return this._orientation().horizontal ? x : y;
1491
+ }
1492
+
1493
+ _orientation(): ListOrientation {
1494
+ return {
1495
+ horizontal: horizontalOrDefault(this.props.horizontal),
1496
+ rtl: I18nManager.isRTL,
1497
+ };
1449
1498
  }
1450
1499
 
1451
1500
  _maybeCallOnEdgeReached() {
@@ -1456,11 +1505,17 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1456
1505
  onStartReachedThreshold,
1457
1506
  onEndReached,
1458
1507
  onEndReachedThreshold,
1459
- initialScrollIndex,
1460
1508
  } = this.props;
1461
- const {contentLength, visibleLength, offset} = this._scrollMetrics;
1509
+ // If we have any pending scroll updates it means that the scroll metrics
1510
+ // are out of date and we should not call any of the edge reached callbacks.
1511
+ if (this.state.pendingScrollUpdateCount > 0) {
1512
+ return;
1513
+ }
1514
+
1515
+ const {visibleLength, offset} = this._scrollMetrics;
1462
1516
  let distanceFromStart = offset;
1463
- let distanceFromEnd = contentLength - visibleLength - offset;
1517
+ let distanceFromEnd =
1518
+ this._listMetrics.getContentLength() - visibleLength - offset;
1464
1519
 
1465
1520
  // Especially when oERT is zero it's necessary to 'floor' very small distance values to be 0
1466
1521
  // since debouncing causes us to not fire this event for every single "pixel" we scroll and can thus
@@ -1494,9 +1549,9 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1494
1549
  onEndReached &&
1495
1550
  this.state.cellsAroundViewport.last === getItemCount(data) - 1 &&
1496
1551
  isWithinEndThreshold &&
1497
- this._scrollMetrics.contentLength !== this._sentEndForContentLength
1552
+ this._listMetrics.getContentLength() !== this._sentEndForContentLength
1498
1553
  ) {
1499
- this._sentEndForContentLength = this._scrollMetrics.contentLength;
1554
+ this._sentEndForContentLength = this._listMetrics.getContentLength();
1500
1555
  onEndReached({distanceFromEnd});
1501
1556
  }
1502
1557
 
@@ -1507,16 +1562,10 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1507
1562
  onStartReached != null &&
1508
1563
  this.state.cellsAroundViewport.first === 0 &&
1509
1564
  isWithinStartThreshold &&
1510
- this._scrollMetrics.contentLength !== this._sentStartForContentLength
1565
+ this._listMetrics.getContentLength() !== this._sentStartForContentLength
1511
1566
  ) {
1512
- // On initial mount when using initialScrollIndex the offset will be 0 initially
1513
- // and will trigger an unexpected onStartReached. To avoid this we can use
1514
- // timestamp to differentiate between the initial scroll metrics and when we actually
1515
- // received the first scroll event.
1516
- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) {
1517
- this._sentStartForContentLength = this._scrollMetrics.contentLength;
1518
- onStartReached({distanceFromStart});
1519
- }
1567
+ this._sentStartForContentLength = this._listMetrics.getContentLength();
1568
+ onStartReached({distanceFromStart});
1520
1569
  }
1521
1570
 
1522
1571
  // If the user scrolls away from the start or end and back again,
@@ -1532,9 +1581,32 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1532
1581
  }
1533
1582
 
1534
1583
  _onContentSizeChange = (width: number, height: number) => {
1584
+ this._listMetrics.notifyListContentLayout({
1585
+ layout: {width, height},
1586
+ orientation: this._orientation(),
1587
+ });
1588
+
1589
+ this._maybeScrollToInitialScrollIndex(width, height);
1590
+
1591
+ if (this.props.onContentSizeChange) {
1592
+ this.props.onContentSizeChange(width, height);
1593
+ }
1594
+ this._scheduleCellsToRenderUpdate();
1595
+ this._maybeCallOnEdgeReached();
1596
+ };
1597
+
1598
+ /**
1599
+ * Scroll to a specified `initialScrollIndex` prop after the ScrollView
1600
+ * content has been laid out, if it is still valid. Only a single scroll is
1601
+ * triggered throughout the lifetime of the list.
1602
+ */
1603
+ _maybeScrollToInitialScrollIndex(
1604
+ contentWidth: number,
1605
+ contentHeight: number,
1606
+ ) {
1535
1607
  if (
1536
- width > 0 &&
1537
- height > 0 &&
1608
+ contentWidth > 0 &&
1609
+ contentHeight > 0 &&
1538
1610
  this.props.initialScrollIndex != null &&
1539
1611
  this.props.initialScrollIndex > 0 &&
1540
1612
  !this._hasTriggeredInitialScrollToIndex
@@ -1554,13 +1626,7 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1554
1626
  }
1555
1627
  this._hasTriggeredInitialScrollToIndex = true;
1556
1628
  }
1557
- if (this.props.onContentSizeChange) {
1558
- this.props.onContentSizeChange(width, height);
1559
- }
1560
- this._scrollMetrics.contentLength = this._selectLength({height, width});
1561
- this._scheduleCellsToRenderUpdate();
1562
- this._maybeCallOnEdgeReached();
1563
- };
1629
+ }
1564
1630
 
1565
1631
  /* Translates metrics from a scroll event in a parent VirtualizedList into
1566
1632
  * coordinates relative to the child list.
@@ -1575,7 +1641,7 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1575
1641
  // Child's visible length is the same as its parent's
1576
1642
  const visibleLength = metrics.visibleLength;
1577
1643
  const dOffset = offset - this._scrollMetrics.offset;
1578
- const contentLength = this._scrollMetrics.contentLength;
1644
+ const contentLength = this._listMetrics.getContentLength();
1579
1645
 
1580
1646
  return {
1581
1647
  visibleLength,
@@ -1595,11 +1661,11 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1595
1661
  const timestamp = e.timeStamp;
1596
1662
  let visibleLength = this._selectLength(e.nativeEvent.layoutMeasurement);
1597
1663
  let contentLength = this._selectLength(e.nativeEvent.contentSize);
1598
- let offset = this._selectOffset(e.nativeEvent.contentOffset);
1664
+ let offset = this._offsetFromScrollEvent(e);
1599
1665
  let dOffset = offset - this._scrollMetrics.offset;
1600
1666
 
1601
1667
  if (this._isNestedWithSameOrientation()) {
1602
- if (this._scrollMetrics.contentLength === 0) {
1668
+ if (this._listMetrics.getContentLength() === 0) {
1603
1669
  // Ignore scroll events until onLayout has been called and we
1604
1670
  // know our offset from our offset from our parent
1605
1671
  return;
@@ -1634,7 +1700,6 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1634
1700
  // For invalid negative values (w/ RTL), set this to 1.
1635
1701
  const zoomScale = e.nativeEvent.zoomScale < 0 ? 1 : e.nativeEvent.zoomScale;
1636
1702
  this._scrollMetrics = {
1637
- contentLength,
1638
1703
  dt,
1639
1704
  dOffset,
1640
1705
  offset,
@@ -1643,6 +1708,11 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1643
1708
  visibleLength,
1644
1709
  zoomScale,
1645
1710
  };
1711
+ if (this.state.pendingScrollUpdateCount > 0) {
1712
+ this.setState(state => ({
1713
+ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1,
1714
+ }));
1715
+ }
1646
1716
  this._updateViewableItems(this.props, this.state.cellsAroundViewport);
1647
1717
  if (!this.props) {
1648
1718
  return;
@@ -1655,7 +1725,45 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1655
1725
  this._scheduleCellsToRenderUpdate();
1656
1726
  };
1657
1727
 
1728
+ _offsetFromScrollEvent(e: ScrollEvent): number {
1729
+ const {contentOffset, contentSize, layoutMeasurement} = e.nativeEvent;
1730
+ const {horizontal, rtl} = this._orientation();
1731
+ if (horizontal && rtl) {
1732
+ return (
1733
+ this._selectLength(contentSize) -
1734
+ (this._selectOffset(contentOffset) +
1735
+ this._selectLength(layoutMeasurement))
1736
+ );
1737
+ } else {
1738
+ return this._selectOffset(contentOffset);
1739
+ }
1740
+ }
1741
+
1658
1742
  _scheduleCellsToRenderUpdate() {
1743
+ // Only trigger high-priority updates if we've actually rendered cells,
1744
+ // and with that size estimate, accurately compute how many cells we should render.
1745
+ // Otherwise, it would just render as many cells as it can (of zero dimension),
1746
+ // each time through attempting to render more (limited by maxToRenderPerBatch),
1747
+ // starving the renderer from actually laying out the objects and computing _averageCellLength.
1748
+ // If this is triggered in an `componentDidUpdate` followed by a hiPri cellToRenderUpdate
1749
+ // We shouldn't do another hipri cellToRenderUpdate
1750
+ if (
1751
+ this._shouldRenderWithPriority() &&
1752
+ (this._listMetrics.getAverageCellLength() || this.props.getItemLayout) &&
1753
+ !this._hiPriInProgress
1754
+ ) {
1755
+ this._hiPriInProgress = true;
1756
+ // Don't worry about interactions when scrolling quickly; focus on filling content as fast
1757
+ // as possible.
1758
+ this._updateCellsToRenderBatcher.dispose({abort: true});
1759
+ this._updateCellsToRender();
1760
+ return;
1761
+ } else {
1762
+ this._updateCellsToRenderBatcher.schedule();
1763
+ }
1764
+ }
1765
+
1766
+ _shouldRenderWithPriority(): boolean {
1659
1767
  const {first, last} = this.state.cellsAroundViewport;
1660
1768
  const {offset, visibleLength, velocity} = this._scrollMetrics;
1661
1769
  const itemCount = this.props.getItemCount(this.props.data);
@@ -1670,7 +1778,8 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1670
1778
  // But only if there are items before the first rendered item
1671
1779
  if (first > 0) {
1672
1780
  const distTop =
1673
- offset - this.__getFrameMetricsApprox(first, this.props).offset;
1781
+ offset -
1782
+ this._listMetrics.getCellMetricsApprox(first, this.props).offset;
1674
1783
  hiPri =
1675
1784
  distTop < 0 ||
1676
1785
  (velocity < -2 &&
@@ -1681,7 +1790,7 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1681
1790
  // But only if there are items after the last rendered item
1682
1791
  if (!hiPri && last >= 0 && last < itemCount - 1) {
1683
1792
  const distBottom =
1684
- this.__getFrameMetricsApprox(last, this.props).offset -
1793
+ this._listMetrics.getCellMetricsApprox(last, this.props).offset -
1685
1794
  (offset + visibleLength);
1686
1795
  hiPri =
1687
1796
  distBottom < 0 ||
@@ -1689,27 +1798,8 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1689
1798
  distBottom <
1690
1799
  getScrollingThreshold(onEndReachedThreshold, visibleLength));
1691
1800
  }
1692
- // Only trigger high-priority updates if we've actually rendered cells,
1693
- // and with that size estimate, accurately compute how many cells we should render.
1694
- // Otherwise, it would just render as many cells as it can (of zero dimension),
1695
- // each time through attempting to render more (limited by maxToRenderPerBatch),
1696
- // starving the renderer from actually laying out the objects and computing _averageCellLength.
1697
- // If this is triggered in an `componentDidUpdate` followed by a hiPri cellToRenderUpdate
1698
- // We shouldn't do another hipri cellToRenderUpdate
1699
- if (
1700
- hiPri &&
1701
- (this._averageCellLength || this.props.getItemLayout) &&
1702
- !this._hiPriInProgress
1703
- ) {
1704
- this._hiPriInProgress = true;
1705
- // Don't worry about interactions when scrolling quickly; focus on filling content as fast
1706
- // as possible.
1707
- this._updateCellsToRenderBatcher.dispose({abort: true});
1708
- this._updateCellsToRender();
1709
- return;
1710
- } else {
1711
- this._updateCellsToRenderBatcher.schedule();
1712
- }
1801
+
1802
+ return hiPri;
1713
1803
  }
1714
1804
 
1715
1805
  _onScrollBeginDrag = (e: ScrollEvent): void => {
@@ -1758,6 +1848,7 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1758
1848
  const cellsAroundViewport = this._adjustCellsAroundViewport(
1759
1849
  props,
1760
1850
  state.cellsAroundViewport,
1851
+ state.pendingScrollUpdateCount,
1761
1852
  );
1762
1853
  const renderMask = VirtualizedList._createRenderMask(
1763
1854
  props,
@@ -1780,7 +1871,7 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1780
1871
  _createViewToken = (
1781
1872
  index: number,
1782
1873
  isViewable: boolean,
1783
- props: FrameMetricProps,
1874
+ props: CellMetricProps,
1784
1875
  // $FlowFixMe[missing-local-annot]
1785
1876
  ) => {
1786
1877
  const {data, getItem} = props;
@@ -1788,87 +1879,17 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1788
1879
  return {
1789
1880
  index,
1790
1881
  item,
1791
- key: this._keyExtractor(item, index, props),
1882
+ key: VirtualizedList._keyExtractor(item, index, props),
1792
1883
  isViewable,
1793
1884
  };
1794
1885
  };
1795
1886
 
1796
- /**
1797
- * Gets an approximate offset to an item at a given index. Supports
1798
- * fractional indices.
1799
- */
1800
- _getOffsetApprox = (index: number, props: FrameMetricProps): number => {
1801
- if (Number.isInteger(index)) {
1802
- return this.__getFrameMetricsApprox(index, props).offset;
1803
- } else {
1804
- const frameMetrics = this.__getFrameMetricsApprox(
1805
- Math.floor(index),
1806
- props,
1807
- );
1808
- const remainder = index - Math.floor(index);
1809
- return frameMetrics.offset + remainder * frameMetrics.length;
1810
- }
1811
- };
1812
-
1813
- __getFrameMetricsApprox: (
1814
- index: number,
1815
- props: FrameMetricProps,
1816
- ) => {
1817
- length: number,
1818
- offset: number,
1819
- ...
1820
- } = (index, props) => {
1821
- const frame = this._getFrameMetrics(index, props);
1822
- if (frame && frame.index === index) {
1823
- // check for invalid frames due to row re-ordering
1824
- return frame;
1825
- } else {
1826
- const {data, getItemCount, getItemLayout} = props;
1827
- invariant(
1828
- index >= 0 && index < getItemCount(data),
1829
- 'Tried to get frame for out of range index ' + index,
1830
- );
1831
- invariant(
1832
- !getItemLayout,
1833
- 'Should not have to estimate frames when a measurement metrics function is provided',
1834
- );
1835
- return {
1836
- length: this._averageCellLength,
1837
- offset: this._averageCellLength * index,
1838
- };
1839
- }
1840
- };
1841
-
1842
- _getFrameMetrics = (
1843
- index: number,
1844
- props: FrameMetricProps,
1845
- ): ?{
1846
- length: number,
1847
- offset: number,
1848
- index: number,
1849
- inLayout?: boolean,
1850
- ...
1851
- } => {
1852
- const {data, getItem, getItemCount, getItemLayout} = props;
1853
- invariant(
1854
- index >= 0 && index < getItemCount(data),
1855
- 'Tried to get frame for out of range index ' + index,
1856
- );
1857
- const item = getItem(data, index);
1858
- const frame = this._frames[this._keyExtractor(item, index, props)];
1859
- if (!frame || frame.index !== index) {
1860
- if (getItemLayout) {
1861
- /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment
1862
- * suppresses an error found when Flow v0.63 was deployed. To see the error
1863
- * delete this comment and run Flow. */
1864
- return getItemLayout(data, index);
1865
- }
1866
- }
1867
- return frame;
1868
- };
1887
+ __getListMetrics(): ListMetricsAggregator {
1888
+ return this._listMetrics;
1889
+ }
1869
1890
 
1870
1891
  _getNonViewportRenderRegions = (
1871
- props: FrameMetricProps,
1892
+ props: CellMetricProps,
1872
1893
  ): $ReadOnlyArray<{
1873
1894
  first: number,
1874
1895
  last: number,
@@ -1890,11 +1911,8 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1890
1911
  // where it is.
1891
1912
  if (
1892
1913
  focusedCellIndex >= itemCount ||
1893
- this._keyExtractor(
1894
- props.getItem(props.data, focusedCellIndex),
1895
- focusedCellIndex,
1896
- props,
1897
- ) !== this._lastFocusedCellKey
1914
+ VirtualizedList._getItemKey(props, focusedCellIndex) !==
1915
+ this._lastFocusedCellKey
1898
1916
  ) {
1899
1917
  return [];
1900
1918
  }
@@ -1907,7 +1925,7 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1907
1925
  i--
1908
1926
  ) {
1909
1927
  first--;
1910
- heightOfCellsBeforeFocused += this.__getFrameMetricsApprox(
1928
+ heightOfCellsBeforeFocused += this._listMetrics.getCellMetricsApprox(
1911
1929
  i,
1912
1930
  props,
1913
1931
  ).length;
@@ -1922,7 +1940,7 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1922
1940
  i++
1923
1941
  ) {
1924
1942
  last++;
1925
- heightOfCellsAfterFocused += this.__getFrameMetricsApprox(
1943
+ heightOfCellsAfterFocused += this._listMetrics.getCellMetricsApprox(
1926
1944
  i,
1927
1945
  props,
1928
1946
  ).length;
@@ -1932,15 +1950,20 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1932
1950
  };
1933
1951
 
1934
1952
  _updateViewableItems(
1935
- props: FrameMetricProps,
1953
+ props: CellMetricProps,
1936
1954
  cellsAroundViewport: {first: number, last: number},
1937
1955
  ) {
1956
+ // If we have any pending scroll updates it means that the scroll metrics
1957
+ // are out of date and we should not call any of the visibility callbacks.
1958
+ if (this.state.pendingScrollUpdateCount > 0) {
1959
+ return;
1960
+ }
1938
1961
  this._viewabilityTuples.forEach(tuple => {
1939
1962
  tuple.viewabilityHelper.onUpdate(
1940
1963
  props,
1941
1964
  this._scrollMetrics.offset,
1942
1965
  this._scrollMetrics.visibleLength,
1943
- this._getFrameMetrics,
1966
+ this._listMetrics,
1944
1967
  this._createViewToken,
1945
1968
  tuple.onViewableItemsChanged,
1946
1969
  cellsAroundViewport,
@@ -1950,9 +1973,10 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1950
1973
  }
1951
1974
 
1952
1975
  const styles = StyleSheet.create({
1953
- verticallyInverted: {
1954
- transform: [{scaleY: -1}],
1955
- },
1976
+ verticallyInverted:
1977
+ Platform.OS === 'android'
1978
+ ? {transform: [{scale: -1}]}
1979
+ : {transform: [{scaleY: -1}]},
1956
1980
  horizontallyInverted: {
1957
1981
  transform: [{scaleX: -1}],
1958
1982
  },