@react-native/virtualized-lists 0.73.0 → 0.73.1
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.
- package/Lists/FillRateHelper.js +12 -20
- package/Lists/ListMetricsAggregator.js +385 -0
- package/Lists/StateSafePureComponent.js +2 -2
- package/Lists/ViewabilityHelper.js +15 -27
- package/Lists/VirtualizeUtils.js +11 -24
- package/Lists/VirtualizedList.d.ts +1 -1
- package/Lists/VirtualizedList.js +390 -337
- package/Lists/VirtualizedListCellRenderer.js +1 -1
- package/Lists/VirtualizedListContext.js +1 -3
- package/Lists/VirtualizedListProps.js +40 -9
- package/Lists/VirtualizedSectionList.js +5 -5
- package/README.md +21 -0
- package/package.json +15 -5
package/Lists/FillRateHelper.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
'use strict';
|
|
12
12
|
|
|
13
|
-
import type {
|
|
13
|
+
import type {CellMetricProps} from './ListMetricsAggregator';
|
|
14
|
+
import ListMetricsAggregator from './ListMetricsAggregator';
|
|
14
15
|
|
|
15
16
|
export type FillRateInfo = Info;
|
|
16
17
|
|
|
@@ -27,13 +28,6 @@ class Info {
|
|
|
27
28
|
sample_count: number = 0;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
type FrameMetrics = {
|
|
31
|
-
inLayout?: boolean,
|
|
32
|
-
length: number,
|
|
33
|
-
offset: number,
|
|
34
|
-
...
|
|
35
|
-
};
|
|
36
|
-
|
|
37
31
|
const DEBUG = false;
|
|
38
32
|
|
|
39
33
|
let _listeners: Array<(Info) => void> = [];
|
|
@@ -51,7 +45,7 @@ let _sampleRate = DEBUG ? 1 : null;
|
|
|
51
45
|
class FillRateHelper {
|
|
52
46
|
_anyBlankStartTime: ?number = null;
|
|
53
47
|
_enabled = false;
|
|
54
|
-
|
|
48
|
+
_listMetrics: ListMetricsAggregator;
|
|
55
49
|
_info: Info = new Info();
|
|
56
50
|
_mostlyBlankStartTime: ?number = null;
|
|
57
51
|
_samplesStartTime: ?number = null;
|
|
@@ -79,10 +73,8 @@ class FillRateHelper {
|
|
|
79
73
|
_minSampleCount = minSampleCount;
|
|
80
74
|
}
|
|
81
75
|
|
|
82
|
-
constructor(
|
|
83
|
-
|
|
84
|
-
) {
|
|
85
|
-
this._getFrameMetrics = getFrameMetrics;
|
|
76
|
+
constructor(listMetrics: ListMetricsAggregator) {
|
|
77
|
+
this._listMetrics = listMetrics;
|
|
86
78
|
this._enabled = (_sampleRate || 0) > Math.random();
|
|
87
79
|
this._resetData();
|
|
88
80
|
}
|
|
@@ -139,7 +131,7 @@ class FillRateHelper {
|
|
|
139
131
|
|
|
140
132
|
computeBlankness(
|
|
141
133
|
props: {
|
|
142
|
-
...
|
|
134
|
+
...CellMetricProps,
|
|
143
135
|
initialNumToRender?: ?number,
|
|
144
136
|
...
|
|
145
137
|
},
|
|
@@ -186,12 +178,12 @@ class FillRateHelper {
|
|
|
186
178
|
|
|
187
179
|
let blankTop = 0;
|
|
188
180
|
let first = cellsAroundViewport.first;
|
|
189
|
-
let firstFrame = this.
|
|
181
|
+
let firstFrame = this._listMetrics.getCellMetrics(first, props);
|
|
190
182
|
while (
|
|
191
183
|
first <= cellsAroundViewport.last &&
|
|
192
|
-
(!firstFrame || !firstFrame.
|
|
184
|
+
(!firstFrame || !firstFrame.isMounted)
|
|
193
185
|
) {
|
|
194
|
-
firstFrame = this.
|
|
186
|
+
firstFrame = this._listMetrics.getCellMetrics(first, props);
|
|
195
187
|
first++;
|
|
196
188
|
}
|
|
197
189
|
// Only count blankTop if we aren't rendering the first item, otherwise we will count the header
|
|
@@ -204,12 +196,12 @@ class FillRateHelper {
|
|
|
204
196
|
}
|
|
205
197
|
let blankBottom = 0;
|
|
206
198
|
let last = cellsAroundViewport.last;
|
|
207
|
-
let lastFrame = this.
|
|
199
|
+
let lastFrame = this._listMetrics.getCellMetrics(last, props);
|
|
208
200
|
while (
|
|
209
201
|
last >= cellsAroundViewport.first &&
|
|
210
|
-
(!lastFrame || !lastFrame.
|
|
202
|
+
(!lastFrame || !lastFrame.isMounted)
|
|
211
203
|
) {
|
|
212
|
-
lastFrame = this.
|
|
204
|
+
lastFrame = this._listMetrics.getCellMetrics(last, props);
|
|
213
205
|
last--;
|
|
214
206
|
}
|
|
215
207
|
// Only count blankBottom if we aren't rendering the last item, otherwise we will count the
|
|
@@ -0,0 +1,385 @@
|
|
|
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 strict-local
|
|
8
|
+
* @format
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {Layout} from 'react-native/Libraries/Types/CoreEventTypes';
|
|
12
|
+
import type {Props as VirtualizedListProps} from './VirtualizedListProps';
|
|
13
|
+
import {keyExtractor as defaultKeyExtractor} from './VirtualizeUtils';
|
|
14
|
+
|
|
15
|
+
import invariant from 'invariant';
|
|
16
|
+
|
|
17
|
+
type LayoutEventDirection = 'top-down' | 'bottom-up';
|
|
18
|
+
|
|
19
|
+
export type CellMetrics = {
|
|
20
|
+
/**
|
|
21
|
+
* Index of the item in the list
|
|
22
|
+
*/
|
|
23
|
+
index: number,
|
|
24
|
+
/**
|
|
25
|
+
* Length of the cell along the scrolling axis
|
|
26
|
+
*/
|
|
27
|
+
length: number,
|
|
28
|
+
/**
|
|
29
|
+
* Distance between this cell and the start of the list along the scrolling
|
|
30
|
+
* axis
|
|
31
|
+
*/
|
|
32
|
+
offset: number,
|
|
33
|
+
/**
|
|
34
|
+
* Whether the cell is last known to be mounted
|
|
35
|
+
*/
|
|
36
|
+
isMounted: boolean,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// TODO: `inverted` can be incorporated here if it is moved to an order
|
|
40
|
+
// based implementation instead of transform.
|
|
41
|
+
export type ListOrientation = {
|
|
42
|
+
horizontal: boolean,
|
|
43
|
+
rtl: boolean,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Subset of VirtualizedList props needed to calculate cell metrics
|
|
48
|
+
*/
|
|
49
|
+
export type CellMetricProps = {
|
|
50
|
+
data: VirtualizedListProps['data'],
|
|
51
|
+
getItemCount: VirtualizedListProps['getItemCount'],
|
|
52
|
+
getItem: VirtualizedListProps['getItem'],
|
|
53
|
+
getItemLayout?: VirtualizedListProps['getItemLayout'],
|
|
54
|
+
keyExtractor?: VirtualizedListProps['keyExtractor'],
|
|
55
|
+
...
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type UnresolvedCellMetrics = {
|
|
59
|
+
index: number,
|
|
60
|
+
layout: Layout,
|
|
61
|
+
isMounted: boolean,
|
|
62
|
+
|
|
63
|
+
// The length of list content at the time of layout is needed to correctly
|
|
64
|
+
// resolve flow relative offset in RTL. We are lazily notified of this after
|
|
65
|
+
// the layout of the cell, unless the cell relayout does not cause a length
|
|
66
|
+
// change. To keep stability, we use content length at time of query, or
|
|
67
|
+
// unmount if never queried.
|
|
68
|
+
listContentLength?: ?number,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Provides an interface to query information about the metrics of a list and its cells.
|
|
73
|
+
*/
|
|
74
|
+
export default class ListMetricsAggregator {
|
|
75
|
+
_averageCellLength = 0;
|
|
76
|
+
_cellMetrics: Map<string, UnresolvedCellMetrics> = new Map();
|
|
77
|
+
_contentLength: ?number;
|
|
78
|
+
_highestMeasuredCellIndex = 0;
|
|
79
|
+
_measuredCellsLength = 0;
|
|
80
|
+
_measuredCellsCount = 0;
|
|
81
|
+
_orientation: ListOrientation = {
|
|
82
|
+
horizontal: false,
|
|
83
|
+
rtl: false,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Fabric and Paper may call onLayout in different orders. We can tell which
|
|
87
|
+
// direction layout events happen on the first layout.
|
|
88
|
+
_onLayoutDirection: LayoutEventDirection = 'top-down';
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Notify the ListMetricsAggregator that a cell has been laid out.
|
|
92
|
+
*
|
|
93
|
+
* @returns whether the cell layout has changed since last notification
|
|
94
|
+
*/
|
|
95
|
+
notifyCellLayout({
|
|
96
|
+
cellIndex,
|
|
97
|
+
cellKey,
|
|
98
|
+
orientation,
|
|
99
|
+
layout,
|
|
100
|
+
}: {
|
|
101
|
+
cellIndex: number,
|
|
102
|
+
cellKey: string,
|
|
103
|
+
orientation: ListOrientation,
|
|
104
|
+
layout: Layout,
|
|
105
|
+
}): boolean {
|
|
106
|
+
if (this._contentLength == null) {
|
|
107
|
+
this._onLayoutDirection = 'bottom-up';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this._invalidateIfOrientationChanged(orientation);
|
|
111
|
+
|
|
112
|
+
// If layout is top-down, our most recently cached content length
|
|
113
|
+
// corresponds to this cell. Otherwise, we need to resolve when events fire
|
|
114
|
+
// up the tree to the new length.
|
|
115
|
+
const listContentLength =
|
|
116
|
+
this._onLayoutDirection === 'top-down' ? this._contentLength : null;
|
|
117
|
+
|
|
118
|
+
const next: UnresolvedCellMetrics = {
|
|
119
|
+
index: cellIndex,
|
|
120
|
+
layout: layout,
|
|
121
|
+
isMounted: true,
|
|
122
|
+
listContentLength,
|
|
123
|
+
};
|
|
124
|
+
const curr = this._cellMetrics.get(cellKey);
|
|
125
|
+
|
|
126
|
+
if (
|
|
127
|
+
!curr ||
|
|
128
|
+
this._selectOffset(next.layout) !== this._selectOffset(curr.layout) ||
|
|
129
|
+
this._selectLength(next.layout) !== this._selectLength(curr.layout) ||
|
|
130
|
+
(curr.listContentLength != null &&
|
|
131
|
+
curr.listContentLength !== this._contentLength)
|
|
132
|
+
) {
|
|
133
|
+
if (curr) {
|
|
134
|
+
const dLength =
|
|
135
|
+
this._selectLength(next.layout) - this._selectLength(curr.layout);
|
|
136
|
+
this._measuredCellsLength += dLength;
|
|
137
|
+
} else {
|
|
138
|
+
this._measuredCellsLength += this._selectLength(next.layout);
|
|
139
|
+
this._measuredCellsCount += 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this._averageCellLength =
|
|
143
|
+
this._measuredCellsLength / this._measuredCellsCount;
|
|
144
|
+
this._cellMetrics.set(cellKey, next);
|
|
145
|
+
this._highestMeasuredCellIndex = Math.max(
|
|
146
|
+
this._highestMeasuredCellIndex,
|
|
147
|
+
cellIndex,
|
|
148
|
+
);
|
|
149
|
+
return true;
|
|
150
|
+
} else {
|
|
151
|
+
curr.isMounted = true;
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Notify ListMetricsAggregator that a cell has been unmounted.
|
|
158
|
+
*/
|
|
159
|
+
notifyCellUnmounted(cellKey: string): void {
|
|
160
|
+
const curr = this._cellMetrics.get(cellKey);
|
|
161
|
+
if (curr) {
|
|
162
|
+
curr.isMounted = false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Notify ListMetricsAggregator that the lists content container has been laid out.
|
|
168
|
+
*/
|
|
169
|
+
notifyListContentLayout({
|
|
170
|
+
orientation,
|
|
171
|
+
layout,
|
|
172
|
+
}: {
|
|
173
|
+
orientation: ListOrientation,
|
|
174
|
+
layout: $ReadOnly<{width: number, height: number}>,
|
|
175
|
+
}): void {
|
|
176
|
+
this._invalidateIfOrientationChanged(orientation);
|
|
177
|
+
const newLength = this._selectLength(layout);
|
|
178
|
+
|
|
179
|
+
// Fill in any just-measured cells which did not have this length available.
|
|
180
|
+
// This logic assumes that cell relayout will always change list content
|
|
181
|
+
// size, which isn't strictly correct, but issues should be rare and only
|
|
182
|
+
// on Paper.
|
|
183
|
+
if (this._onLayoutDirection === 'bottom-up') {
|
|
184
|
+
for (const cellMetric of this._cellMetrics.values()) {
|
|
185
|
+
if (cellMetric.listContentLength == null) {
|
|
186
|
+
cellMetric.listContentLength = newLength;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this._contentLength = newLength;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Return the average length of the cells which have been measured
|
|
196
|
+
*/
|
|
197
|
+
getAverageCellLength(): number {
|
|
198
|
+
return this._averageCellLength;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Return the highest measured cell index (or 0 if nothing has been measured
|
|
203
|
+
* yet)
|
|
204
|
+
*/
|
|
205
|
+
getHighestMeasuredCellIndex(): number {
|
|
206
|
+
return this._highestMeasuredCellIndex;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Returns the exact metrics of a cell if it has already been laid out,
|
|
211
|
+
* otherwise an estimate based on the average length of previously measured
|
|
212
|
+
* cells
|
|
213
|
+
*/
|
|
214
|
+
getCellMetricsApprox(index: number, props: CellMetricProps): CellMetrics {
|
|
215
|
+
const frame = this.getCellMetrics(index, props);
|
|
216
|
+
if (frame && frame.index === index) {
|
|
217
|
+
// check for invalid frames due to row re-ordering
|
|
218
|
+
return frame;
|
|
219
|
+
} else {
|
|
220
|
+
const {data, getItemCount} = props;
|
|
221
|
+
invariant(
|
|
222
|
+
index >= 0 && index < getItemCount(data),
|
|
223
|
+
'Tried to get frame for out of range index ' + index,
|
|
224
|
+
);
|
|
225
|
+
return {
|
|
226
|
+
length: this._averageCellLength,
|
|
227
|
+
offset: this._averageCellLength * index,
|
|
228
|
+
index,
|
|
229
|
+
isMounted: false,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Returns the exact metrics of a cell if it has already been laid out
|
|
236
|
+
*/
|
|
237
|
+
getCellMetrics(index: number, props: CellMetricProps): ?CellMetrics {
|
|
238
|
+
const {data, getItem, getItemCount, getItemLayout} = props;
|
|
239
|
+
invariant(
|
|
240
|
+
index >= 0 && index < getItemCount(data),
|
|
241
|
+
'Tried to get metrics for out of range cell index ' + index,
|
|
242
|
+
);
|
|
243
|
+
const keyExtractor = props.keyExtractor ?? defaultKeyExtractor;
|
|
244
|
+
const frame = this._cellMetrics.get(
|
|
245
|
+
keyExtractor(getItem(data, index), index),
|
|
246
|
+
);
|
|
247
|
+
if (frame && frame.index === index) {
|
|
248
|
+
return this._resolveCellMetrics(frame);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (getItemLayout) {
|
|
252
|
+
const {length, offset} = getItemLayout(data, index);
|
|
253
|
+
// TODO: `isMounted` is used for both "is exact layout" and "has been
|
|
254
|
+
// unmounted". Should be refactored.
|
|
255
|
+
return {index, length, offset, isMounted: true};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Gets an approximate offset to an item at a given index. Supports
|
|
263
|
+
* fractional indices.
|
|
264
|
+
*/
|
|
265
|
+
getCellOffsetApprox(index: number, props: CellMetricProps): number {
|
|
266
|
+
if (Number.isInteger(index)) {
|
|
267
|
+
return this.getCellMetricsApprox(index, props).offset;
|
|
268
|
+
} else {
|
|
269
|
+
const frameMetrics = this.getCellMetricsApprox(Math.floor(index), props);
|
|
270
|
+
const remainder = index - Math.floor(index);
|
|
271
|
+
return frameMetrics.offset + remainder * frameMetrics.length;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Returns the length of all ScrollView content along the scrolling axis.
|
|
277
|
+
*/
|
|
278
|
+
getContentLength(): number {
|
|
279
|
+
return this._contentLength ?? 0;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Whether a content length has been observed
|
|
284
|
+
*/
|
|
285
|
+
hasContentLength(): boolean {
|
|
286
|
+
return this._contentLength != null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Whether the ListMetricsAggregator is notified of cell metrics before
|
|
291
|
+
* ScrollView metrics (bottom-up) or ScrollView metrics before cell metrics
|
|
292
|
+
* (top-down).
|
|
293
|
+
*
|
|
294
|
+
* Must be queried after cell layout
|
|
295
|
+
*/
|
|
296
|
+
getLayoutEventDirection(): LayoutEventDirection {
|
|
297
|
+
return this._onLayoutDirection;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Whether the ListMetricsAggregator must be aware of the current length of
|
|
302
|
+
* ScrollView content to be able to correctly resolve the (flow-relative)
|
|
303
|
+
* metrics of a cell.
|
|
304
|
+
*/
|
|
305
|
+
needsContentLengthForCellMetrics(): boolean {
|
|
306
|
+
return this._orientation.horizontal && this._orientation.rtl;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Finds the flow-relative offset (e.g. starting from the left in LTR, but
|
|
311
|
+
* right in RTL) from a layout box.
|
|
312
|
+
*/
|
|
313
|
+
flowRelativeOffset(layout: Layout, referenceContentLength?: ?number): number {
|
|
314
|
+
const {horizontal, rtl} = this._orientation;
|
|
315
|
+
|
|
316
|
+
if (horizontal && rtl) {
|
|
317
|
+
const contentLength = referenceContentLength ?? this._contentLength;
|
|
318
|
+
invariant(
|
|
319
|
+
contentLength != null,
|
|
320
|
+
'ListMetricsAggregator must be notified of list content layout before resolving offsets',
|
|
321
|
+
);
|
|
322
|
+
return (
|
|
323
|
+
contentLength -
|
|
324
|
+
(this._selectOffset(layout) + this._selectLength(layout))
|
|
325
|
+
);
|
|
326
|
+
} else {
|
|
327
|
+
return this._selectOffset(layout);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Converts a flow-relative offset to a cartesian offset
|
|
333
|
+
*/
|
|
334
|
+
cartesianOffset(flowRelativeOffset: number): number {
|
|
335
|
+
const {horizontal, rtl} = this._orientation;
|
|
336
|
+
|
|
337
|
+
if (horizontal && rtl) {
|
|
338
|
+
invariant(
|
|
339
|
+
this._contentLength != null,
|
|
340
|
+
'ListMetricsAggregator must be notified of list content layout before resolving offsets',
|
|
341
|
+
);
|
|
342
|
+
return this._contentLength - flowRelativeOffset;
|
|
343
|
+
} else {
|
|
344
|
+
return flowRelativeOffset;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
_invalidateIfOrientationChanged(orientation: ListOrientation): void {
|
|
349
|
+
if (orientation.rtl !== this._orientation.rtl) {
|
|
350
|
+
this._cellMetrics.clear();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (orientation.horizontal !== this._orientation.horizontal) {
|
|
354
|
+
this._averageCellLength = 0;
|
|
355
|
+
this._contentLength = null;
|
|
356
|
+
this._highestMeasuredCellIndex = 0;
|
|
357
|
+
this._measuredCellsLength = 0;
|
|
358
|
+
this._measuredCellsCount = 0;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
this._orientation = orientation;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
_selectLength({
|
|
365
|
+
width,
|
|
366
|
+
height,
|
|
367
|
+
}: $ReadOnly<{width: number, height: number, ...}>): number {
|
|
368
|
+
return this._orientation.horizontal ? width : height;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
_selectOffset({x, y}: $ReadOnly<{x: number, y: number, ...}>): number {
|
|
372
|
+
return this._orientation.horizontal ? x : y;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
_resolveCellMetrics(metrics: UnresolvedCellMetrics): CellMetrics {
|
|
376
|
+
const {index, layout, isMounted, listContentLength} = metrics;
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
index,
|
|
380
|
+
length: this._selectLength(layout),
|
|
381
|
+
isMounted,
|
|
382
|
+
offset: this.flowRelativeOffset(layout, listContentLength),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
}
|
|
@@ -14,7 +14,7 @@ import * as React from 'react';
|
|
|
14
14
|
/**
|
|
15
15
|
* `setState` is called asynchronously, and should not rely on the value of
|
|
16
16
|
* `this.props` or `this.state`:
|
|
17
|
-
* https://
|
|
17
|
+
* https://react.dev/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
|
|
18
18
|
*
|
|
19
19
|
* SafePureComponent adds runtime enforcement, to catch cases where these
|
|
20
20
|
* variables are read in a state updater function, instead of the ones passed
|
|
@@ -32,7 +32,7 @@ export default class StateSafePureComponent<
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
setState(
|
|
35
|
-
partialState: ?(
|
|
35
|
+
partialState: ?(Partial<State> | ((State, Props) => ?Partial<State>)),
|
|
36
36
|
callback?: () => mixed,
|
|
37
37
|
): void {
|
|
38
38
|
if (typeof partialState === 'function') {
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
'use strict';
|
|
12
12
|
|
|
13
|
-
import type {
|
|
13
|
+
import type {CellMetricProps} from './ListMetricsAggregator';
|
|
14
|
+
import ListMetricsAggregator from './ListMetricsAggregator';
|
|
14
15
|
|
|
15
16
|
const invariant = require('invariant');
|
|
16
17
|
|
|
@@ -101,17 +102,10 @@ class ViewabilityHelper {
|
|
|
101
102
|
* Determines which items are viewable based on the current metrics and config.
|
|
102
103
|
*/
|
|
103
104
|
computeViewableItems(
|
|
104
|
-
props:
|
|
105
|
+
props: CellMetricProps,
|
|
105
106
|
scrollOffset: number,
|
|
106
107
|
viewportHeight: number,
|
|
107
|
-
|
|
108
|
-
index: number,
|
|
109
|
-
props: FrameMetricProps,
|
|
110
|
-
) => ?{
|
|
111
|
-
length: number,
|
|
112
|
-
offset: number,
|
|
113
|
-
...
|
|
114
|
-
},
|
|
108
|
+
listMetrics: ListMetricsAggregator,
|
|
115
109
|
// Optional optimization to reduce the scan size
|
|
116
110
|
renderRange?: {
|
|
117
111
|
first: number,
|
|
@@ -146,12 +140,13 @@ class ViewabilityHelper {
|
|
|
146
140
|
return [];
|
|
147
141
|
}
|
|
148
142
|
for (let idx = first; idx <= last; idx++) {
|
|
149
|
-
const metrics =
|
|
143
|
+
const metrics = listMetrics.getCellMetrics(idx, props);
|
|
150
144
|
if (!metrics) {
|
|
151
145
|
continue;
|
|
152
146
|
}
|
|
153
|
-
const top = metrics.offset - scrollOffset;
|
|
154
|
-
const bottom = top + metrics.length;
|
|
147
|
+
const top = Math.floor(metrics.offset - scrollOffset);
|
|
148
|
+
const bottom = Math.floor(top + metrics.length);
|
|
149
|
+
|
|
155
150
|
if (top < viewportHeight && bottom > 0) {
|
|
156
151
|
firstVisible = idx;
|
|
157
152
|
if (
|
|
@@ -178,21 +173,14 @@ class ViewabilityHelper {
|
|
|
178
173
|
* `onViewableItemsChanged` as appropriate.
|
|
179
174
|
*/
|
|
180
175
|
onUpdate(
|
|
181
|
-
props:
|
|
176
|
+
props: CellMetricProps,
|
|
182
177
|
scrollOffset: number,
|
|
183
178
|
viewportHeight: number,
|
|
184
|
-
|
|
185
|
-
index: number,
|
|
186
|
-
props: FrameMetricProps,
|
|
187
|
-
) => ?{
|
|
188
|
-
length: number,
|
|
189
|
-
offset: number,
|
|
190
|
-
...
|
|
191
|
-
},
|
|
179
|
+
listMetrics: ListMetricsAggregator,
|
|
192
180
|
createViewToken: (
|
|
193
181
|
index: number,
|
|
194
182
|
isViewable: boolean,
|
|
195
|
-
props:
|
|
183
|
+
props: CellMetricProps,
|
|
196
184
|
) => ViewToken,
|
|
197
185
|
onViewableItemsChanged: ({
|
|
198
186
|
viewableItems: Array<ViewToken>,
|
|
@@ -210,7 +198,7 @@ class ViewabilityHelper {
|
|
|
210
198
|
if (
|
|
211
199
|
(this._config.waitForInteraction && !this._hasInteracted) ||
|
|
212
200
|
itemCount === 0 ||
|
|
213
|
-
!
|
|
201
|
+
!listMetrics.getCellMetrics(0, props)
|
|
214
202
|
) {
|
|
215
203
|
return;
|
|
216
204
|
}
|
|
@@ -220,7 +208,7 @@ class ViewabilityHelper {
|
|
|
220
208
|
props,
|
|
221
209
|
scrollOffset,
|
|
222
210
|
viewportHeight,
|
|
223
|
-
|
|
211
|
+
listMetrics,
|
|
224
212
|
renderRange,
|
|
225
213
|
);
|
|
226
214
|
}
|
|
@@ -275,7 +263,7 @@ class ViewabilityHelper {
|
|
|
275
263
|
}
|
|
276
264
|
|
|
277
265
|
_onUpdateSync(
|
|
278
|
-
props:
|
|
266
|
+
props: CellMetricProps,
|
|
279
267
|
viewableIndicesToCheck: Array<number>,
|
|
280
268
|
onViewableItemsChanged: ({
|
|
281
269
|
changed: Array<ViewToken>,
|
|
@@ -285,7 +273,7 @@ class ViewabilityHelper {
|
|
|
285
273
|
createViewToken: (
|
|
286
274
|
index: number,
|
|
287
275
|
isViewable: boolean,
|
|
288
|
-
props:
|
|
276
|
+
props: CellMetricProps,
|
|
289
277
|
) => ViewToken,
|
|
290
278
|
) {
|
|
291
279
|
// Filter out indices that have gone out of view since this call was scheduled.
|
package/Lists/VirtualizeUtils.js
CHANGED
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
'use strict';
|
|
12
12
|
|
|
13
|
-
import type {
|
|
13
|
+
import type ListMetricsAggregator, {
|
|
14
|
+
CellMetricProps,
|
|
15
|
+
} from './ListMetricsAggregator';
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Used to find the indices of the frames that overlap the given offsets. Useful for finding the
|
|
@@ -19,15 +21,8 @@ import type {FrameMetricProps} from './VirtualizedListProps';
|
|
|
19
21
|
*/
|
|
20
22
|
export function elementsThatOverlapOffsets(
|
|
21
23
|
offsets: Array<number>,
|
|
22
|
-
props:
|
|
23
|
-
|
|
24
|
-
index: number,
|
|
25
|
-
props: FrameMetricProps,
|
|
26
|
-
) => {
|
|
27
|
-
length: number,
|
|
28
|
-
offset: number,
|
|
29
|
-
...
|
|
30
|
-
},
|
|
24
|
+
props: CellMetricProps,
|
|
25
|
+
listMetrics: ListMetricsAggregator,
|
|
31
26
|
zoomScale: number = 1,
|
|
32
27
|
): Array<number> {
|
|
33
28
|
const itemCount = props.getItemCount(props.data);
|
|
@@ -38,9 +33,8 @@ export function elementsThatOverlapOffsets(
|
|
|
38
33
|
let right = itemCount - 1;
|
|
39
34
|
|
|
40
35
|
while (left <= right) {
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
const frame = getFrameMetrics(mid, props);
|
|
36
|
+
const mid = left + Math.floor((right - left) / 2);
|
|
37
|
+
const frame = listMetrics.getCellMetricsApprox(mid, props);
|
|
44
38
|
const scaledOffsetStart = frame.offset * zoomScale;
|
|
45
39
|
const scaledOffsetEnd = (frame.offset + frame.length) * zoomScale;
|
|
46
40
|
|
|
@@ -99,21 +93,14 @@ export function newRangeCount(
|
|
|
99
93
|
* biased in the direction of scroll.
|
|
100
94
|
*/
|
|
101
95
|
export function computeWindowedRenderLimits(
|
|
102
|
-
props:
|
|
96
|
+
props: CellMetricProps,
|
|
103
97
|
maxToRenderPerBatch: number,
|
|
104
98
|
windowSize: number,
|
|
105
99
|
prev: {
|
|
106
100
|
first: number,
|
|
107
101
|
last: number,
|
|
108
102
|
},
|
|
109
|
-
|
|
110
|
-
index: number,
|
|
111
|
-
props: FrameMetricProps,
|
|
112
|
-
) => {
|
|
113
|
-
length: number,
|
|
114
|
-
offset: number,
|
|
115
|
-
...
|
|
116
|
-
},
|
|
103
|
+
listMetrics: ListMetricsAggregator,
|
|
117
104
|
scrollMetrics: {
|
|
118
105
|
dt: number,
|
|
119
106
|
offset: number,
|
|
@@ -152,7 +139,7 @@ export function computeWindowedRenderLimits(
|
|
|
152
139
|
const overscanEnd = Math.max(0, visibleEnd + leadFactor * overscanLength);
|
|
153
140
|
|
|
154
141
|
const lastItemOffset =
|
|
155
|
-
|
|
142
|
+
listMetrics.getCellMetricsApprox(itemCount - 1, props).offset * zoomScale;
|
|
156
143
|
if (lastItemOffset < overscanBegin) {
|
|
157
144
|
// Entire list is before our overscan window
|
|
158
145
|
return {
|
|
@@ -165,7 +152,7 @@ export function computeWindowedRenderLimits(
|
|
|
165
152
|
let [overscanFirst, first, last, overscanLast] = elementsThatOverlapOffsets(
|
|
166
153
|
[overscanBegin, visibleBegin, visibleEnd, overscanEnd],
|
|
167
154
|
props,
|
|
168
|
-
|
|
155
|
+
listMetrics,
|
|
169
156
|
zoomScale,
|
|
170
157
|
);
|
|
171
158
|
overscanFirst = overscanFirst == null ? 0 : overscanFirst;
|
|
@@ -267,7 +267,7 @@ export interface VirtualizedListWithoutRenderItemProps<ItemT>
|
|
|
267
267
|
|
|
268
268
|
/**
|
|
269
269
|
* The maximum number of items to render in each incremental render batch. The more rendered at
|
|
270
|
-
* once, the better the fill rate, but responsiveness
|
|
270
|
+
* once, the better the fill rate, but responsiveness may suffer because rendering content may
|
|
271
271
|
* interfere with responding to button taps or other interactions.
|
|
272
272
|
*/
|
|
273
273
|
maxToRenderPerBatch?: number | undefined;
|