@react-native-macos/virtualized-lists 0.76.10

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.
@@ -0,0 +1,2153 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ * @format
9
+ */
10
+
11
+ import type {CellMetricProps, ListOrientation} from './ListMetricsAggregator';
12
+ import type {ViewToken} from './ViewabilityHelper';
13
+ import type {
14
+ Item,
15
+ Props,
16
+ RenderItemProps,
17
+ RenderItemType,
18
+ Separators,
19
+ } from './VirtualizedListProps';
20
+ import type {ScrollResponderType} from 'react-native/Libraries/Components/ScrollView/ScrollView';
21
+ import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet';
22
+ import type {
23
+ LayoutEvent,
24
+ ScrollEvent,
25
+ } from 'react-native/Libraries/Types/CoreEventTypes';
26
+ import type {KeyEvent} from 'react-native/Libraries/Types/CoreEventTypes'; // [macOS]
27
+
28
+ import Batchinator from '../Interaction/Batchinator';
29
+ import clamp from '../Utilities/clamp';
30
+ import infoLog from '../Utilities/infoLog';
31
+ import {CellRenderMask} from './CellRenderMask';
32
+ import ChildListCollection from './ChildListCollection';
33
+ import FillRateHelper from './FillRateHelper';
34
+ import ListMetricsAggregator from './ListMetricsAggregator';
35
+ import StateSafePureComponent from './StateSafePureComponent';
36
+ import ViewabilityHelper from './ViewabilityHelper';
37
+ import CellRenderer from './VirtualizedListCellRenderer';
38
+ import {
39
+ VirtualizedListCellContextProvider,
40
+ VirtualizedListContext,
41
+ VirtualizedListContextProvider,
42
+ } from './VirtualizedListContext.js';
43
+ import {
44
+ horizontalOrDefault,
45
+ initialNumToRenderOrDefault,
46
+ maxToRenderPerBatchOrDefault,
47
+ onEndReachedThresholdOrDefault,
48
+ onStartReachedThresholdOrDefault,
49
+ windowSizeOrDefault,
50
+ } from './VirtualizedListProps';
51
+ import {
52
+ computeWindowedRenderLimits,
53
+ keyExtractor as defaultKeyExtractor,
54
+ } from './VirtualizeUtils';
55
+ import invariant from 'invariant';
56
+ import nullthrows from 'nullthrows';
57
+ import * as React from 'react';
58
+ import {
59
+ I18nManager,
60
+ Platform,
61
+ RefreshControl,
62
+ ScrollView,
63
+ StyleSheet,
64
+ View,
65
+ findNodeHandle,
66
+ } from 'react-native';
67
+
68
+ export type {RenderItemProps, RenderItemType, Separators};
69
+
70
+ const ON_EDGE_REACHED_EPSILON = 0.001;
71
+
72
+ let _usedIndexForKey = false;
73
+ let _keylessItemComponentName: string = '';
74
+
75
+ type ViewabilityHelperCallbackTuple = {
76
+ viewabilityHelper: ViewabilityHelper,
77
+ onViewableItemsChanged: (info: {
78
+ viewableItems: Array<ViewToken>,
79
+ changed: Array<ViewToken>,
80
+ ...
81
+ }) => void,
82
+ ...
83
+ };
84
+
85
+ type State = {
86
+ renderMask: CellRenderMask,
87
+ cellsAroundViewport: {first: number, last: number},
88
+ selectedRowIndex: number, // [macOS]
89
+ // Used to track items added at the start of the list for maintainVisibleContentPosition.
90
+ firstVisibleItemKey: ?string,
91
+ // When > 0 the scroll position available in JS is considered stale and should not be used.
92
+ pendingScrollUpdateCount: number,
93
+ };
94
+
95
+ function getScrollingThreshold(threshold: number, visibleLength: number) {
96
+ return (threshold * visibleLength) / 2;
97
+ }
98
+
99
+ /**
100
+ * Base implementation for the more convenient [`<FlatList>`](https://reactnative.dev/docs/flatlist)
101
+ * and [`<SectionList>`](https://reactnative.dev/docs/sectionlist) components, which are also better
102
+ * documented. In general, this should only really be used if you need more flexibility than
103
+ * `FlatList` provides, e.g. for use with immutable data instead of plain arrays.
104
+ *
105
+ * Virtualization massively improves memory consumption and performance of large lists by
106
+ * maintaining a finite render window of active items and replacing all items outside of the render
107
+ * window with appropriately sized blank space. The window adapts to scrolling behavior, and items
108
+ * are rendered incrementally with low-pri (after any running interactions) if they are far from the
109
+ * visible area, or with hi-pri otherwise to minimize the potential of seeing blank space.
110
+ *
111
+ * Some caveats:
112
+ *
113
+ * - Internal state is not preserved when content scrolls out of the render window. Make sure all
114
+ * your data is captured in the item data or external stores like Flux, Redux, or Relay.
115
+ * - This is a `PureComponent` which means that it will not re-render if `props` remain shallow-
116
+ * equal. Make sure that everything your `renderItem` function depends on is passed as a prop
117
+ * (e.g. `extraData`) that is not `===` after updates, otherwise your UI may not update on
118
+ * changes. This includes the `data` prop and parent component state.
119
+ * - In order to constrain memory and enable smooth scrolling, content is rendered asynchronously
120
+ * offscreen. This means it's possible to scroll faster than the fill rate ands momentarily see
121
+ * blank content. This is a tradeoff that can be adjusted to suit the needs of each application,
122
+ * and we are working on improving it behind the scenes.
123
+ * - By default, the list looks for a `key` or `id` prop on each item and uses that for the React key.
124
+ * Alternatively, you can provide a custom `keyExtractor` prop.
125
+ * - As an effort to remove defaultProps, use helper functions when referencing certain props
126
+ *
127
+ */
128
+ class VirtualizedList extends StateSafePureComponent<Props, State> {
129
+ static contextType: typeof VirtualizedListContext = VirtualizedListContext;
130
+
131
+ // scrollToEnd may be janky without getItemLayout prop
132
+ scrollToEnd(params?: ?{animated?: ?boolean, ...}) {
133
+ const animated = params ? params.animated : true;
134
+ const veryLast = this.props.getItemCount(this.props.data) - 1;
135
+ if (veryLast < 0) {
136
+ return;
137
+ }
138
+ const frame = this._listMetrics.getCellMetricsApprox(veryLast, this.props);
139
+ const offset = Math.max(
140
+ 0,
141
+ frame.offset +
142
+ frame.length +
143
+ this._footerLength -
144
+ this._scrollMetrics.visibleLength,
145
+ );
146
+
147
+ // TODO: consider using `ref.scrollToEnd` directly
148
+ this.scrollToOffset({animated, offset});
149
+ }
150
+
151
+ // scrollToIndex may be janky without getItemLayout prop
152
+ scrollToIndex(params: {
153
+ animated?: ?boolean,
154
+ index: number,
155
+ viewOffset?: number,
156
+ viewPosition?: number,
157
+ ...
158
+ }): $FlowFixMe {
159
+ const {data, getItemCount, getItemLayout, onScrollToIndexFailed} =
160
+ this.props;
161
+ const {animated, index, viewOffset, viewPosition} = params;
162
+ invariant(
163
+ index >= 0,
164
+ `scrollToIndex out of range: requested index ${index} but minimum is 0`,
165
+ );
166
+ invariant(
167
+ getItemCount(data) >= 1,
168
+ `scrollToIndex out of range: item length ${getItemCount(
169
+ data,
170
+ )} but minimum is 1`,
171
+ );
172
+ invariant(
173
+ index < getItemCount(data),
174
+ `scrollToIndex out of range: requested index ${index} is out of 0 to ${
175
+ getItemCount(data) - 1
176
+ }`,
177
+ );
178
+ if (
179
+ !getItemLayout &&
180
+ index > this._listMetrics.getHighestMeasuredCellIndex()
181
+ ) {
182
+ invariant(
183
+ !!onScrollToIndexFailed,
184
+ 'scrollToIndex should be used in conjunction with getItemLayout or onScrollToIndexFailed, ' +
185
+ 'otherwise there is no way to know the location of offscreen indices or handle failures.',
186
+ );
187
+ onScrollToIndexFailed({
188
+ averageItemLength: this._listMetrics.getAverageCellLength(),
189
+ highestMeasuredFrameIndex:
190
+ this._listMetrics.getHighestMeasuredCellIndex(),
191
+ index,
192
+ });
193
+ return;
194
+ }
195
+ const frame = this._listMetrics.getCellMetricsApprox(
196
+ Math.floor(index),
197
+ this.props,
198
+ );
199
+ const offset =
200
+ Math.max(
201
+ 0,
202
+ this._listMetrics.getCellOffsetApprox(index, this.props) -
203
+ (viewPosition || 0) *
204
+ (this._scrollMetrics.visibleLength - frame.length),
205
+ ) - (viewOffset || 0);
206
+
207
+ this.scrollToOffset({offset, animated});
208
+ }
209
+
210
+ // scrollToItem may be janky without getItemLayout prop. Required linear scan through items -
211
+ // use scrollToIndex instead if possible.
212
+ scrollToItem(params: {
213
+ animated?: ?boolean,
214
+ item: Item,
215
+ viewOffset?: number,
216
+ viewPosition?: number,
217
+ ...
218
+ }) {
219
+ const {item} = params;
220
+ const {data, getItem, getItemCount} = this.props;
221
+ const itemCount = getItemCount(data);
222
+ for (let index = 0; index < itemCount; index++) {
223
+ if (getItem(data, index) === item) {
224
+ this.scrollToIndex({...params, index});
225
+ break;
226
+ }
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Scroll to a specific content pixel offset in the list.
232
+ *
233
+ * Param `offset` expects the offset to scroll to.
234
+ * In case of `horizontal` is true, the offset is the x-value,
235
+ * in any other case the offset is the y-value.
236
+ *
237
+ * Param `animated` (`true` by default) defines whether the list
238
+ * should do an animation while scrolling.
239
+ */
240
+ scrollToOffset(params: {animated?: ?boolean, offset: number, ...}) {
241
+ const {animated, offset} = params;
242
+ const scrollRef = this._scrollRef;
243
+
244
+ if (scrollRef == null) {
245
+ return;
246
+ }
247
+
248
+ if (scrollRef.scrollTo == null) {
249
+ console.warn(
250
+ 'No scrollTo method provided. This may be because you have two nested ' +
251
+ 'VirtualizedLists with the same orientation, or because you are ' +
252
+ 'using a custom component that does not implement scrollTo.',
253
+ );
254
+ return;
255
+ }
256
+
257
+ const {horizontal, rtl} = this._orientation();
258
+ if (horizontal && rtl && !this._listMetrics.hasContentLength()) {
259
+ console.warn(
260
+ 'scrollToOffset may not be called in RTL before content is laid out',
261
+ );
262
+ return;
263
+ }
264
+
265
+ scrollRef.scrollTo({
266
+ animated,
267
+ ...this._scrollToParamsFromOffset(offset),
268
+ });
269
+ }
270
+
271
+ _scrollToParamsFromOffset(offset: number): {x?: number, y?: number} {
272
+ const {horizontal, rtl} = this._orientation();
273
+ if (horizontal && rtl) {
274
+ // Add the visible length of the scrollview so that the offset is right-aligned
275
+ const cartOffset = this._listMetrics.cartesianOffset(
276
+ offset + this._scrollMetrics.visibleLength,
277
+ );
278
+ return horizontal ? {x: cartOffset} : {y: cartOffset};
279
+ } else {
280
+ return horizontal ? {x: offset} : {y: offset};
281
+ }
282
+ }
283
+
284
+ // [macOS
285
+ ensureItemAtIndexIsVisible(rowIndex: number) {
286
+ const frame = this._listMetrics.getCellMetricsApprox(rowIndex, this.props);
287
+ const visTop = this._scrollMetrics.offset;
288
+ const visLen = this._scrollMetrics.visibleLength;
289
+ const visEnd = visTop + visLen;
290
+ const contentLength = this._listMetrics.getContentLength();
291
+ const frameEnd = frame.offset + frame.length;
292
+
293
+ if (frameEnd > visEnd) {
294
+ const newOffset = Math.min(contentLength, visTop + (frameEnd - visEnd));
295
+ this.scrollToOffset({offset: newOffset});
296
+ } else if (frame.offset < visTop) {
297
+ const newOffset = Math.min(frame.offset, visTop - frame.length);
298
+ this.scrollToOffset({offset: newOffset});
299
+ }
300
+ }
301
+
302
+ selectRowAtIndex(rowIndex: number) {
303
+ this._selectRowAtIndex(rowIndex);
304
+ }
305
+ // macOS]
306
+
307
+ recordInteraction() {
308
+ this._nestedChildLists.forEach(childList => {
309
+ childList.recordInteraction();
310
+ });
311
+ this._viewabilityTuples.forEach(t => {
312
+ t.viewabilityHelper.recordInteraction();
313
+ });
314
+ this._updateViewableItems(this.props, this.state.cellsAroundViewport);
315
+ }
316
+
317
+ flashScrollIndicators() {
318
+ if (this._scrollRef == null) {
319
+ return;
320
+ }
321
+
322
+ this._scrollRef.flashScrollIndicators();
323
+ }
324
+
325
+ /**
326
+ * Provides a handle to the underlying scroll responder.
327
+ * Note that `this._scrollRef` might not be a `ScrollView`, so we
328
+ * need to check that it responds to `getScrollResponder` before calling it.
329
+ */
330
+ getScrollResponder(): ?ScrollResponderType {
331
+ if (this._scrollRef && this._scrollRef.getScrollResponder) {
332
+ return this._scrollRef.getScrollResponder();
333
+ }
334
+ }
335
+
336
+ getScrollableNode(): ?number {
337
+ if (this._scrollRef && this._scrollRef.getScrollableNode) {
338
+ return this._scrollRef.getScrollableNode();
339
+ } else {
340
+ return findNodeHandle(this._scrollRef);
341
+ }
342
+ }
343
+
344
+ getScrollRef():
345
+ | ?React.ElementRef<typeof ScrollView>
346
+ | ?React.ElementRef<typeof View> {
347
+ if (this._scrollRef && this._scrollRef.getScrollRef) {
348
+ return this._scrollRef.getScrollRef();
349
+ } else {
350
+ return this._scrollRef;
351
+ }
352
+ }
353
+
354
+ setNativeProps(props: Object) {
355
+ if (this._scrollRef) {
356
+ this._scrollRef.setNativeProps(props);
357
+ }
358
+ }
359
+
360
+ _getCellKey(): string {
361
+ return this.context?.cellKey || 'rootList';
362
+ }
363
+
364
+ // $FlowFixMe[missing-local-annot]
365
+ _getScrollMetrics = () => {
366
+ return this._scrollMetrics;
367
+ };
368
+
369
+ hasMore(): boolean {
370
+ return this._hasMore;
371
+ }
372
+
373
+ // $FlowFixMe[missing-local-annot]
374
+ _getOutermostParentListRef = () => {
375
+ if (this._isNestedWithSameOrientation()) {
376
+ return this.context.getOutermostParentListRef();
377
+ } else {
378
+ return this;
379
+ }
380
+ };
381
+
382
+ _registerAsNestedChild = (childList: {
383
+ cellKey: string,
384
+ ref: React.ElementRef<typeof VirtualizedList>,
385
+ }): void => {
386
+ this._nestedChildLists.add(childList.ref, childList.cellKey);
387
+ if (this._hasInteracted) {
388
+ childList.ref.recordInteraction();
389
+ }
390
+ };
391
+
392
+ _unregisterAsNestedChild = (childList: {
393
+ ref: React.ElementRef<typeof VirtualizedList>,
394
+ }): void => {
395
+ this._nestedChildLists.remove(childList.ref);
396
+ };
397
+
398
+ state: State;
399
+
400
+ constructor(props: Props) {
401
+ super(props);
402
+ this._checkProps(props);
403
+
404
+ this._fillRateHelper = new FillRateHelper(this._listMetrics);
405
+ this._updateCellsToRenderBatcher = new Batchinator(
406
+ this._updateCellsToRender,
407
+ this.props.updateCellsBatchingPeriod ?? 50,
408
+ );
409
+
410
+ if (this.props.viewabilityConfigCallbackPairs) {
411
+ this._viewabilityTuples = this.props.viewabilityConfigCallbackPairs.map(
412
+ pair => ({
413
+ viewabilityHelper: new ViewabilityHelper(pair.viewabilityConfig),
414
+ onViewableItemsChanged: pair.onViewableItemsChanged,
415
+ }),
416
+ );
417
+ } else {
418
+ const {onViewableItemsChanged, viewabilityConfig} = this.props;
419
+ if (onViewableItemsChanged) {
420
+ this._viewabilityTuples.push({
421
+ viewabilityHelper: new ViewabilityHelper(viewabilityConfig),
422
+ onViewableItemsChanged: onViewableItemsChanged,
423
+ });
424
+ }
425
+ }
426
+
427
+ const initialRenderRegion = VirtualizedList._initialRenderRegion(props);
428
+
429
+ const minIndexForVisible =
430
+ this.props.maintainVisibleContentPosition?.minIndexForVisible ?? 0;
431
+
432
+ this.state = {
433
+ cellsAroundViewport: initialRenderRegion,
434
+ renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion),
435
+ selectedRowIndex: this.props.initialSelectedIndex ?? -1, // [macOS]
436
+ firstVisibleItemKey:
437
+ this.props.getItemCount(this.props.data) > minIndexForVisible
438
+ ? VirtualizedList._getItemKey(this.props, minIndexForVisible)
439
+ : null,
440
+ // When we have a non-zero initialScrollIndex, we will receive a
441
+ // scroll event later so this will prevent the window from updating
442
+ // until we get a valid offset.
443
+ pendingScrollUpdateCount:
444
+ this.props.initialScrollIndex != null &&
445
+ this.props.initialScrollIndex > 0
446
+ ? 1
447
+ : 0,
448
+ };
449
+ }
450
+
451
+ _checkProps(props: Props) {
452
+ const {onScroll, windowSize, getItemCount, data, initialScrollIndex} =
453
+ props;
454
+
455
+ invariant(
456
+ // $FlowFixMe[prop-missing]
457
+ !onScroll || !onScroll.__isNative,
458
+ 'Components based on VirtualizedList must be wrapped with Animated.createAnimatedComponent ' +
459
+ 'to support native onScroll events with useNativeDriver',
460
+ );
461
+ invariant(
462
+ windowSizeOrDefault(windowSize) > 0,
463
+ 'VirtualizedList: The windowSize prop must be present and set to a value greater than 0.',
464
+ );
465
+
466
+ invariant(
467
+ getItemCount,
468
+ 'VirtualizedList: The "getItemCount" prop must be provided',
469
+ );
470
+
471
+ const itemCount = getItemCount(data);
472
+
473
+ if (
474
+ initialScrollIndex != null &&
475
+ !this._hasTriggeredInitialScrollToIndex &&
476
+ (initialScrollIndex < 0 ||
477
+ (itemCount > 0 && initialScrollIndex >= itemCount)) &&
478
+ !this._hasWarned.initialScrollIndex
479
+ ) {
480
+ console.warn(
481
+ `initialScrollIndex "${initialScrollIndex}" is not valid (list has ${itemCount} items)`,
482
+ );
483
+ this._hasWarned.initialScrollIndex = true;
484
+ }
485
+
486
+ if (__DEV__ && !this._hasWarned.flexWrap) {
487
+ // $FlowFixMe[underconstrained-implicit-instantiation]
488
+ const flatStyles = StyleSheet.flatten(this.props.contentContainerStyle);
489
+ if (flatStyles != null && flatStyles.flexWrap === 'wrap') {
490
+ console.warn(
491
+ '`flexWrap: `wrap`` is not supported with the `VirtualizedList` components.' +
492
+ 'Consider using `numColumns` with `FlatList` instead.',
493
+ );
494
+ this._hasWarned.flexWrap = true;
495
+ }
496
+ }
497
+ }
498
+
499
+ static _findItemIndexWithKey(
500
+ props: Props,
501
+ key: string,
502
+ hint: ?number,
503
+ ): ?number {
504
+ const itemCount = props.getItemCount(props.data);
505
+ if (hint != null && hint >= 0 && hint < itemCount) {
506
+ const curKey = VirtualizedList._getItemKey(props, hint);
507
+ if (curKey === key) {
508
+ return hint;
509
+ }
510
+ }
511
+ for (let ii = 0; ii < itemCount; ii++) {
512
+ const curKey = VirtualizedList._getItemKey(props, ii);
513
+ if (curKey === key) {
514
+ return ii;
515
+ }
516
+ }
517
+ return null;
518
+ }
519
+
520
+ static _getItemKey(
521
+ props: {
522
+ data: Props['data'],
523
+ getItem: Props['getItem'],
524
+ keyExtractor: Props['keyExtractor'],
525
+ ...
526
+ },
527
+ index: number,
528
+ ): string {
529
+ const item = props.getItem(props.data, index);
530
+ return VirtualizedList._keyExtractor(item, index, props);
531
+ }
532
+
533
+ static _createRenderMask(
534
+ props: Props,
535
+ cellsAroundViewport: {first: number, last: number},
536
+ additionalRegions?: ?$ReadOnlyArray<{first: number, last: number}>,
537
+ ): CellRenderMask {
538
+ const itemCount = props.getItemCount(props.data);
539
+
540
+ invariant(
541
+ cellsAroundViewport.first >= 0 &&
542
+ cellsAroundViewport.last >= cellsAroundViewport.first - 1 &&
543
+ cellsAroundViewport.last < itemCount,
544
+ `Invalid cells around viewport "[${cellsAroundViewport.first}, ${cellsAroundViewport.last}]" was passed to VirtualizedList._createRenderMask`,
545
+ );
546
+
547
+ const renderMask = new CellRenderMask(itemCount);
548
+
549
+ if (itemCount > 0) {
550
+ const allRegions = [cellsAroundViewport, ...(additionalRegions ?? [])];
551
+ for (const region of allRegions) {
552
+ renderMask.addCells(region);
553
+ }
554
+
555
+ // The initially rendered cells are retained as part of the
556
+ // "scroll-to-top" optimization
557
+ if (props.initialScrollIndex == null || props.initialScrollIndex <= 0) {
558
+ const initialRegion = VirtualizedList._initialRenderRegion(props);
559
+ renderMask.addCells(initialRegion);
560
+ }
561
+
562
+ // The layout coordinates of sticker headers may be off-screen while the
563
+ // actual header is on-screen. Keep the most recent before the viewport
564
+ // rendered, even if its layout coordinates are not in viewport.
565
+ const stickyIndicesSet = new Set(props.stickyHeaderIndices);
566
+ VirtualizedList._ensureClosestStickyHeader(
567
+ props,
568
+ stickyIndicesSet,
569
+ renderMask,
570
+ cellsAroundViewport.first,
571
+ );
572
+ }
573
+
574
+ return renderMask;
575
+ }
576
+
577
+ static _initialRenderRegion(props: Props): {first: number, last: number} {
578
+ const itemCount = props.getItemCount(props.data);
579
+
580
+ const firstCellIndex = Math.max(
581
+ 0,
582
+ Math.min(itemCount - 1, Math.floor(props.initialScrollIndex ?? 0)),
583
+ );
584
+
585
+ const lastCellIndex =
586
+ Math.min(
587
+ itemCount,
588
+ firstCellIndex + initialNumToRenderOrDefault(props.initialNumToRender),
589
+ ) - 1;
590
+
591
+ return {
592
+ first: firstCellIndex,
593
+ last: lastCellIndex,
594
+ };
595
+ }
596
+
597
+ static _ensureClosestStickyHeader(
598
+ props: Props,
599
+ stickyIndicesSet: Set<number>,
600
+ renderMask: CellRenderMask,
601
+ cellIdx: number,
602
+ ) {
603
+ const stickyOffset = props.ListHeaderComponent ? 1 : 0;
604
+
605
+ for (let itemIdx = cellIdx - 1; itemIdx >= 0; itemIdx--) {
606
+ if (stickyIndicesSet.has(itemIdx + stickyOffset)) {
607
+ renderMask.addCells({first: itemIdx, last: itemIdx});
608
+ break;
609
+ }
610
+ }
611
+ }
612
+
613
+ _adjustCellsAroundViewport(
614
+ props: Props,
615
+ cellsAroundViewport: {first: number, last: number},
616
+ pendingScrollUpdateCount: number,
617
+ ): {first: number, last: number} {
618
+ const {data, getItemCount} = props;
619
+ const onEndReachedThreshold = onEndReachedThresholdOrDefault(
620
+ props.onEndReachedThreshold,
621
+ );
622
+ const {offset, visibleLength} = this._scrollMetrics;
623
+ const contentLength = this._listMetrics.getContentLength();
624
+ const distanceFromEnd = contentLength - visibleLength - offset;
625
+
626
+ // Wait until the scroll view metrics have been set up. And until then,
627
+ // we will trust the initialNumToRender suggestion
628
+ if (visibleLength <= 0 || contentLength <= 0) {
629
+ return cellsAroundViewport.last >= getItemCount(data)
630
+ ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props)
631
+ : cellsAroundViewport;
632
+ }
633
+
634
+ let newCellsAroundViewport: {first: number, last: number};
635
+ if (props.disableVirtualization) {
636
+ const renderAhead =
637
+ distanceFromEnd < onEndReachedThreshold * visibleLength
638
+ ? maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch)
639
+ : 0;
640
+
641
+ newCellsAroundViewport = {
642
+ first: 0,
643
+ last: Math.min(
644
+ cellsAroundViewport.last + renderAhead,
645
+ getItemCount(data) - 1,
646
+ ),
647
+ };
648
+ } else {
649
+ // If we have a pending scroll update, we should not adjust the render window as it
650
+ // might override the correct window.
651
+ if (pendingScrollUpdateCount > 0) {
652
+ return cellsAroundViewport.last >= getItemCount(data)
653
+ ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props)
654
+ : cellsAroundViewport;
655
+ }
656
+
657
+ newCellsAroundViewport = computeWindowedRenderLimits(
658
+ props,
659
+ maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch),
660
+ windowSizeOrDefault(props.windowSize),
661
+ cellsAroundViewport,
662
+ this._listMetrics,
663
+ this._scrollMetrics,
664
+ );
665
+ invariant(
666
+ newCellsAroundViewport.last < getItemCount(data),
667
+ 'computeWindowedRenderLimits() should return range in-bounds',
668
+ );
669
+ }
670
+
671
+ if (this._nestedChildLists.size() > 0) {
672
+ // If some cell in the new state has a child list in it, we should only render
673
+ // up through that item, so that we give that list a chance to render.
674
+ // Otherwise there's churn from multiple child lists mounting and un-mounting
675
+ // their items.
676
+
677
+ // Will this prevent rendering if the nested list doesn't realize the end?
678
+ const childIdx = this._findFirstChildWithMore(
679
+ newCellsAroundViewport.first,
680
+ newCellsAroundViewport.last,
681
+ );
682
+
683
+ newCellsAroundViewport.last = childIdx ?? newCellsAroundViewport.last;
684
+ }
685
+
686
+ return newCellsAroundViewport;
687
+ }
688
+
689
+ _findFirstChildWithMore(first: number, last: number): number | null {
690
+ for (let ii = first; ii <= last; ii++) {
691
+ const cellKeyForIndex = this._indicesToKeys.get(ii);
692
+ if (
693
+ cellKeyForIndex != null &&
694
+ this._nestedChildLists.anyInCell(cellKeyForIndex, childList =>
695
+ childList.hasMore(),
696
+ )
697
+ ) {
698
+ return ii;
699
+ }
700
+ }
701
+
702
+ return null;
703
+ }
704
+
705
+ componentDidMount() {
706
+ if (this._isNestedWithSameOrientation()) {
707
+ this.context.registerAsNestedChild({
708
+ ref: this,
709
+ cellKey: this.context.cellKey,
710
+ });
711
+ }
712
+ }
713
+
714
+ componentWillUnmount() {
715
+ if (this._isNestedWithSameOrientation()) {
716
+ this.context.unregisterAsNestedChild({ref: this});
717
+ }
718
+ this._updateCellsToRenderBatcher.dispose({abort: true});
719
+ this._viewabilityTuples.forEach(tuple => {
720
+ tuple.viewabilityHelper.dispose();
721
+ });
722
+ this._fillRateHelper.deactivateAndFlush();
723
+ }
724
+
725
+ static getDerivedStateFromProps(newProps: Props, prevState: State): State {
726
+ // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make
727
+ // sure we're rendering a reasonable range here.
728
+ const itemCount = newProps.getItemCount(newProps.data);
729
+ if (itemCount === prevState.renderMask.numCells()) {
730
+ return prevState;
731
+ }
732
+
733
+ let maintainVisibleContentPositionAdjustment: ?number = null;
734
+ const prevFirstVisibleItemKey = prevState.firstVisibleItemKey;
735
+ const minIndexForVisible =
736
+ newProps.maintainVisibleContentPosition?.minIndexForVisible ?? 0;
737
+ const newFirstVisibleItemKey =
738
+ newProps.getItemCount(newProps.data) > minIndexForVisible
739
+ ? VirtualizedList._getItemKey(newProps, minIndexForVisible)
740
+ : null;
741
+ if (
742
+ newProps.maintainVisibleContentPosition != null &&
743
+ prevFirstVisibleItemKey != null &&
744
+ newFirstVisibleItemKey != null
745
+ ) {
746
+ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) {
747
+ // Fast path if items were added at the start of the list.
748
+ const hint =
749
+ itemCount - prevState.renderMask.numCells() + minIndexForVisible;
750
+ const firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey(
751
+ newProps,
752
+ prevFirstVisibleItemKey,
753
+ hint,
754
+ );
755
+ maintainVisibleContentPositionAdjustment =
756
+ firstVisibleItemIndex != null
757
+ ? firstVisibleItemIndex - minIndexForVisible
758
+ : null;
759
+ } else {
760
+ maintainVisibleContentPositionAdjustment = null;
761
+ }
762
+ }
763
+
764
+ const constrainedCells = VirtualizedList._constrainToItemCount(
765
+ maintainVisibleContentPositionAdjustment != null
766
+ ? {
767
+ first:
768
+ prevState.cellsAroundViewport.first +
769
+ maintainVisibleContentPositionAdjustment,
770
+ last:
771
+ prevState.cellsAroundViewport.last +
772
+ maintainVisibleContentPositionAdjustment,
773
+ }
774
+ : prevState.cellsAroundViewport,
775
+ newProps,
776
+ );
777
+
778
+ return {
779
+ cellsAroundViewport: constrainedCells,
780
+ renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells),
781
+ // [macOS
782
+ selectedRowIndex: Math.max(
783
+ -1, // Used to indicate no row is selected
784
+ Math.min(prevState.selectedRowIndex, itemCount),
785
+ ), // macOS]
786
+ firstVisibleItemKey: newFirstVisibleItemKey,
787
+ pendingScrollUpdateCount:
788
+ maintainVisibleContentPositionAdjustment != null
789
+ ? prevState.pendingScrollUpdateCount + 1
790
+ : prevState.pendingScrollUpdateCount,
791
+ };
792
+ }
793
+
794
+ _pushCells(
795
+ cells: Array<Object>,
796
+ stickyHeaderIndices: Array<number>,
797
+ stickyIndicesFromProps: Set<number>,
798
+ first: number,
799
+ last: number,
800
+ inversionStyle: ViewStyleProp,
801
+ ) {
802
+ const {
803
+ CellRendererComponent,
804
+ ItemSeparatorComponent,
805
+ ListHeaderComponent,
806
+ ListItemComponent,
807
+ data,
808
+ debug,
809
+ getItem,
810
+ getItemCount,
811
+ getItemLayout,
812
+ horizontal,
813
+ renderItem,
814
+ } = this.props;
815
+ const stickyOffset = ListHeaderComponent ? 1 : 0;
816
+ const end = getItemCount(data) - 1;
817
+ let prevCellKey;
818
+ last = Math.min(end, last);
819
+
820
+ for (let ii = first; ii <= last; ii++) {
821
+ const item = getItem(data, ii);
822
+ const key = VirtualizedList._keyExtractor(item, ii, this.props);
823
+
824
+ this._indicesToKeys.set(ii, key);
825
+ if (stickyIndicesFromProps.has(ii + stickyOffset)) {
826
+ stickyHeaderIndices.push(cells.length);
827
+ }
828
+
829
+ const shouldListenForLayout =
830
+ getItemLayout == null || debug || this._fillRateHelper.enabled();
831
+
832
+ cells.push(
833
+ <CellRenderer
834
+ CellRendererComponent={CellRendererComponent}
835
+ ItemSeparatorComponent={ii < end ? ItemSeparatorComponent : undefined}
836
+ ListItemComponent={ListItemComponent}
837
+ cellKey={key}
838
+ horizontal={horizontal}
839
+ index={ii}
840
+ inversionStyle={inversionStyle}
841
+ item={item}
842
+ // [macOS
843
+ isSelected={
844
+ this.props.enableSelectionOnKeyPress &&
845
+ this.state.selectedRowIndex === ii
846
+ ? true
847
+ : false
848
+ } // macOS]
849
+ key={key}
850
+ prevCellKey={prevCellKey}
851
+ onUpdateSeparators={this._onUpdateSeparators}
852
+ onCellFocusCapture={this._onCellFocusCapture}
853
+ onUnmount={this._onCellUnmount}
854
+ ref={ref => {
855
+ this._cellRefs[key] = ref;
856
+ }}
857
+ renderItem={renderItem}
858
+ {...(shouldListenForLayout && {
859
+ onCellLayout: this._onCellLayout,
860
+ })}
861
+ />,
862
+ );
863
+ prevCellKey = key;
864
+ }
865
+ }
866
+
867
+ static _constrainToItemCount(
868
+ cells: {first: number, last: number},
869
+ props: Props,
870
+ ): {first: number, last: number} {
871
+ const itemCount = props.getItemCount(props.data);
872
+ const lastPossibleCellIndex = itemCount - 1;
873
+
874
+ // Constraining `last` may significantly shrink the window. Adjust `first`
875
+ // to expand the window if the new `last` results in a new window smaller
876
+ // than the number of cells rendered per batch.
877
+ const maxToRenderPerBatch = maxToRenderPerBatchOrDefault(
878
+ props.maxToRenderPerBatch,
879
+ );
880
+ const maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch);
881
+
882
+ return {
883
+ first: clamp(0, cells.first, maxFirst),
884
+ last: Math.min(lastPossibleCellIndex, cells.last),
885
+ };
886
+ }
887
+
888
+ _onUpdateSeparators = (keys: Array<?string>, newProps: Object) => {
889
+ keys.forEach(key => {
890
+ const ref = key != null && this._cellRefs[key];
891
+ ref && ref.updateSeparatorProps(newProps);
892
+ });
893
+ };
894
+
895
+ _isNestedWithSameOrientation(): boolean {
896
+ const nestedContext = this.context;
897
+ return !!(
898
+ nestedContext &&
899
+ !!nestedContext.horizontal === horizontalOrDefault(this.props.horizontal)
900
+ );
901
+ }
902
+
903
+ _getSpacerKey = (isVertical: boolean): string =>
904
+ isVertical ? 'height' : 'width';
905
+
906
+ static _keyExtractor(
907
+ item: Item,
908
+ index: number,
909
+ props: {
910
+ keyExtractor?: ?(item: Item, index: number) => string,
911
+ ...
912
+ },
913
+ ): string {
914
+ if (props.keyExtractor != null) {
915
+ return props.keyExtractor(item, index);
916
+ }
917
+
918
+ const key = defaultKeyExtractor(item, index);
919
+ if (key === String(index)) {
920
+ _usedIndexForKey = true;
921
+ if (item.type && item.type.displayName) {
922
+ _keylessItemComponentName = item.type.displayName;
923
+ }
924
+ }
925
+ return key;
926
+ }
927
+
928
+ render(): React.Node {
929
+ this._checkProps(this.props);
930
+ const {ListEmptyComponent, ListFooterComponent, ListHeaderComponent} =
931
+ this.props;
932
+ const {data, horizontal} = this.props;
933
+ // macOS natively supports inverted lists, thus not needing an inversion style
934
+ const inversionStyle =
935
+ this.props.inverted && Platform.OS !== 'macos' // [macOS]
936
+ ? horizontalOrDefault(this.props.horizontal)
937
+ ? styles.horizontallyInverted
938
+ : styles.verticallyInverted
939
+ : null;
940
+ const cells: Array<any | React.Node> = [];
941
+ const stickyIndicesFromProps = new Set(this.props.stickyHeaderIndices);
942
+ const stickyHeaderIndices = [];
943
+
944
+ // 1. Add cell for ListHeaderComponent
945
+ if (ListHeaderComponent) {
946
+ if (stickyIndicesFromProps.has(0)) {
947
+ stickyHeaderIndices.push(0);
948
+ }
949
+ const element = React.isValidElement(ListHeaderComponent) ? (
950
+ ListHeaderComponent
951
+ ) : (
952
+ // $FlowFixMe[not-a-component]
953
+ // $FlowFixMe[incompatible-type-arg]
954
+ <ListHeaderComponent />
955
+ );
956
+ cells.push(
957
+ <VirtualizedListCellContextProvider
958
+ cellKey={this._getCellKey() + '-header'}
959
+ key="$header">
960
+ <View
961
+ // We expect that header component will be a single native view so make it
962
+ // not collapsable to avoid this view being flattened and make this assumption
963
+ // no longer true.
964
+ collapsable={false}
965
+ onLayout={this._onLayoutHeader}
966
+ style={StyleSheet.compose(
967
+ inversionStyle,
968
+ this.props.ListHeaderComponentStyle,
969
+ )}>
970
+ {
971
+ // $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors
972
+ element
973
+ }
974
+ </View>
975
+ </VirtualizedListCellContextProvider>,
976
+ );
977
+ }
978
+
979
+ // 2a. Add a cell for ListEmptyComponent if applicable
980
+ const itemCount = this.props.getItemCount(data);
981
+ if (itemCount === 0 && ListEmptyComponent) {
982
+ const element: ExactReactElement_DEPRECATED<any> = ((React.isValidElement(
983
+ ListEmptyComponent,
984
+ ) ? (
985
+ ListEmptyComponent
986
+ ) : (
987
+ // $FlowFixMe[not-a-component]
988
+ // $FlowFixMe[incompatible-type-arg]
989
+ <ListEmptyComponent />
990
+ )): any);
991
+ cells.push(
992
+ <VirtualizedListCellContextProvider
993
+ cellKey={this._getCellKey() + '-empty'}
994
+ key="$empty">
995
+ {React.cloneElement(element, {
996
+ onLayout: (event: LayoutEvent) => {
997
+ this._onLayoutEmpty(event);
998
+ // $FlowFixMe[prop-missing] React.Element internal inspection
999
+ if (element.props.onLayout) {
1000
+ element.props.onLayout(event);
1001
+ }
1002
+ },
1003
+ // $FlowFixMe[prop-missing] React.Element internal inspection
1004
+ style: StyleSheet.compose(inversionStyle, element.props.style),
1005
+ })}
1006
+ </VirtualizedListCellContextProvider>,
1007
+ );
1008
+ }
1009
+
1010
+ // 2b. Add cells and spacers for each item
1011
+ if (itemCount > 0) {
1012
+ _usedIndexForKey = false;
1013
+ _keylessItemComponentName = '';
1014
+ const spacerKey = this._getSpacerKey(!horizontal);
1015
+
1016
+ const renderRegions = this.state.renderMask.enumerateRegions();
1017
+ const lastRegion = renderRegions[renderRegions.length - 1];
1018
+ const lastSpacer = lastRegion?.isSpacer ? lastRegion : null;
1019
+
1020
+ for (const section of renderRegions) {
1021
+ if (section.isSpacer) {
1022
+ // Legacy behavior is to avoid spacers when virtualization is
1023
+ // disabled (including head spacers on initial render).
1024
+ if (this.props.disableVirtualization) {
1025
+ continue;
1026
+ }
1027
+
1028
+ // Without getItemLayout, we limit our tail spacer to the _highestMeasuredFrameIndex to
1029
+ // prevent the user for hyperscrolling into un-measured area because otherwise content will
1030
+ // likely jump around as it renders in above the viewport.
1031
+ const isLastSpacer = section === lastSpacer;
1032
+ const constrainToMeasured = isLastSpacer && !this.props.getItemLayout;
1033
+ const last = constrainToMeasured
1034
+ ? clamp(
1035
+ section.first - 1,
1036
+ section.last,
1037
+ this._listMetrics.getHighestMeasuredCellIndex(),
1038
+ )
1039
+ : section.last;
1040
+
1041
+ const firstMetrics = this._listMetrics.getCellMetricsApprox(
1042
+ section.first,
1043
+ this.props,
1044
+ );
1045
+ const lastMetrics = this._listMetrics.getCellMetricsApprox(
1046
+ last,
1047
+ this.props,
1048
+ );
1049
+ const spacerSize =
1050
+ lastMetrics.offset + lastMetrics.length - firstMetrics.offset;
1051
+ cells.push(
1052
+ <View
1053
+ key={`$spacer-${section.first}`}
1054
+ style={{[spacerKey]: spacerSize}}
1055
+ />,
1056
+ );
1057
+ } else {
1058
+ this._pushCells(
1059
+ cells,
1060
+ stickyHeaderIndices,
1061
+ stickyIndicesFromProps,
1062
+ section.first,
1063
+ section.last,
1064
+ inversionStyle,
1065
+ );
1066
+ }
1067
+ }
1068
+
1069
+ if (!this._hasWarned.keys && _usedIndexForKey) {
1070
+ console.warn(
1071
+ 'VirtualizedList: missing keys for items, make sure to specify a key or id property on each ' +
1072
+ 'item or provide a custom keyExtractor.',
1073
+ _keylessItemComponentName,
1074
+ );
1075
+ this._hasWarned.keys = true;
1076
+ }
1077
+ }
1078
+
1079
+ // 3. Add cell for ListFooterComponent
1080
+ if (ListFooterComponent) {
1081
+ const element = React.isValidElement(ListFooterComponent) ? (
1082
+ ListFooterComponent
1083
+ ) : (
1084
+ // $FlowFixMe[not-a-component]
1085
+ // $FlowFixMe[incompatible-type-arg]
1086
+ <ListFooterComponent />
1087
+ );
1088
+ cells.push(
1089
+ <VirtualizedListCellContextProvider
1090
+ cellKey={this._getFooterCellKey()}
1091
+ key="$footer">
1092
+ <View
1093
+ onLayout={this._onLayoutFooter}
1094
+ style={StyleSheet.compose(
1095
+ inversionStyle,
1096
+ this.props.ListFooterComponentStyle,
1097
+ )}>
1098
+ {
1099
+ // $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors
1100
+ element
1101
+ }
1102
+ </View>
1103
+ </VirtualizedListCellContextProvider>,
1104
+ );
1105
+ }
1106
+
1107
+ // 4. Render the ScrollView
1108
+ const scrollProps = {
1109
+ ...this.props,
1110
+ onContentSizeChange: this._onContentSizeChange,
1111
+ onLayout: this._onLayout,
1112
+ onScroll: this._onScroll,
1113
+ onScrollBeginDrag: this._onScrollBeginDrag,
1114
+ onScrollEndDrag: this._onScrollEndDrag,
1115
+ onMomentumScrollBegin: this._onMomentumScrollBegin,
1116
+ onMomentumScrollEnd: this._onMomentumScrollEnd,
1117
+ // iOS/macOS requires a non-zero scrollEventThrottle to fire more than a
1118
+ // single notification while scrolling. This will otherwise no-op.
1119
+ scrollEventThrottle: this.props.scrollEventThrottle ?? 0.0001,
1120
+ invertStickyHeaders:
1121
+ this.props.invertStickyHeaders !== undefined
1122
+ ? this.props.invertStickyHeaders
1123
+ : this.props.inverted,
1124
+ stickyHeaderIndices,
1125
+ style: inversionStyle
1126
+ ? [inversionStyle, this.props.style]
1127
+ : this.props.style,
1128
+ isInvertedVirtualizedList: this.props.inverted,
1129
+ maintainVisibleContentPosition:
1130
+ this.props.maintainVisibleContentPosition != null
1131
+ ? {
1132
+ ...this.props.maintainVisibleContentPosition,
1133
+ // Adjust index to account for ListHeaderComponent.
1134
+ minIndexForVisible:
1135
+ this.props.maintainVisibleContentPosition.minIndexForVisible +
1136
+ (this.props.ListHeaderComponent ? 1 : 0),
1137
+ }
1138
+ : undefined,
1139
+ };
1140
+
1141
+ this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1;
1142
+
1143
+ const innerRet = (
1144
+ <VirtualizedListContextProvider
1145
+ value={{
1146
+ cellKey: null,
1147
+ getScrollMetrics: this._getScrollMetrics,
1148
+ horizontal: horizontalOrDefault(this.props.horizontal),
1149
+ getOutermostParentListRef: this._getOutermostParentListRef,
1150
+ registerAsNestedChild: this._registerAsNestedChild,
1151
+ unregisterAsNestedChild: this._unregisterAsNestedChild,
1152
+ }}>
1153
+ {React.cloneElement(
1154
+ (
1155
+ this.props.renderScrollComponent ||
1156
+ this._defaultRenderScrollComponent
1157
+ )(scrollProps),
1158
+ {
1159
+ ref: this._captureScrollRef,
1160
+ },
1161
+ cells,
1162
+ )}
1163
+ </VirtualizedListContextProvider>
1164
+ );
1165
+ let ret: React.Node = innerRet;
1166
+ if (__DEV__) {
1167
+ ret = (
1168
+ <ScrollView.Context.Consumer>
1169
+ {scrollContext => {
1170
+ if (
1171
+ scrollContext != null &&
1172
+ !scrollContext.horizontal ===
1173
+ !horizontalOrDefault(this.props.horizontal) &&
1174
+ !this._hasWarned.nesting &&
1175
+ this.context == null &&
1176
+ this.props.scrollEnabled !== false
1177
+ ) {
1178
+ console.error(
1179
+ 'VirtualizedLists should never be nested inside plain ScrollViews with the same ' +
1180
+ 'orientation because it can break windowing and other functionality - use another ' +
1181
+ 'VirtualizedList-backed container instead.',
1182
+ );
1183
+ this._hasWarned.nesting = true;
1184
+ }
1185
+ return innerRet;
1186
+ }}
1187
+ </ScrollView.Context.Consumer>
1188
+ );
1189
+ }
1190
+ if (this.props.debug) {
1191
+ return (
1192
+ <View style={styles.debug}>
1193
+ {ret}
1194
+ {this._renderDebugOverlay()}
1195
+ </View>
1196
+ );
1197
+ } else {
1198
+ return ret;
1199
+ }
1200
+ }
1201
+
1202
+ componentDidUpdate(prevProps: Props) {
1203
+ const {data, extraData} = this.props;
1204
+ if (data !== prevProps.data || extraData !== prevProps.extraData) {
1205
+ // clear the viewableIndices cache to also trigger
1206
+ // the onViewableItemsChanged callback with the new data
1207
+ this._viewabilityTuples.forEach(tuple => {
1208
+ tuple.viewabilityHelper.resetViewableIndices();
1209
+ });
1210
+ }
1211
+ // The `this._hiPriInProgress` is guaranteeing a hiPri cell update will only happen
1212
+ // once per fiber update. The `_scheduleCellsToRenderUpdate` will set it to true
1213
+ // if a hiPri update needs to perform. If `componentDidUpdate` is triggered with
1214
+ // `this._hiPriInProgress=true`, means it's triggered by the hiPri update. The
1215
+ // `_scheduleCellsToRenderUpdate` will check this condition and not perform
1216
+ // another hiPri update.
1217
+ const hiPriInProgress = this._hiPriInProgress;
1218
+ this._scheduleCellsToRenderUpdate();
1219
+ // Make sure setting `this._hiPriInProgress` back to false after `componentDidUpdate`
1220
+ // is triggered with `this._hiPriInProgress = true`
1221
+ if (hiPriInProgress) {
1222
+ this._hiPriInProgress = false;
1223
+ }
1224
+ }
1225
+
1226
+ _cellRefs: {[string]: null | CellRenderer<any>} = {};
1227
+ _fillRateHelper: FillRateHelper;
1228
+ _listMetrics: ListMetricsAggregator = new ListMetricsAggregator();
1229
+ _footerLength = 0;
1230
+ // Used for preventing scrollToIndex from being called multiple times for initialScrollIndex
1231
+ _hasTriggeredInitialScrollToIndex = false;
1232
+ _hasInteracted = false;
1233
+ _hasMore = false;
1234
+ _hasWarned: {[string]: boolean} = {};
1235
+ _headerLength = 0;
1236
+ _hiPriInProgress: boolean = false; // flag to prevent infinite hiPri cell limit update
1237
+ _indicesToKeys: Map<number, string> = new Map();
1238
+ _lastFocusedCellKey: ?string = null;
1239
+ _nestedChildLists: ChildListCollection<VirtualizedList> =
1240
+ new ChildListCollection();
1241
+ _offsetFromParentVirtualizedList: number = 0;
1242
+ _pendingViewabilityUpdate: boolean = false;
1243
+ _prevParentOffset: number = 0;
1244
+ _scrollMetrics: {
1245
+ dOffset: number,
1246
+ dt: number,
1247
+ offset: number,
1248
+ timestamp: number,
1249
+ velocity: number,
1250
+ visibleLength: number,
1251
+ zoomScale: number,
1252
+ } = {
1253
+ dOffset: 0,
1254
+ dt: 10,
1255
+ offset: 0,
1256
+ timestamp: 0,
1257
+ velocity: 0,
1258
+ visibleLength: 0,
1259
+ zoomScale: 1,
1260
+ };
1261
+ _scrollRef: ?React.ElementRef<any> = null;
1262
+ _sentStartForContentLength = 0;
1263
+ _sentEndForContentLength = 0;
1264
+ _updateCellsToRenderBatcher: Batchinator;
1265
+ _viewabilityTuples: Array<ViewabilityHelperCallbackTuple> = [];
1266
+
1267
+ /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
1268
+ * LTI update could not be added via codemod */
1269
+ _captureScrollRef = ref => {
1270
+ this._scrollRef = ref;
1271
+ };
1272
+
1273
+ _computeBlankness() {
1274
+ this._fillRateHelper.computeBlankness(
1275
+ this.props,
1276
+ this.state.cellsAroundViewport,
1277
+ this._scrollMetrics,
1278
+ );
1279
+ }
1280
+
1281
+ /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
1282
+ * LTI update could not be added via codemod */
1283
+ _defaultRenderScrollComponent = props => {
1284
+ // [macOS
1285
+ const preferredScrollerStyleDidChangeHandler =
1286
+ this.props.onPreferredScrollerStyleDidChange;
1287
+ const invertedDidChange = this.props.onInvertedDidChange;
1288
+
1289
+ let keyDownEvents = [
1290
+ {key: 'ArrowUp'},
1291
+ {key: 'ArrowDown'},
1292
+ {key: 'Home'},
1293
+ {key: 'End'},
1294
+ ];
1295
+
1296
+ const keyboardNavigationProps = {
1297
+ focusable: true,
1298
+ keyDownEvents: keyDownEvents,
1299
+ onKeyDown: this._handleKeyDown,
1300
+ };
1301
+ // macOS]
1302
+
1303
+ const onRefresh = props.onRefresh;
1304
+ if (this._isNestedWithSameOrientation()) {
1305
+ // Prevent VirtualizedList._onContentSizeChange from being triggered by a bubbling onContentSizeChange event.
1306
+ // This could lead to internal inconsistencies within VirtualizedList.
1307
+ const {onContentSizeChange, ...otherProps} = props;
1308
+ return <View {...otherProps} />;
1309
+ } else if (onRefresh) {
1310
+ invariant(
1311
+ typeof props.refreshing === 'boolean',
1312
+ '`refreshing` prop must be set as a boolean in order to use `onRefresh`, but got `' +
1313
+ JSON.stringify(props.refreshing ?? 'undefined') +
1314
+ '`',
1315
+ );
1316
+ return (
1317
+ // $FlowFixMe[prop-missing] Invalid prop usage
1318
+ // $FlowFixMe[incompatible-use]
1319
+ <ScrollView
1320
+ // [macOS
1321
+ {...(props.enableSelectionOnKeyPress && keyboardNavigationProps)}
1322
+ onInvertedDidChange={invertedDidChange}
1323
+ onPreferredScrollerStyleDidChange={
1324
+ preferredScrollerStyleDidChangeHandler
1325
+ }
1326
+ {...props}
1327
+ // macOS]
1328
+ refreshControl={
1329
+ props.refreshControl == null ? (
1330
+ <RefreshControl
1331
+ // $FlowFixMe[incompatible-type]
1332
+ refreshing={props.refreshing}
1333
+ onRefresh={onRefresh}
1334
+ progressViewOffset={props.progressViewOffset}
1335
+ />
1336
+ ) : (
1337
+ props.refreshControl
1338
+ )
1339
+ }
1340
+ />
1341
+ );
1342
+ } else {
1343
+ return (
1344
+ // $FlowFixMe[prop-missing] Invalid prop usage
1345
+ // $FlowFixMe[incompatible-use]
1346
+ <ScrollView
1347
+ // [macOS
1348
+ {...(props.enableSelectionOnKeyPress && keyboardNavigationProps)}
1349
+ onInvertedDidChange={invertedDidChange}
1350
+ onPreferredScrollerStyleDidChange={
1351
+ preferredScrollerStyleDidChangeHandler
1352
+ } // macOS]
1353
+ {...props}
1354
+ />
1355
+ );
1356
+ }
1357
+ };
1358
+
1359
+ _onCellLayout = (
1360
+ e: LayoutEvent,
1361
+ cellKey: string,
1362
+ cellIndex: number,
1363
+ ): void => {
1364
+ const layoutHasChanged = this._listMetrics.notifyCellLayout({
1365
+ cellIndex,
1366
+ cellKey,
1367
+ layout: e.nativeEvent.layout,
1368
+ orientation: this._orientation(),
1369
+ });
1370
+
1371
+ if (layoutHasChanged) {
1372
+ this._scheduleCellsToRenderUpdate();
1373
+ }
1374
+
1375
+ this._triggerRemeasureForChildListsInCell(cellKey);
1376
+ this._computeBlankness();
1377
+ this._updateViewableItems(this.props, this.state.cellsAroundViewport);
1378
+ };
1379
+
1380
+ _onCellFocusCapture = (cellKey: string) => {
1381
+ this._lastFocusedCellKey = cellKey;
1382
+ this._updateCellsToRender();
1383
+ };
1384
+
1385
+ _onCellUnmount = (cellKey: string) => {
1386
+ delete this._cellRefs[cellKey];
1387
+ this._listMetrics.notifyCellUnmounted(cellKey);
1388
+ };
1389
+
1390
+ _triggerRemeasureForChildListsInCell(cellKey: string): void {
1391
+ this._nestedChildLists.forEachInCell(cellKey, childList => {
1392
+ childList.measureLayoutRelativeToContainingList();
1393
+ });
1394
+ }
1395
+
1396
+ measureLayoutRelativeToContainingList(): void {
1397
+ // TODO (T35574538): findNodeHandle sometimes crashes with "Unable to find
1398
+ // node on an unmounted component" during scrolling
1399
+ try {
1400
+ if (!this._scrollRef) {
1401
+ return;
1402
+ }
1403
+ // We are assuming that getOutermostParentListRef().getScrollRef()
1404
+ // is a non-null reference to a ScrollView
1405
+ this._scrollRef.measureLayout(
1406
+ this.context.getOutermostParentListRef().getScrollRef(),
1407
+ (x, y, width, height) => {
1408
+ this._offsetFromParentVirtualizedList = this._selectOffset({x, y});
1409
+ this._listMetrics.notifyListContentLayout({
1410
+ layout: {width, height},
1411
+ orientation: this._orientation(),
1412
+ });
1413
+ const scrollMetrics = this._convertParentScrollMetrics(
1414
+ this.context.getScrollMetrics(),
1415
+ );
1416
+
1417
+ const metricsChanged =
1418
+ this._scrollMetrics.visibleLength !== scrollMetrics.visibleLength ||
1419
+ this._scrollMetrics.offset !== scrollMetrics.offset;
1420
+
1421
+ if (metricsChanged) {
1422
+ this._scrollMetrics.visibleLength = scrollMetrics.visibleLength;
1423
+ this._scrollMetrics.offset = scrollMetrics.offset;
1424
+
1425
+ // If metrics of the scrollView changed, then we triggered remeasure for child list
1426
+ // to ensure VirtualizedList has the right information.
1427
+ this._nestedChildLists.forEach(childList => {
1428
+ childList.measureLayoutRelativeToContainingList();
1429
+ });
1430
+ }
1431
+ },
1432
+ error => {
1433
+ console.warn(
1434
+ "VirtualizedList: Encountered an error while measuring a list's" +
1435
+ ' offset from its containing VirtualizedList.',
1436
+ );
1437
+ },
1438
+ );
1439
+ } catch (error) {
1440
+ console.warn(
1441
+ 'measureLayoutRelativeToContainingList threw an error',
1442
+ error.stack,
1443
+ );
1444
+ }
1445
+ }
1446
+
1447
+ _onLayout = (e: LayoutEvent) => {
1448
+ if (this._isNestedWithSameOrientation()) {
1449
+ // Need to adjust our scroll metrics to be relative to our containing
1450
+ // VirtualizedList before we can make claims about list item viewability
1451
+ this.measureLayoutRelativeToContainingList();
1452
+ } else {
1453
+ this._scrollMetrics.visibleLength = this._selectLength(
1454
+ e.nativeEvent.layout,
1455
+ );
1456
+ }
1457
+ this.props.onLayout && this.props.onLayout(e);
1458
+ this._scheduleCellsToRenderUpdate();
1459
+ this._maybeCallOnEdgeReached();
1460
+ };
1461
+
1462
+ _onLayoutEmpty = (e: LayoutEvent) => {
1463
+ this.props.onLayout && this.props.onLayout(e);
1464
+ };
1465
+
1466
+ _getFooterCellKey(): string {
1467
+ return this._getCellKey() + '-footer';
1468
+ }
1469
+
1470
+ _onLayoutFooter = (e: LayoutEvent) => {
1471
+ this._triggerRemeasureForChildListsInCell(this._getFooterCellKey());
1472
+ this._footerLength = this._selectLength(e.nativeEvent.layout);
1473
+ };
1474
+
1475
+ _onLayoutHeader = (e: LayoutEvent) => {
1476
+ this._headerLength = this._selectLength(e.nativeEvent.layout);
1477
+ };
1478
+
1479
+ // [macOS
1480
+ _selectRowAtIndex = (rowIndex: number) => {
1481
+ const prevIndex = this.state.selectedRowIndex;
1482
+ const newIndex = rowIndex;
1483
+ this.setState({selectedRowIndex: newIndex});
1484
+
1485
+ this.ensureItemAtIndexIsVisible(newIndex);
1486
+ if (prevIndex !== newIndex) {
1487
+ const item = this.props.getItem(this.props.data, newIndex);
1488
+ if (this.props.onSelectionChanged) {
1489
+ this.props.onSelectionChanged({
1490
+ previousSelection: prevIndex,
1491
+ newSelection: newIndex,
1492
+ item: item,
1493
+ });
1494
+ }
1495
+ }
1496
+ };
1497
+
1498
+ _selectRowAboveIndex = (rowIndex: number) => {
1499
+ const rowAbove = rowIndex > 0 ? rowIndex - 1 : rowIndex;
1500
+ this._selectRowAtIndex(rowAbove);
1501
+ };
1502
+
1503
+ _selectRowBelowIndex = (rowIndex: number) => {
1504
+ const rowBelow =
1505
+ rowIndex < this.state.cellsAroundViewport.last ? rowIndex + 1 : rowIndex;
1506
+ this._selectRowAtIndex(rowBelow);
1507
+ };
1508
+
1509
+ _handleKeyDown = (event: KeyEvent) => {
1510
+ if (Platform.OS === 'macos') {
1511
+ this.props.onKeyDown?.(event);
1512
+ if (event.defaultPrevented) {
1513
+ return;
1514
+ }
1515
+
1516
+ const nativeEvent = event.nativeEvent;
1517
+ const key = nativeEvent.key;
1518
+
1519
+ let selectedIndex = -1;
1520
+ if (this.state.selectedRowIndex >= 0) {
1521
+ selectedIndex = this.state.selectedRowIndex;
1522
+ }
1523
+
1524
+ if (key === 'ArrowUp') {
1525
+ if (nativeEvent.altKey) {
1526
+ // Option+Up selects the first element
1527
+ this._selectRowAtIndex(0);
1528
+ } else {
1529
+ this._selectRowAboveIndex(selectedIndex);
1530
+ }
1531
+ } else if (key === 'ArrowDown') {
1532
+ if (nativeEvent.altKey) {
1533
+ // Option+Down selects the last element
1534
+ this._selectRowAtIndex(this.state.cellsAroundViewport.last);
1535
+ } else {
1536
+ this._selectRowBelowIndex(selectedIndex);
1537
+ }
1538
+ } else if (key === 'Enter') {
1539
+ if (this.props.onSelectionEntered) {
1540
+ const item = this.props.getItem(this.props.data, selectedIndex);
1541
+ if (this.props.onSelectionEntered) {
1542
+ this.props.onSelectionEntered(item);
1543
+ }
1544
+ }
1545
+ } else if (key === 'Home') {
1546
+ this.scrollToOffset({animated: true, offset: 0});
1547
+ } else if (key === 'End') {
1548
+ this.scrollToEnd({animated: true});
1549
+ }
1550
+ }
1551
+ };
1552
+ // macOS]
1553
+
1554
+ // $FlowFixMe[missing-local-annot]
1555
+ _renderDebugOverlay() {
1556
+ const normalize =
1557
+ this._scrollMetrics.visibleLength /
1558
+ (this._listMetrics.getContentLength() || 1);
1559
+ const framesInLayout = [];
1560
+ const itemCount = this.props.getItemCount(this.props.data);
1561
+ for (let ii = 0; ii < itemCount; ii++) {
1562
+ const frame = this._listMetrics.getCellMetricsApprox(ii, this.props);
1563
+ if (frame.isMounted) {
1564
+ framesInLayout.push(frame);
1565
+ }
1566
+ }
1567
+ const windowTop = this._listMetrics.getCellMetricsApprox(
1568
+ this.state.cellsAroundViewport.first,
1569
+ this.props,
1570
+ ).offset;
1571
+ const frameLast = this._listMetrics.getCellMetricsApprox(
1572
+ this.state.cellsAroundViewport.last,
1573
+ this.props,
1574
+ );
1575
+ const windowLen = frameLast.offset + frameLast.length - windowTop;
1576
+ const visTop = this._scrollMetrics.offset;
1577
+ const visLen = this._scrollMetrics.visibleLength;
1578
+
1579
+ return (
1580
+ <View style={[styles.debugOverlayBase, styles.debugOverlay]}>
1581
+ {framesInLayout.map((f, ii) => (
1582
+ <View
1583
+ key={'f' + ii}
1584
+ style={[
1585
+ styles.debugOverlayBase,
1586
+ styles.debugOverlayFrame,
1587
+ {
1588
+ top: f.offset * normalize,
1589
+ height: f.length * normalize,
1590
+ },
1591
+ ]}
1592
+ />
1593
+ ))}
1594
+ <View
1595
+ style={[
1596
+ styles.debugOverlayBase,
1597
+ styles.debugOverlayFrameLast,
1598
+ {
1599
+ top: windowTop * normalize,
1600
+ height: windowLen * normalize,
1601
+ },
1602
+ ]}
1603
+ />
1604
+ <View
1605
+ style={[
1606
+ styles.debugOverlayBase,
1607
+ styles.debugOverlayFrameVis,
1608
+ {
1609
+ top: visTop * normalize,
1610
+ height: visLen * normalize,
1611
+ },
1612
+ ]}
1613
+ />
1614
+ </View>
1615
+ );
1616
+ }
1617
+
1618
+ _selectLength(
1619
+ metrics: $ReadOnly<{
1620
+ height: number,
1621
+ width: number,
1622
+ ...
1623
+ }>,
1624
+ ): number {
1625
+ return !horizontalOrDefault(this.props.horizontal)
1626
+ ? metrics.height
1627
+ : metrics.width;
1628
+ }
1629
+
1630
+ _selectOffset({x, y}: $ReadOnly<{x: number, y: number, ...}>): number {
1631
+ return this._orientation().horizontal ? x : y;
1632
+ }
1633
+
1634
+ _orientation(): ListOrientation {
1635
+ return {
1636
+ horizontal: horizontalOrDefault(this.props.horizontal),
1637
+ rtl: I18nManager.isRTL,
1638
+ };
1639
+ }
1640
+
1641
+ _maybeCallOnEdgeReached() {
1642
+ const {
1643
+ data,
1644
+ getItemCount,
1645
+ onStartReached,
1646
+ onStartReachedThreshold,
1647
+ onEndReached,
1648
+ onEndReachedThreshold,
1649
+ } = this.props;
1650
+ // If we have any pending scroll updates it means that the scroll metrics
1651
+ // are out of date and we should not call any of the edge reached callbacks.
1652
+ if (this.state.pendingScrollUpdateCount > 0) {
1653
+ return;
1654
+ }
1655
+
1656
+ const {visibleLength, offset} = this._scrollMetrics;
1657
+ let distanceFromStart = offset;
1658
+ let distanceFromEnd =
1659
+ this._listMetrics.getContentLength() - visibleLength - offset;
1660
+
1661
+ // Especially when oERT is zero it's necessary to 'floor' very small distance values to be 0
1662
+ // since debouncing causes us to not fire this event for every single "pixel" we scroll and can thus
1663
+ // be at the edge of the list with a distance approximating 0 but not quite there.
1664
+ if (distanceFromStart < ON_EDGE_REACHED_EPSILON) {
1665
+ distanceFromStart = 0;
1666
+ }
1667
+ if (distanceFromEnd < ON_EDGE_REACHED_EPSILON) {
1668
+ distanceFromEnd = 0;
1669
+ }
1670
+
1671
+ // TODO: T121172172 Look into why we're "defaulting" to a threshold of 2px
1672
+ // when oERT is not present (different from 2 viewports used elsewhere)
1673
+ const DEFAULT_THRESHOLD_PX = 2;
1674
+
1675
+ const startThreshold =
1676
+ onStartReachedThreshold != null
1677
+ ? onStartReachedThreshold * visibleLength
1678
+ : DEFAULT_THRESHOLD_PX;
1679
+ const endThreshold =
1680
+ onEndReachedThreshold != null
1681
+ ? onEndReachedThreshold * visibleLength
1682
+ : DEFAULT_THRESHOLD_PX;
1683
+ const isWithinStartThreshold = distanceFromStart <= startThreshold;
1684
+ const isWithinEndThreshold = distanceFromEnd <= endThreshold;
1685
+
1686
+ // First check if the user just scrolled within the end threshold
1687
+ // and call onEndReached only once for a given content length,
1688
+ // and only if onStartReached is not being executed
1689
+ if (
1690
+ onEndReached &&
1691
+ this.state.cellsAroundViewport.last === getItemCount(data) - 1 &&
1692
+ isWithinEndThreshold &&
1693
+ this._listMetrics.getContentLength() !== this._sentEndForContentLength
1694
+ ) {
1695
+ this._sentEndForContentLength = this._listMetrics.getContentLength();
1696
+ onEndReached({distanceFromEnd});
1697
+ }
1698
+
1699
+ // Next check if the user just scrolled within the start threshold
1700
+ // and call onStartReached only once for a given content length,
1701
+ // and only if onEndReached is not being executed
1702
+ if (
1703
+ onStartReached != null &&
1704
+ this.state.cellsAroundViewport.first === 0 &&
1705
+ isWithinStartThreshold &&
1706
+ this._listMetrics.getContentLength() !== this._sentStartForContentLength
1707
+ ) {
1708
+ this._sentStartForContentLength = this._listMetrics.getContentLength();
1709
+ onStartReached({distanceFromStart});
1710
+ }
1711
+
1712
+ // If the user scrolls away from the start or end and back again,
1713
+ // cause onStartReached or onEndReached to be triggered again
1714
+ if (!isWithinStartThreshold) {
1715
+ this._sentStartForContentLength = 0;
1716
+ }
1717
+ if (!isWithinEndThreshold) {
1718
+ this._sentEndForContentLength = 0;
1719
+ }
1720
+ }
1721
+
1722
+ _onContentSizeChange = (width: number, height: number) => {
1723
+ this._listMetrics.notifyListContentLayout({
1724
+ layout: {width, height},
1725
+ orientation: this._orientation(),
1726
+ });
1727
+
1728
+ this._maybeScrollToInitialScrollIndex(width, height);
1729
+
1730
+ if (this.props.onContentSizeChange) {
1731
+ this.props.onContentSizeChange(width, height);
1732
+ }
1733
+ this._scheduleCellsToRenderUpdate();
1734
+ this._maybeCallOnEdgeReached();
1735
+ };
1736
+
1737
+ /**
1738
+ * Scroll to a specified `initialScrollIndex` prop after the ScrollView
1739
+ * content has been laid out, if it is still valid. Only a single scroll is
1740
+ * triggered throughout the lifetime of the list.
1741
+ */
1742
+ _maybeScrollToInitialScrollIndex(
1743
+ contentWidth: number,
1744
+ contentHeight: number,
1745
+ ) {
1746
+ if (
1747
+ contentWidth > 0 &&
1748
+ contentHeight > 0 &&
1749
+ this.props.initialScrollIndex != null &&
1750
+ this.props.initialScrollIndex > 0 &&
1751
+ !this._hasTriggeredInitialScrollToIndex
1752
+ ) {
1753
+ if (this.props.contentOffset == null) {
1754
+ if (
1755
+ this.props.initialScrollIndex <
1756
+ this.props.getItemCount(this.props.data)
1757
+ ) {
1758
+ this.scrollToIndex({
1759
+ animated: false,
1760
+ index: nullthrows(this.props.initialScrollIndex),
1761
+ });
1762
+ } else {
1763
+ this.scrollToEnd({animated: false});
1764
+ }
1765
+ }
1766
+ this._hasTriggeredInitialScrollToIndex = true;
1767
+ }
1768
+ }
1769
+
1770
+ /* Translates metrics from a scroll event in a parent VirtualizedList into
1771
+ * coordinates relative to the child list.
1772
+ */
1773
+ _convertParentScrollMetrics = (metrics: {
1774
+ visibleLength: number,
1775
+ offset: number,
1776
+ ...
1777
+ }): $FlowFixMe => {
1778
+ // Offset of the top of the nested list relative to the top of its parent's viewport
1779
+ const offset = metrics.offset - this._offsetFromParentVirtualizedList;
1780
+ // Child's visible length is the same as its parent's
1781
+ const visibleLength = metrics.visibleLength;
1782
+ const dOffset = offset - this._scrollMetrics.offset;
1783
+ const contentLength = this._listMetrics.getContentLength();
1784
+
1785
+ return {
1786
+ visibleLength,
1787
+ contentLength,
1788
+ offset,
1789
+ dOffset,
1790
+ };
1791
+ };
1792
+
1793
+ _onScroll = (e: Object) => {
1794
+ this._nestedChildLists.forEach(childList => {
1795
+ childList._onScroll(e);
1796
+ });
1797
+ if (this.props.onScroll) {
1798
+ this.props.onScroll(e);
1799
+ }
1800
+ const timestamp = e.timeStamp;
1801
+ let visibleLength = this._selectLength(e.nativeEvent.layoutMeasurement);
1802
+ let contentLength = this._selectLength(e.nativeEvent.contentSize);
1803
+ let offset = this._offsetFromScrollEvent(e);
1804
+ let dOffset = offset - this._scrollMetrics.offset;
1805
+
1806
+ if (this._isNestedWithSameOrientation()) {
1807
+ if (this._listMetrics.getContentLength() === 0) {
1808
+ // Ignore scroll events until onLayout has been called and we
1809
+ // know our offset from our offset from our parent
1810
+ return;
1811
+ }
1812
+ ({visibleLength, contentLength, offset, dOffset} =
1813
+ this._convertParentScrollMetrics({
1814
+ visibleLength,
1815
+ offset,
1816
+ }));
1817
+ }
1818
+
1819
+ const dt = this._scrollMetrics.timestamp
1820
+ ? Math.max(1, timestamp - this._scrollMetrics.timestamp)
1821
+ : 1;
1822
+ const velocity = dOffset / dt;
1823
+
1824
+ if (
1825
+ dt > 500 &&
1826
+ this._scrollMetrics.dt > 500 &&
1827
+ contentLength > 5 * visibleLength &&
1828
+ !this._hasWarned.perf
1829
+ ) {
1830
+ infoLog(
1831
+ 'VirtualizedList: You have a large list that is slow to update - make sure your ' +
1832
+ 'renderItem function renders components that follow React performance best practices ' +
1833
+ 'like PureComponent, shouldComponentUpdate, etc.',
1834
+ {dt, prevDt: this._scrollMetrics.dt, contentLength},
1835
+ );
1836
+ this._hasWarned.perf = true;
1837
+ }
1838
+
1839
+ // For invalid negative values (w/ RTL), set this to 1.
1840
+ const zoomScale = e.nativeEvent.zoomScale < 0 ? 1 : e.nativeEvent.zoomScale;
1841
+ this._scrollMetrics = {
1842
+ dt,
1843
+ dOffset,
1844
+ offset,
1845
+ timestamp,
1846
+ velocity,
1847
+ visibleLength,
1848
+ zoomScale,
1849
+ };
1850
+ if (this.state.pendingScrollUpdateCount > 0) {
1851
+ this.setState(state => ({
1852
+ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1,
1853
+ }));
1854
+ }
1855
+ this._updateViewableItems(this.props, this.state.cellsAroundViewport);
1856
+ if (!this.props) {
1857
+ return;
1858
+ }
1859
+ this._maybeCallOnEdgeReached();
1860
+ if (velocity !== 0) {
1861
+ this._fillRateHelper.activate();
1862
+ }
1863
+ this._computeBlankness();
1864
+ this._scheduleCellsToRenderUpdate();
1865
+ };
1866
+
1867
+ _offsetFromScrollEvent(e: ScrollEvent): number {
1868
+ const {contentOffset, contentSize, layoutMeasurement} = e.nativeEvent;
1869
+ const {horizontal, rtl} = this._orientation();
1870
+ if (horizontal && rtl) {
1871
+ return (
1872
+ this._selectLength(contentSize) -
1873
+ (this._selectOffset(contentOffset) +
1874
+ this._selectLength(layoutMeasurement))
1875
+ );
1876
+ } else {
1877
+ return this._selectOffset(contentOffset);
1878
+ }
1879
+ }
1880
+
1881
+ _scheduleCellsToRenderUpdate() {
1882
+ // Only trigger high-priority updates if we've actually rendered cells,
1883
+ // and with that size estimate, accurately compute how many cells we should render.
1884
+ // Otherwise, it would just render as many cells as it can (of zero dimension),
1885
+ // each time through attempting to render more (limited by maxToRenderPerBatch),
1886
+ // starving the renderer from actually laying out the objects and computing _averageCellLength.
1887
+ // If this is triggered in an `componentDidUpdate` followed by a hiPri cellToRenderUpdate
1888
+ // We shouldn't do another hipri cellToRenderUpdate
1889
+ if (
1890
+ (this._listMetrics.getAverageCellLength() > 0 ||
1891
+ this.props.getItemLayout != null) &&
1892
+ this._shouldRenderWithPriority() &&
1893
+ !this._hiPriInProgress
1894
+ ) {
1895
+ this._hiPriInProgress = true;
1896
+ // Don't worry about interactions when scrolling quickly; focus on filling content as fast
1897
+ // as possible.
1898
+ this._updateCellsToRenderBatcher.dispose({abort: true});
1899
+ this._updateCellsToRender();
1900
+ return;
1901
+ } else {
1902
+ this._updateCellsToRenderBatcher.schedule();
1903
+ }
1904
+ }
1905
+
1906
+ _shouldRenderWithPriority(): boolean {
1907
+ const {first, last} = this.state.cellsAroundViewport;
1908
+ const {offset, visibleLength, velocity} = this._scrollMetrics;
1909
+ const itemCount = this.props.getItemCount(this.props.data);
1910
+ let hiPri = false;
1911
+ const onStartReachedThreshold = onStartReachedThresholdOrDefault(
1912
+ this.props.onStartReachedThreshold,
1913
+ );
1914
+ const onEndReachedThreshold = onEndReachedThresholdOrDefault(
1915
+ this.props.onEndReachedThreshold,
1916
+ );
1917
+ // Mark as high priority if we're close to the start of the first item
1918
+ // But only if there are items before the first rendered item
1919
+ if (first > 0) {
1920
+ const distTop =
1921
+ offset -
1922
+ this._listMetrics.getCellMetricsApprox(first, this.props).offset;
1923
+ hiPri =
1924
+ distTop < 0 ||
1925
+ (velocity < -2 &&
1926
+ distTop <
1927
+ getScrollingThreshold(onStartReachedThreshold, visibleLength));
1928
+ }
1929
+ // Mark as high priority if we're close to the end of the last item
1930
+ // But only if there are items after the last rendered item
1931
+ if (!hiPri && last >= 0 && last < itemCount - 1) {
1932
+ const distBottom =
1933
+ this._listMetrics.getCellMetricsApprox(last, this.props).offset -
1934
+ (offset + visibleLength);
1935
+ hiPri =
1936
+ distBottom < 0 ||
1937
+ (velocity > 2 &&
1938
+ distBottom <
1939
+ getScrollingThreshold(onEndReachedThreshold, visibleLength));
1940
+ }
1941
+
1942
+ return hiPri;
1943
+ }
1944
+
1945
+ _onScrollBeginDrag = (e: ScrollEvent): void => {
1946
+ this._nestedChildLists.forEach(childList => {
1947
+ childList._onScrollBeginDrag(e);
1948
+ });
1949
+ this._viewabilityTuples.forEach(tuple => {
1950
+ tuple.viewabilityHelper.recordInteraction();
1951
+ });
1952
+ this._hasInteracted = true;
1953
+ this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e);
1954
+ };
1955
+
1956
+ _onScrollEndDrag = (e: ScrollEvent): void => {
1957
+ this._nestedChildLists.forEach(childList => {
1958
+ childList._onScrollEndDrag(e);
1959
+ });
1960
+ const {velocity} = e.nativeEvent;
1961
+ if (velocity) {
1962
+ this._scrollMetrics.velocity = this._selectOffset(velocity);
1963
+ }
1964
+ this._computeBlankness();
1965
+ this.props.onScrollEndDrag && this.props.onScrollEndDrag(e);
1966
+ };
1967
+
1968
+ _onMomentumScrollBegin = (e: ScrollEvent): void => {
1969
+ this._nestedChildLists.forEach(childList => {
1970
+ childList._onMomentumScrollBegin(e);
1971
+ });
1972
+ this.props.onMomentumScrollBegin && this.props.onMomentumScrollBegin(e);
1973
+ };
1974
+
1975
+ _onMomentumScrollEnd = (e: ScrollEvent): void => {
1976
+ this._nestedChildLists.forEach(childList => {
1977
+ childList._onMomentumScrollEnd(e);
1978
+ });
1979
+ this._scrollMetrics.velocity = 0;
1980
+ this._computeBlankness();
1981
+ this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e);
1982
+ };
1983
+
1984
+ _updateCellsToRender = () => {
1985
+ this._updateViewableItems(this.props, this.state.cellsAroundViewport);
1986
+
1987
+ this.setState((state, props) => {
1988
+ const cellsAroundViewport = this._adjustCellsAroundViewport(
1989
+ props,
1990
+ state.cellsAroundViewport,
1991
+ state.pendingScrollUpdateCount,
1992
+ );
1993
+ const renderMask = VirtualizedList._createRenderMask(
1994
+ props,
1995
+ cellsAroundViewport,
1996
+ this._getNonViewportRenderRegions(props),
1997
+ );
1998
+
1999
+ if (
2000
+ cellsAroundViewport.first === state.cellsAroundViewport.first &&
2001
+ cellsAroundViewport.last === state.cellsAroundViewport.last &&
2002
+ renderMask.equals(state.renderMask)
2003
+ ) {
2004
+ return null;
2005
+ }
2006
+
2007
+ return {cellsAroundViewport, renderMask};
2008
+ });
2009
+ };
2010
+
2011
+ _createViewToken = (
2012
+ index: number,
2013
+ isViewable: boolean,
2014
+ props: CellMetricProps,
2015
+ // $FlowFixMe[missing-local-annot]
2016
+ ) => {
2017
+ const {data, getItem} = props;
2018
+ const item = getItem(data, index);
2019
+ return {
2020
+ index,
2021
+ item,
2022
+ key: VirtualizedList._keyExtractor(item, index, props),
2023
+ isViewable,
2024
+ };
2025
+ };
2026
+
2027
+ __getListMetrics(): ListMetricsAggregator {
2028
+ return this._listMetrics;
2029
+ }
2030
+
2031
+ _getNonViewportRenderRegions = (
2032
+ props: CellMetricProps,
2033
+ ): $ReadOnlyArray<{
2034
+ first: number,
2035
+ last: number,
2036
+ }> => {
2037
+ // Keep a viewport's worth of content around the last focused cell to allow
2038
+ // random navigation around it without any blanking. E.g. tabbing from one
2039
+ // focused item out of viewport to another.
2040
+ if (
2041
+ !(this._lastFocusedCellKey && this._cellRefs[this._lastFocusedCellKey])
2042
+ ) {
2043
+ return [];
2044
+ }
2045
+
2046
+ const lastFocusedCellRenderer = this._cellRefs[this._lastFocusedCellKey];
2047
+ const focusedCellIndex = lastFocusedCellRenderer.props.index;
2048
+ const itemCount = props.getItemCount(props.data);
2049
+
2050
+ // The last cell we rendered may be at a new index. Bail if we don't know
2051
+ // where it is.
2052
+ if (
2053
+ focusedCellIndex >= itemCount ||
2054
+ VirtualizedList._getItemKey(props, focusedCellIndex) !==
2055
+ this._lastFocusedCellKey
2056
+ ) {
2057
+ return [];
2058
+ }
2059
+
2060
+ let first = focusedCellIndex;
2061
+ let heightOfCellsBeforeFocused = 0;
2062
+ for (
2063
+ let i = first - 1;
2064
+ i >= 0 && heightOfCellsBeforeFocused < this._scrollMetrics.visibleLength;
2065
+ i--
2066
+ ) {
2067
+ first--;
2068
+ heightOfCellsBeforeFocused += this._listMetrics.getCellMetricsApprox(
2069
+ i,
2070
+ props,
2071
+ ).length;
2072
+ }
2073
+
2074
+ let last = focusedCellIndex;
2075
+ let heightOfCellsAfterFocused = 0;
2076
+ for (
2077
+ let i = last + 1;
2078
+ i < itemCount &&
2079
+ heightOfCellsAfterFocused < this._scrollMetrics.visibleLength;
2080
+ i++
2081
+ ) {
2082
+ last++;
2083
+ heightOfCellsAfterFocused += this._listMetrics.getCellMetricsApprox(
2084
+ i,
2085
+ props,
2086
+ ).length;
2087
+ }
2088
+
2089
+ return [{first, last}];
2090
+ };
2091
+
2092
+ _updateViewableItems(
2093
+ props: CellMetricProps,
2094
+ cellsAroundViewport: {first: number, last: number},
2095
+ ) {
2096
+ // If we have any pending scroll updates it means that the scroll metrics
2097
+ // are out of date and we should not call any of the visibility callbacks.
2098
+ if (this.state.pendingScrollUpdateCount > 0) {
2099
+ return;
2100
+ }
2101
+ this._viewabilityTuples.forEach(tuple => {
2102
+ tuple.viewabilityHelper.onUpdate(
2103
+ props,
2104
+ this._scrollMetrics.offset,
2105
+ this._scrollMetrics.visibleLength,
2106
+ this._listMetrics,
2107
+ this._createViewToken,
2108
+ tuple.onViewableItemsChanged,
2109
+ cellsAroundViewport,
2110
+ );
2111
+ });
2112
+ }
2113
+ }
2114
+
2115
+ const styles = StyleSheet.create({
2116
+ verticallyInverted:
2117
+ Platform.OS === 'android'
2118
+ ? {transform: [{scale: -1}]}
2119
+ : {transform: [{scaleY: -1}]},
2120
+ horizontallyInverted: {
2121
+ transform: [{scaleX: -1}],
2122
+ },
2123
+ debug: {
2124
+ flex: 1,
2125
+ },
2126
+ debugOverlayBase: {
2127
+ position: 'absolute',
2128
+ top: 0,
2129
+ right: 0,
2130
+ },
2131
+ debugOverlay: {
2132
+ bottom: 0,
2133
+ width: 20,
2134
+ borderColor: 'blue',
2135
+ borderWidth: 1,
2136
+ },
2137
+ debugOverlayFrame: {
2138
+ left: 0,
2139
+ backgroundColor: 'orange',
2140
+ },
2141
+ debugOverlayFrameLast: {
2142
+ left: 0,
2143
+ borderColor: 'green',
2144
+ borderWidth: 2,
2145
+ },
2146
+ debugOverlayFrameVis: {
2147
+ left: 0,
2148
+ borderColor: 'red',
2149
+ borderWidth: 2,
2150
+ },
2151
+ });
2152
+
2153
+ module.exports = VirtualizedList;