@react-native-tvos/virtualized-lists 0.73.4-0
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/Interaction/Batchinator.js +76 -0
- package/Lists/CellRenderMask.js +155 -0
- package/Lists/ChildListCollection.js +72 -0
- package/Lists/FillRateHelper.js +245 -0
- package/Lists/ListMetricsAggregator.js +303 -0
- package/Lists/StateSafePureComponent.js +85 -0
- package/Lists/ViewabilityHelper.js +348 -0
- package/Lists/VirtualizeUtils.js +245 -0
- package/Lists/VirtualizedList.d.ts +393 -0
- package/Lists/VirtualizedList.js +2019 -0
- package/Lists/VirtualizedListCellRenderer.js +245 -0
- package/Lists/VirtualizedListContext.js +114 -0
- package/Lists/VirtualizedListProps.js +337 -0
- package/Lists/VirtualizedSectionList.js +617 -0
- package/README.md +23 -0
- package/Utilities/clamp.js +23 -0
- package/Utilities/infoLog.js +20 -0
- package/index.d.ts +10 -0
- package/index.js +58 -0
- package/package.json +32 -0
|
@@ -0,0 +1,303 @@
|
|
|
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
|
+
export type CellMetrics = {
|
|
18
|
+
/**
|
|
19
|
+
* Index of the item in the list
|
|
20
|
+
*/
|
|
21
|
+
index: number,
|
|
22
|
+
/**
|
|
23
|
+
* Length of the cell along the scrolling axis
|
|
24
|
+
*/
|
|
25
|
+
length: number,
|
|
26
|
+
/**
|
|
27
|
+
* Distance between this cell and the start of the list along the scrolling
|
|
28
|
+
* axis
|
|
29
|
+
*/
|
|
30
|
+
offset: number,
|
|
31
|
+
/**
|
|
32
|
+
* Whether the cell is last known to be mounted
|
|
33
|
+
*/
|
|
34
|
+
isMounted: boolean,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// TODO: `inverted` can be incorporated here if it is moved to an order
|
|
38
|
+
// based implementation instead of transform.
|
|
39
|
+
export type ListOrientation = {
|
|
40
|
+
horizontal: boolean,
|
|
41
|
+
rtl: boolean,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Subset of VirtualizedList props needed to calculate cell metrics
|
|
46
|
+
*/
|
|
47
|
+
export type CellMetricProps = {
|
|
48
|
+
data: VirtualizedListProps['data'],
|
|
49
|
+
getItemCount: VirtualizedListProps['getItemCount'],
|
|
50
|
+
getItem: VirtualizedListProps['getItem'],
|
|
51
|
+
getItemLayout?: VirtualizedListProps['getItemLayout'],
|
|
52
|
+
keyExtractor?: VirtualizedListProps['keyExtractor'],
|
|
53
|
+
...
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Provides an interface to query information about the metrics of a list and its cells.
|
|
58
|
+
*/
|
|
59
|
+
export default class ListMetricsAggregator {
|
|
60
|
+
_averageCellLength = 0;
|
|
61
|
+
_cellMetrics: Map<string, CellMetrics> = new Map();
|
|
62
|
+
_contentLength: ?number;
|
|
63
|
+
_highestMeasuredCellIndex = 0;
|
|
64
|
+
_measuredCellsLength = 0;
|
|
65
|
+
_measuredCellsCount = 0;
|
|
66
|
+
_orientation: ListOrientation = {
|
|
67
|
+
horizontal: false,
|
|
68
|
+
rtl: false,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Notify the ListMetricsAggregator that a cell has been laid out.
|
|
73
|
+
*
|
|
74
|
+
* @returns whether the cell layout has changed since last notification
|
|
75
|
+
*/
|
|
76
|
+
notifyCellLayout({
|
|
77
|
+
cellIndex,
|
|
78
|
+
cellKey,
|
|
79
|
+
orientation,
|
|
80
|
+
layout,
|
|
81
|
+
}: {
|
|
82
|
+
cellIndex: number,
|
|
83
|
+
cellKey: string,
|
|
84
|
+
orientation: ListOrientation,
|
|
85
|
+
layout: Layout,
|
|
86
|
+
}): boolean {
|
|
87
|
+
this._invalidateIfOrientationChanged(orientation);
|
|
88
|
+
|
|
89
|
+
const next: CellMetrics = {
|
|
90
|
+
index: cellIndex,
|
|
91
|
+
length: this._selectLength(layout),
|
|
92
|
+
isMounted: true,
|
|
93
|
+
offset: this.flowRelativeOffset(layout),
|
|
94
|
+
};
|
|
95
|
+
const curr = this._cellMetrics.get(cellKey);
|
|
96
|
+
|
|
97
|
+
if (!curr || next.offset !== curr.offset || next.length !== curr.length) {
|
|
98
|
+
if (curr) {
|
|
99
|
+
const dLength = next.length - curr.length;
|
|
100
|
+
this._measuredCellsLength += dLength;
|
|
101
|
+
} else {
|
|
102
|
+
this._measuredCellsLength += next.length;
|
|
103
|
+
this._measuredCellsCount += 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this._averageCellLength =
|
|
107
|
+
this._measuredCellsLength / this._measuredCellsCount;
|
|
108
|
+
this._cellMetrics.set(cellKey, next);
|
|
109
|
+
this._highestMeasuredCellIndex = Math.max(
|
|
110
|
+
this._highestMeasuredCellIndex,
|
|
111
|
+
cellIndex,
|
|
112
|
+
);
|
|
113
|
+
return true;
|
|
114
|
+
} else {
|
|
115
|
+
curr.isMounted = true;
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Notify ListMetricsAggregator that a cell has been unmounted.
|
|
122
|
+
*/
|
|
123
|
+
notifyCellUnmounted(cellKey: string): void {
|
|
124
|
+
const curr = this._cellMetrics.get(cellKey);
|
|
125
|
+
if (curr) {
|
|
126
|
+
curr.isMounted = false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Notify ListMetricsAggregator that the lists content container has been laid out.
|
|
132
|
+
*/
|
|
133
|
+
notifyListContentLayout({
|
|
134
|
+
orientation,
|
|
135
|
+
layout,
|
|
136
|
+
}: {
|
|
137
|
+
orientation: ListOrientation,
|
|
138
|
+
layout: $ReadOnly<{width: number, height: number}>,
|
|
139
|
+
}): void {
|
|
140
|
+
this._invalidateIfOrientationChanged(orientation);
|
|
141
|
+
this._contentLength = this._selectLength(layout);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Return the average length of the cells which have been measured
|
|
146
|
+
*/
|
|
147
|
+
getAverageCellLength(): number {
|
|
148
|
+
return this._averageCellLength;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Return the highest measured cell index (or 0 if nothing has been measured
|
|
153
|
+
* yet)
|
|
154
|
+
*/
|
|
155
|
+
getHighestMeasuredCellIndex(): number {
|
|
156
|
+
return this._highestMeasuredCellIndex;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Returns the exact metrics of a cell if it has already been laid out,
|
|
161
|
+
* otherwise an estimate based on the average length of previously measured
|
|
162
|
+
* cells
|
|
163
|
+
*/
|
|
164
|
+
getCellMetricsApprox(index: number, props: CellMetricProps): CellMetrics {
|
|
165
|
+
const frame = this.getCellMetrics(index, props);
|
|
166
|
+
if (frame && frame.index === index) {
|
|
167
|
+
// check for invalid frames due to row re-ordering
|
|
168
|
+
return frame;
|
|
169
|
+
} else {
|
|
170
|
+
const {data, getItemCount} = props;
|
|
171
|
+
invariant(
|
|
172
|
+
index >= 0 && index < getItemCount(data),
|
|
173
|
+
'Tried to get frame for out of range index ' + index,
|
|
174
|
+
);
|
|
175
|
+
return {
|
|
176
|
+
length: this._averageCellLength,
|
|
177
|
+
offset: this._averageCellLength * index,
|
|
178
|
+
index,
|
|
179
|
+
isMounted: false,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Returns the exact metrics of a cell if it has already been laid out
|
|
186
|
+
*/
|
|
187
|
+
getCellMetrics(index: number, props: CellMetricProps): ?CellMetrics {
|
|
188
|
+
const {data, getItem, getItemCount, getItemLayout} = props;
|
|
189
|
+
invariant(
|
|
190
|
+
index >= 0 && index < getItemCount(data),
|
|
191
|
+
'Tried to get metrics for out of range cell index ' + index,
|
|
192
|
+
);
|
|
193
|
+
const keyExtractor = props.keyExtractor ?? defaultKeyExtractor;
|
|
194
|
+
const frame = this._cellMetrics.get(
|
|
195
|
+
keyExtractor(getItem(data, index), index),
|
|
196
|
+
);
|
|
197
|
+
if (frame && frame.index === index) {
|
|
198
|
+
return frame;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (getItemLayout) {
|
|
202
|
+
const {length, offset} = getItemLayout(data, index);
|
|
203
|
+
// TODO: `isMounted` is used for both "is exact layout" and "has been
|
|
204
|
+
// unmounted". Should be refactored.
|
|
205
|
+
return {index, length, offset, isMounted: true};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Gets an approximate offset to an item at a given index. Supports
|
|
213
|
+
* fractional indices.
|
|
214
|
+
*/
|
|
215
|
+
getCellOffsetApprox(index: number, props: CellMetricProps): number {
|
|
216
|
+
if (Number.isInteger(index)) {
|
|
217
|
+
return this.getCellMetricsApprox(index, props).offset;
|
|
218
|
+
} else {
|
|
219
|
+
const frameMetrics = this.getCellMetricsApprox(Math.floor(index), props);
|
|
220
|
+
const remainder = index - Math.floor(index);
|
|
221
|
+
return frameMetrics.offset + remainder * frameMetrics.length;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Returns the length of all ScrollView content along the scrolling axis.
|
|
227
|
+
*/
|
|
228
|
+
getContentLength(): number {
|
|
229
|
+
return this._contentLength ?? 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Whether a content length has been observed
|
|
234
|
+
*/
|
|
235
|
+
hasContentLength(): boolean {
|
|
236
|
+
return this._contentLength != null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Finds the flow-relative offset (e.g. starting from the left in LTR, but
|
|
241
|
+
* right in RTL) from a layout box.
|
|
242
|
+
*/
|
|
243
|
+
flowRelativeOffset(layout: Layout, referenceContentLength?: ?number): number {
|
|
244
|
+
const {horizontal, rtl} = this._orientation;
|
|
245
|
+
|
|
246
|
+
if (horizontal && rtl) {
|
|
247
|
+
const contentLength = referenceContentLength ?? this._contentLength;
|
|
248
|
+
invariant(
|
|
249
|
+
contentLength != null,
|
|
250
|
+
'ListMetricsAggregator must be notified of list content layout before resolving offsets',
|
|
251
|
+
);
|
|
252
|
+
return (
|
|
253
|
+
contentLength -
|
|
254
|
+
(this._selectOffset(layout) + this._selectLength(layout))
|
|
255
|
+
);
|
|
256
|
+
} else {
|
|
257
|
+
return this._selectOffset(layout);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Converts a flow-relative offset to a cartesian offset
|
|
263
|
+
*/
|
|
264
|
+
cartesianOffset(flowRelativeOffset: number): number {
|
|
265
|
+
const {horizontal, rtl} = this._orientation;
|
|
266
|
+
|
|
267
|
+
if (horizontal && rtl) {
|
|
268
|
+
invariant(
|
|
269
|
+
this._contentLength != null,
|
|
270
|
+
'ListMetricsAggregator must be notified of list content layout before resolving offsets',
|
|
271
|
+
);
|
|
272
|
+
return this._contentLength - flowRelativeOffset;
|
|
273
|
+
} else {
|
|
274
|
+
return flowRelativeOffset;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
_invalidateIfOrientationChanged(orientation: ListOrientation): void {
|
|
279
|
+
if (orientation.rtl !== this._orientation.rtl) {
|
|
280
|
+
this._cellMetrics.clear();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (orientation.horizontal !== this._orientation.horizontal) {
|
|
284
|
+
this._averageCellLength = 0;
|
|
285
|
+
this._highestMeasuredCellIndex = 0;
|
|
286
|
+
this._measuredCellsLength = 0;
|
|
287
|
+
this._measuredCellsCount = 0;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
this._orientation = orientation;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
_selectLength({
|
|
294
|
+
width,
|
|
295
|
+
height,
|
|
296
|
+
}: $ReadOnly<{width: number, height: number, ...}>): number {
|
|
297
|
+
return this._orientation.horizontal ? width : height;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
_selectOffset({x, y}: $ReadOnly<{x: number, y: number, ...}>): number {
|
|
301
|
+
return this._orientation.horizontal ? x : y;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
|
8
|
+
* @format
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import invariant from 'invariant';
|
|
12
|
+
import * as React from 'react';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `setState` is called asynchronously, and should not rely on the value of
|
|
16
|
+
* `this.props` or `this.state`:
|
|
17
|
+
* https://react.dev/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
|
|
18
|
+
*
|
|
19
|
+
* SafePureComponent adds runtime enforcement, to catch cases where these
|
|
20
|
+
* variables are read in a state updater function, instead of the ones passed
|
|
21
|
+
* in.
|
|
22
|
+
*/
|
|
23
|
+
export default class StateSafePureComponent<
|
|
24
|
+
Props,
|
|
25
|
+
State: interface {},
|
|
26
|
+
> extends React.PureComponent<Props, State> {
|
|
27
|
+
_inAsyncStateUpdate = false;
|
|
28
|
+
|
|
29
|
+
constructor(props: Props) {
|
|
30
|
+
super(props);
|
|
31
|
+
this._installSetStateHooks();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setState(
|
|
35
|
+
partialState: ?(Partial<State> | ((State, Props) => ?Partial<State>)),
|
|
36
|
+
callback?: () => mixed,
|
|
37
|
+
): void {
|
|
38
|
+
if (typeof partialState === 'function') {
|
|
39
|
+
super.setState((state, props) => {
|
|
40
|
+
this._inAsyncStateUpdate = true;
|
|
41
|
+
let ret;
|
|
42
|
+
try {
|
|
43
|
+
ret = partialState(state, props);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
throw err;
|
|
46
|
+
} finally {
|
|
47
|
+
this._inAsyncStateUpdate = false;
|
|
48
|
+
}
|
|
49
|
+
return ret;
|
|
50
|
+
}, callback);
|
|
51
|
+
} else {
|
|
52
|
+
super.setState(partialState, callback);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_installSetStateHooks() {
|
|
57
|
+
const that = this;
|
|
58
|
+
let {props, state} = this;
|
|
59
|
+
|
|
60
|
+
Object.defineProperty(this, 'props', {
|
|
61
|
+
get() {
|
|
62
|
+
invariant(
|
|
63
|
+
!that._inAsyncStateUpdate,
|
|
64
|
+
'"this.props" should not be accessed during state updates',
|
|
65
|
+
);
|
|
66
|
+
return props;
|
|
67
|
+
},
|
|
68
|
+
set(newProps: Props) {
|
|
69
|
+
props = newProps;
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
Object.defineProperty(this, 'state', {
|
|
73
|
+
get() {
|
|
74
|
+
invariant(
|
|
75
|
+
!that._inAsyncStateUpdate,
|
|
76
|
+
'"this.state" should not be acceessed during state updates',
|
|
77
|
+
);
|
|
78
|
+
return state;
|
|
79
|
+
},
|
|
80
|
+
set(newState: State) {
|
|
81
|
+
state = newState;
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|