@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.
@@ -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
+ }