[fix] VirtualizedList sync from react-native

Fix #2432
Close #2167
Close #2502
This commit is contained in:
Lucio Bertin Chávez Fuentes
2023-03-27 22:08:09 -06:00
committed by Nicolas Gallagher
parent 1c5119b7e1
commit 5ace60eb7e
19 changed files with 2198 additions and 1191 deletions

View File

@@ -6,11 +6,14 @@
<PROJECT_ROOT>/packages/.*/dist/.*
<PROJECT_ROOT>/packages/react-native-web-docs/.*
<PROJECT_ROOT>/packages/react-native-web-examples/.*
.*/node_modules/.*/.*.json
[include]
[declarations]
.*/node_modules/.*
[libs]
[options]
indexed_access=true
munge_underscores=true

24
package-lock.json generated
View File

@@ -11179,6 +11179,11 @@
"dev": true,
"license": "MIT"
},
"node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
},
"node_modules/memorystream": {
"version": "0.3.1",
"dev": true,
@@ -11736,6 +11741,11 @@
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/nullthrows": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
"integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="
},
"node_modules/nunjucks": {
"version": "3.2.3",
"dev": true,
@@ -15472,7 +15482,9 @@
"@babel/runtime": "^7.18.6",
"fbjs": "^3.0.4",
"inline-style-prefixer": "^6.0.1",
"memoize-one": "^6.0.0",
"normalize-css-color": "^1.0.2",
"nullthrows": "^1.1.1",
"postcss-value-parser": "^4.2.0",
"styleq": "^0.1.2"
},
@@ -22680,6 +22692,11 @@
"version": "1.0.1",
"dev": true
},
"memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
},
"memorystream": {
"version": "0.3.1",
"dev": true
@@ -23016,6 +23033,11 @@
"boolbase": "^1.0.0"
}
},
"nullthrows": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
"integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="
},
"nunjucks": {
"version": "3.2.3",
"dev": true,
@@ -23607,7 +23629,9 @@
"@babel/runtime": "^7.18.6",
"fbjs": "^3.0.4",
"inline-style-prefixer": "^6.0.1",
"memoize-one": "^6.0.0",
"normalize-css-color": "^1.0.2",
"nullthrows": "^1.1.1",
"postcss-value-parser": "^4.2.0",
"styleq": "^0.1.2"
}

View File

@@ -25,7 +25,9 @@
"@babel/runtime": "^7.18.6",
"fbjs": "^3.0.4",
"inline-style-prefixer": "^6.0.1",
"memoize-one": "^6.0.0",
"normalize-css-color": "^1.0.2",
"nullthrows": "^1.1.1",
"postcss-value-parser": "^4.2.0",
"styleq": "^0.1.2"
},

View File

@@ -37,7 +37,7 @@ import InteractionManager from '../../../exports/InteractionManager';
class Batchinator {
_callback: () => void;
_delay: number;
_taskHandle: ?{cancel: () => void};
_taskHandle: ?{cancel: () => void, ...};
constructor(callback: () => void, delayMS: number) {
this._delay = delayMS;
this._callback = callback;
@@ -48,7 +48,7 @@ class Batchinator {
* By default, if there is a pending task the callback is run immediately. Set the option abort to
* true to not call the callback if it was pending.
*/
dispose(options: {abort: boolean} = {abort: false}) {
dispose(options: {abort: boolean, ...} = {abort: false}) {
if (this._taskHandle) {
this._taskHandle.cancel();
if (!options.abort) {

View File

@@ -10,6 +10,8 @@
'use strict';
import type {FrameMetricProps} from '../VirtualizedList/VirtualizedListProps';
export type FillRateInfo = Info;
class Info {
@@ -47,16 +49,17 @@ let _sampleRate = DEBUG ? 1 : null;
* `SceneTracker.getActiveScene` to determine the context of the events.
*/
class FillRateHelper {
_anyBlankStartTime = (null: ?number);
_anyBlankStartTime: ?number = null;
_enabled = false;
_getFrameMetrics: (index: number) => ?FrameMetrics;
_info = new Info();
_mostlyBlankStartTime = (null: ?number);
_samplesStartTime = (null: ?number);
_getFrameMetrics: (index: number, props: FrameMetricProps) => ?FrameMetrics;
_info: Info = new Info();
_mostlyBlankStartTime: ?number = null;
_samplesStartTime: ?number = null;
static addListener(
callback: FillRateInfo => void,
): {remove: () => void, ...} {
static addListener(callback: FillRateInfo => void): {
remove: () => void,
...
} {
if (_sampleRate === null) {
console.warn('Call `FillRateHelper.setSampleRate` before `addListener`.');
}
@@ -76,7 +79,9 @@ class FillRateHelper {
_minSampleCount = minSampleCount;
}
constructor(getFrameMetrics: (index: number) => ?FrameMetrics) {
constructor(
getFrameMetrics: (index: number, props: FrameMetricProps) => ?FrameMetrics,
) {
this._getFrameMetrics = getFrameMetrics;
this._enabled = (_sampleRate || 0) > Math.random();
this._resetData();
@@ -123,6 +128,7 @@ class FillRateHelper {
mostly_blank_time_frac: this._info.mostly_blank_ms / total_time_spent,
};
for (const key in derived) {
// $FlowFixMe[prop-missing]
derived[key] = Math.round(1000 * derived[key]) / 1000;
}
console.debug('FillRateHelper deactivateAndFlush: ', {derived, info});
@@ -133,12 +139,11 @@ class FillRateHelper {
computeBlankness(
props: {
data: any,
getItemCount: (data: any) => number,
initialNumToRender: number,
...FrameMetricProps,
initialNumToRender?: ?number,
...
},
state: {
cellsAroundViewport: {
first: number,
last: number,
...
@@ -154,6 +159,7 @@ class FillRateHelper {
if (
!this._enabled ||
props.getItemCount(props.data) === 0 ||
cellsAroundViewport.last < cellsAroundViewport.first ||
this._samplesStartTime == null
) {
return 0;
@@ -179,10 +185,13 @@ class FillRateHelper {
this._mostlyBlankStartTime = null;
let blankTop = 0;
let first = state.first;
let firstFrame = this._getFrameMetrics(first);
while (first <= state.last && (!firstFrame || !firstFrame.inLayout)) {
firstFrame = this._getFrameMetrics(first);
let first = cellsAroundViewport.first;
let firstFrame = this._getFrameMetrics(first, props);
while (
first <= cellsAroundViewport.last &&
(!firstFrame || !firstFrame.inLayout)
) {
firstFrame = this._getFrameMetrics(first, props);
first++;
}
// Only count blankTop if we aren't rendering the first item, otherwise we will count the header
@@ -194,10 +203,13 @@ class FillRateHelper {
);
}
let blankBottom = 0;
let last = state.last;
let lastFrame = this._getFrameMetrics(last);
while (last >= state.first && (!lastFrame || !lastFrame.inLayout)) {
lastFrame = this._getFrameMetrics(last);
let last = cellsAroundViewport.last;
let lastFrame = this._getFrameMetrics(last, props);
while (
last >= cellsAroundViewport.first &&
(!lastFrame || !lastFrame.inLayout)
) {
lastFrame = this._getFrameMetrics(last, props);
last--;
}
// Only count blankBottom if we aren't rendering the last item, otherwise we will count the
@@ -238,4 +250,4 @@ class FillRateHelper {
}
}
export default FillRateHelper;
export default FillRateHelper;

View File

@@ -8,33 +8,34 @@
* @format
*/
import Platform from '../../../exports/Platform';
import deepDiffer from '../deepDiffer';
import * as React from 'react';
import View, { type ViewProps } from '../../../exports/View';
import VirtualizedList from '../VirtualizedList';
import StyleSheet from '../../../exports/StyleSheet';
import deepDiffer from '../deepDiffer';
import Platform from '../../../exports/Platform';
import invariant from 'fbjs/lib/invariant';
import * as React from 'react';
type ScrollViewNativeComponent = any;
type ScrollResponderType = any;
type ViewStyleProp = $PropertyType<ViewProps, 'style'>;
import type {
ViewToken,
ViewabilityConfigCallbackPair,
} from '../ViewabilityHelper';
import type {RenderItemType, RenderItemProps} from '../VirtualizedList';
type ScrollResponderType = any;
import VirtualizedList from '../VirtualizedList';
import {keyExtractor as defaultKeyExtractor} from '../VirtualizeUtils';
import memoizeOne from 'memoize-one';
type $FlowFixMe = any;
type RequiredProps<ItemT> = {|
/**
* For simplicity, data is just a plain array. If you want to use something else, like an
* immutable list, use the underlying `VirtualizedList` directly.
* An array (or array-like list) of items to render. Other data types can be
* used by targetting VirtualizedList directly.
*/
data: ?$ReadOnlyArray<ItemT>,
data: ?$ArrayLike<ItemT>,
|};
type OptionalProps<ItemT> = {|
/**
@@ -89,7 +90,7 @@ type OptionalProps<ItemT> = {|
* specify `ItemSeparatorComponent`.
*/
getItemLayout?: (
data: ?Array<ItemT>,
data: ?$ArrayLike<ItemT>,
index: number,
) => {
length: number,
@@ -143,6 +144,10 @@ type OptionalProps<ItemT> = {|
* See `ScrollView` for flow type and further documentation.
*/
fadingEdgeLength?: ?number,
/**
* Enable an optimization to memoize the item renderer to prevent unnecessary rerenders.
*/
strictMode?: boolean,
|};
/**
@@ -160,6 +165,11 @@ function numColumnsOrDefault(numColumns: ?number) {
return numColumns ?? 1;
}
function isArrayLike(data: mixed): boolean {
// $FlowExpectedError[incompatible-use]
return typeof Object(data).length === 'number';
}
type FlatListProps<ItemT> = {|
...RequiredProps<ItemT>,
...OptionalProps<ItemT>,
@@ -331,6 +341,7 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
scrollToItem(params: {
animated?: ?boolean,
item: ItemT,
viewOffset?: number,
viewPosition?: number,
...
}) {
@@ -424,6 +435,7 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
}
}
// $FlowFixMe[missing-local-annot]
componentDidUpdate(prevProps: Props<ItemT>) {
invariant(
prevProps.numColumns === this.props.numColumns,
@@ -450,10 +462,11 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
_listRef: ?React.ElementRef<typeof VirtualizedList>;
_virtualizedListPairs: Array<ViewabilityConfigCallbackPair> = [];
_captureRef = ref => {
_captureRef = (ref: ?React.ElementRef<typeof VirtualizedList>) => {
this._listRef = ref;
};
// $FlowFixMe[missing-local-annot]
_checkProps(props: Props<ItemT>) {
const {
// $FlowFixMe[prop-missing] this prop doesn't exist, is only used for an invariant
@@ -485,13 +498,17 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
);
}
_getItem = (data: Array<ItemT>, index: number) => {
_getItem = (
data: $ArrayLike<ItemT>,
index: number,
): ?(ItemT | $ReadOnlyArray<ItemT>) => {
const numColumns = numColumnsOrDefault(this.props.numColumns);
if (numColumns > 1) {
const ret = [];
for (let kk = 0; kk < numColumns; kk++) {
const item = data[index * numColumns + kk];
if (item != null) {
const itemIndex = index * numColumns + kk;
if (itemIndex < data.length) {
const item = data[itemIndex];
ret.push(item);
}
}
@@ -501,8 +518,14 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
}
};
_getItemCount = (data: ?Array<ItemT>): number => {
if (data) {
_getItemCount = (data: ?$ArrayLike<ItemT>): number => {
// Legacy behavior of FlatList was to forward "undefined" length if invalid
// data like a non-arraylike object is passed. VirtualizedList would then
// coerce this, and the math would work out to no-op. For compatibility, if
// invalid data is passed, we tell VirtualizedList there are zero items
// available to prevent it from trying to read from the invalid data
// (without propagating invalidly typed data).
if (data != null && isArrayLike(data)) {
const numColumns = numColumnsOrDefault(this.props.numColumns);
return numColumns > 1 ? Math.ceil(data.length / numColumns) : data.length;
} else {
@@ -510,29 +533,26 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
}
};
_keyExtractor = (items: ItemT | Array<ItemT>, index: number) => {
_keyExtractor = (items: ItemT | Array<ItemT>, index: number): string => {
const numColumns = numColumnsOrDefault(this.props.numColumns);
const keyExtractor = this.props.keyExtractor ?? defaultKeyExtractor;
if (numColumns > 1) {
if (Array.isArray(items)) {
return items
.map((item, kk) =>
keyExtractor(((item: $FlowFixMe): ItemT), index * numColumns + kk),
)
.join(':');
} else {
invariant(
Array.isArray(items),
'FlatList: Encountered internal consistency error, expected each item to consist of an ' +
'array with 1-%s columns; instead, received a single item.',
numColumns,
);
}
} else {
// $FlowFixMe[incompatible-call] Can't call keyExtractor with an array
return keyExtractor(items, index);
invariant(
Array.isArray(items),
'FlatList: Encountered internal consistency error, expected each item to consist of an ' +
'array with 1-%s columns; instead, received a single item.',
numColumns,
);
return items
.map((item, kk) =>
keyExtractor(((item: $FlowFixMe): ItemT), index * numColumns + kk),
)
.join(':');
}
// $FlowFixMe[incompatible-call] Can't call keyExtractor with an array
return keyExtractor(items, index);
};
_pushMultiColumnViewable(arr: Array<ViewToken>, v: ViewToken): void {
@@ -551,6 +571,7 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
changed: Array<ViewToken>,
...
}) => void,
// $FlowFixMe[missing-local-annot]
) {
return (info: {
viewableItems: Array<ViewToken>,
@@ -560,8 +581,8 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
const numColumns = numColumnsOrDefault(this.props.numColumns);
if (onViewableItemsChanged) {
if (numColumns > 1) {
const changed = [];
const viewableItems = [];
const changed: Array<ViewToken> = [];
const viewableItems: Array<ViewToken> = [];
info.viewableItems.forEach(v =>
this._pushMultiColumnViewable(viewableItems, v),
);
@@ -574,15 +595,17 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
};
}
_renderer = () => {
const {ListItemComponent, renderItem, columnWrapperStyle} = this.props;
const numColumns = numColumnsOrDefault(this.props.numColumns);
_renderer = (
ListItemComponent: ?(React.ComponentType<any> | React.Element<any>),
renderItem: ?RenderItemType<ItemT>,
columnWrapperStyle: ?ViewStyleProp,
numColumns: ?number,
extraData: ?any,
// $FlowFixMe[missing-local-annot]
) => {
const cols = numColumnsOrDefault(numColumns);
let virtualizedListRenderKey = ListItemComponent
? 'ListItemComponent'
: 'renderItem';
const renderer = (props): React.Node => {
const render = (props: RenderItemProps<ItemT>): React.Node => {
if (ListItemComponent) {
// $FlowFixMe[not-a-component] Component isn't valid
// $FlowFixMe[incompatible-type-arg] Component isn't valid
@@ -596,47 +619,54 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
}
};
return {
/* $FlowFixMe[invalid-computed-prop] (>=0.111.0 site=react_native_fb)
* This comment suppresses an error found when Flow v0.111 was deployed.
* To see the error, delete this comment and run Flow. */
[virtualizedListRenderKey]: (info: RenderItemProps<ItemT>) => {
if (numColumns > 1) {
const {item, index} = info;
invariant(
Array.isArray(item),
'Expected array of items with numColumns > 1',
);
return (
<View style={[styles.row, columnWrapperStyle]}>
{item.map((it, kk) => {
const element = renderer({
item: it,
index: index * numColumns + kk,
separators: info.separators,
});
return element != null ? (
<React.Fragment key={kk}>{element}</React.Fragment>
) : null;
})}
</View>
);
} else {
return renderer(info);
}
},
const renderProp = (info: RenderItemProps<ItemT>) => {
if (cols > 1) {
const {item, index} = info;
invariant(
Array.isArray(item),
'Expected array of items with numColumns > 1',
);
return (
<View style={StyleSheet.compose(styles.row, columnWrapperStyle)}>
{item.map((it, kk) => {
const element = render({
// $FlowFixMe[incompatible-call]
item: it,
index: index * cols + kk,
separators: info.separators,
});
return element != null ? (
<React.Fragment key={kk}>{element}</React.Fragment>
) : null;
})}
</View>
);
} else {
return render(info);
}
};
return ListItemComponent
? {ListItemComponent: renderProp}
: {renderItem: renderProp};
};
// $FlowFixMe[missing-local-annot]
_memoizedRenderer = memoizeOne(this._renderer);
render(): React.Node {
const {
numColumns,
columnWrapperStyle,
removeClippedSubviews: _removeClippedSubviews,
strictMode = false,
...restProps
} = this.props;
const renderer = strictMode ? this._memoizedRenderer : this._renderer;
return (
// $FlowFixMe[incompatible-exact] - `restProps` (`Props`) is inexact.
<VirtualizedList
{...restProps}
getItem={this._getItem}
@@ -647,7 +677,13 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
removeClippedSubviews={removeClippedSubviewsOrDefault(
_removeClippedSubviews,
)}
{...this._renderer()}
{...renderer(
this.props.ListItemComponent,
this.props.renderItem,
columnWrapperStyle,
numColumns,
this.props.extraData,
)}
/>
);
}

View File

@@ -10,7 +10,7 @@
'use strict';
export type SyntheticEvent<T> = $ReadOnly<{|
export type SyntheticEvent<+T> = $ReadOnly<{|
bubbles: ?boolean,
cancelable: ?boolean,
currentTarget: HTMLElement,
@@ -82,6 +82,144 @@ export type TextLayoutEvent = SyntheticEvent<
|}>,
>;
/**
* https://developer.mozilla.org/en-US/docs/Web/API/UIEvent
*/
export interface NativeUIEvent {
/**
* Returns a long with details about the event, depending on the event type.
*/
+detail: number;
}
/**
* https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent
*/
export interface NativeMouseEvent extends NativeUIEvent {
/**
* The X coordinate of the mouse pointer in global (screen) coordinates.
*/
+screenX: number;
/**
* The Y coordinate of the mouse pointer in global (screen) coordinates.
*/
+screenY: number;
/**
* The X coordinate of the mouse pointer relative to the whole document.
*/
+pageX: number;
/**
* The Y coordinate of the mouse pointer relative to the whole document.
*/
+pageY: number;
/**
* The X coordinate of the mouse pointer in local (DOM content) coordinates.
*/
+clientX: number;
/**
* The Y coordinate of the mouse pointer in local (DOM content) coordinates.
*/
+clientY: number;
/**
* Alias for NativeMouseEvent.clientX
*/
+x: number;
/**
* Alias for NativeMouseEvent.clientY
*/
+y: number;
/**
* Returns true if the control key was down when the mouse event was fired.
*/
+ctrlKey: boolean;
/**
* Returns true if the shift key was down when the mouse event was fired.
*/
+shiftKey: boolean;
/**
* Returns true if the alt key was down when the mouse event was fired.
*/
+altKey: boolean;
/**
* Returns true if the meta key was down when the mouse event was fired.
*/
+metaKey: boolean;
/**
* The button number that was pressed (if applicable) when the mouse event was fired.
*/
+button: number;
/**
* The buttons being depressed (if any) when the mouse event was fired.
*/
+buttons: number;
/**
* The secondary target for the event, if there is one.
*/
+relatedTarget: HTMLElement;
// offset is proposed: https://drafts.csswg.org/cssom-view/#extensions-to-the-mouseevent-interface
/**
* The X coordinate of the mouse pointer between that event and the padding edge of the target node
*/
+offsetX: number;
/**
* The Y coordinate of the mouse pointer between that event and the padding edge of the target node
*/
+offsetY: number;
}
/**
* https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent
*/
export interface NativePointerEvent extends NativeMouseEvent {
/**
* A unique identifier for the pointer causing the event.
*/
+pointerId: number;
/**
* The width (magnitude on the X axis), in CSS pixels, of the contact geometry of the pointer
*/
+width: number;
/**
* The height (magnitude on the Y axis), in CSS pixels, of the contact geometry of the pointer.
*/
+height: number;
/**
* The normalized pressure of the pointer input in the range 0 to 1, where 0 and 1 represent
* the minimum and maximum pressure the hardware is capable of detecting, respectively.
*/
+pressure: number;
/**
* The normalized tangential pressure of the pointer input (also known as barrel pressure or
* cylinder stress) in the range -1 to 1, where 0 is the neutral position of the control.
*/
+tangentialPressure: number;
/**
* The plane angle (in degrees, in the range of -90 to 90) between the YZ plane and the plane
* containing both the pointer (e.g. pen stylus) axis and the Y axis.
*/
+tiltX: number;
/**
* The plane angle (in degrees, in the range of -90 to 90) between the XZ plane and the plane
* containing both the pointer (e.g. pen stylus) axis and the X axis.
*/
+tiltY: number;
/**
* The clockwise rotation of the pointer (e.g. pen stylus) around its major axis in degrees,
* with a value in the range 0 to 359.
*/
+twist: number;
/**
* Indicates the device type that caused the event (mouse, pen, touch, etc.)
*/
+pointerType: string;
/**
* Indicates if the pointer represents the primary pointer of this pointer type.
*/
+isPrimary: boolean;
}
export type PointerEvent = SyntheticEvent<NativePointerEvent>;
export type PressEvent = ResponderSyntheticEvent<
$ReadOnly<{|
changedTouches: $ReadOnlyArray<$PropertyType<PressEvent, 'nativeEvent'>>,
@@ -130,8 +268,24 @@ export type ScrollEvent = SyntheticEvent<
|}>,
>;
export type SwitchChangeEvent = SyntheticEvent<
export type BlurEvent = SyntheticEvent<
$ReadOnly<{|
value: boolean,
target: number,
|}>,
>;
export type FocusEvent = SyntheticEvent<
$ReadOnly<{|
target: number,
|}>,
>;
export type MouseEvent = SyntheticEvent<
$ReadOnly<{|
clientX: number,
clientY: number,
pageX: number,
pageY: number,
timestamp: number,
|}>,
>;

View File

@@ -0,0 +1,23 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict
*/
'use strict';
function clamp(min: number, value: number, max: number): number {
if (value < min) {
return min;
}
if (value > max) {
return max;
}
return value;
}
module.exports = clamp;

View File

@@ -7,8 +7,11 @@
* @flow
* @format
*/
'use strict';
import type {FrameMetricProps} from '../VirtualizedList/VirtualizedListProps';
import invariant from 'fbjs/lib/invariant';
export type ViewToken = {
@@ -17,6 +20,7 @@ export type ViewToken = {
index: ?number,
isViewable: boolean,
section?: any,
...
};
export type ViewabilityConfigCallbackPair = {
@@ -24,7 +28,9 @@ export type ViewabilityConfigCallbackPair = {
onViewableItemsChanged: (info: {
viewableItems: Array<ViewToken>,
changed: Array<ViewToken>,
...
}) => void,
...
};
export type ViewabilityConfig = {|
@@ -71,7 +77,7 @@ export type ViewabilityConfig = {|
class ViewabilityHelper {
_config: ViewabilityConfig;
_hasInteracted: boolean = false;
_timers: Set<TimeoutID> = new Set();
_timers: Set<number> = new Set();
_viewableIndices: Array<number> = [];
_viewableItems: Map<string, ViewToken> = new Map();
@@ -85,6 +91,9 @@ class ViewabilityHelper {
* Cleanup, e.g. on unmount. Clears any pending timers.
*/
dispose() {
/* $FlowFixMe[incompatible-call] (>=0.63.0 site=react_native_fb) This
* comment suppresses an error found when Flow v0.63 was deployed. To see
* the error delete this comment and run Flow. */
this._timers.forEach(clearTimeout);
}
@@ -92,16 +101,27 @@ class ViewabilityHelper {
* Determines which items are viewable based on the current metrics and config.
*/
computeViewableItems(
itemCount: number,
props: FrameMetricProps,
scrollOffset: number,
viewportHeight: number,
getFrameMetrics: (index: number) => ?{length: number, offset: number},
renderRange?: {first: number, last: number}, // Optional optimization to reduce the scan size
getFrameMetrics: (
index: number,
props: FrameMetricProps,
) => ?{
length: number,
offset: number,
...
},
// Optional optimization to reduce the scan size
renderRange?: {
first: number,
last: number,
...
},
): Array<number> {
const {
itemVisiblePercentThreshold,
viewAreaCoveragePercentThreshold,
} = this._config;
const itemCount = props.getItemCount(props.data);
const {itemVisiblePercentThreshold, viewAreaCoveragePercentThreshold} =
this._config;
const viewAreaMode = viewAreaCoveragePercentThreshold != null;
const viewablePercentThreshold = viewAreaMode
? viewAreaCoveragePercentThreshold
@@ -126,7 +146,7 @@ class ViewabilityHelper {
return [];
}
for (let idx = first; idx <= last; idx++) {
const metrics = getFrameMetrics(idx);
const metrics = getFrameMetrics(idx, props);
if (!metrics) {
continue;
}
@@ -158,28 +178,46 @@ class ViewabilityHelper {
* `onViewableItemsChanged` as appropriate.
*/
onUpdate(
itemCount: number,
props: FrameMetricProps,
scrollOffset: number,
viewportHeight: number,
getFrameMetrics: (index: number) => ?{length: number, offset: number},
createViewToken: (index: number, isViewable: boolean) => ViewToken,
getFrameMetrics: (
index: number,
props: FrameMetricProps,
) => ?{
length: number,
offset: number,
...
},
createViewToken: (
index: number,
isViewable: boolean,
props: FrameMetricProps,
) => ViewToken,
onViewableItemsChanged: ({
viewableItems: Array<ViewToken>,
changed: Array<ViewToken>,
...
}) => void,
renderRange?: {first: number, last: number}, // Optional optimization to reduce the scan size
// Optional optimization to reduce the scan size
renderRange?: {
first: number,
last: number,
...
},
): void {
const itemCount = props.getItemCount(props.data);
if (
(this._config.waitForInteraction && !this._hasInteracted) ||
itemCount === 0 ||
!getFrameMetrics(0)
!getFrameMetrics(0, props)
) {
return;
}
let viewableIndices = [];
let viewableIndices: Array<number> = [];
if (itemCount) {
viewableIndices = this.computeViewableItems(
itemCount,
props,
scrollOffset,
viewportHeight,
getFrameMetrics,
@@ -196,17 +234,25 @@ class ViewabilityHelper {
}
this._viewableIndices = viewableIndices;
if (this._config.minimumViewTime) {
const handle = setTimeout(() => {
const handle: TimeoutID = setTimeout(() => {
/* $FlowFixMe[incompatible-call] (>=0.63.0 site=react_native_fb) This
* comment suppresses an error found when Flow v0.63 was deployed. To
* see the error delete this comment and run Flow. */
this._timers.delete(handle);
this._onUpdateSync(
props,
viewableIndices,
onViewableItemsChanged,
createViewToken,
);
}, this._config.minimumViewTime);
/* $FlowFixMe[incompatible-call] (>=0.63.0 site=react_native_fb) This
* comment suppresses an error found when Flow v0.63 was deployed. To see
* the error delete this comment and run Flow. */
this._timers.add(handle);
} else {
this._onUpdateSync(
props,
viewableIndices,
onViewableItemsChanged,
createViewToken,
@@ -229,12 +275,18 @@ class ViewabilityHelper {
}
_onUpdateSync(
// $FlowFixMe
viewableIndicesToCheck,
// $FlowFixMe
onViewableItemsChanged,
// $FlowFixMe
createViewToken,
props: FrameMetricProps,
viewableIndicesToCheck: Array<number>,
onViewableItemsChanged: ({
changed: Array<ViewToken>,
viewableItems: Array<ViewToken>,
...
}) => void,
createViewToken: (
index: number,
isViewable: boolean,
props: FrameMetricProps,
) => ViewToken,
) {
// Filter out indices that have gone out of view since this call was scheduled.
viewableIndicesToCheck = viewableIndicesToCheck.filter(ii =>
@@ -243,7 +295,7 @@ class ViewabilityHelper {
const prevItems = this._viewableItems;
const nextItems = new Map(
viewableIndicesToCheck.map(ii => {
const viewable = createViewToken(ii, true);
const viewable = createViewToken(ii, true, props);
return [viewable.key, viewable];
}),
);

View File

@@ -10,7 +10,7 @@
'use strict';
import invariant from 'fbjs/lib/invariant';
import type {FrameMetricProps} from '../VirtualizedList/VirtualizedListProps';
/**
* Used to find the indices of the frames that overlap the given offsets. Useful for finding the
@@ -19,34 +19,48 @@ import invariant from 'fbjs/lib/invariant';
*/
export function elementsThatOverlapOffsets(
offsets: Array<number>,
itemCount: number,
getFrameMetrics: (index: number) => {
props: FrameMetricProps,
getFrameMetrics: (
index: number,
props: FrameMetricProps,
) => {
length: number,
offset: number,
...
},
zoomScale: number = 1,
): Array<number> {
const out = [];
let outLength = 0;
for (let ii = 0; ii < itemCount; ii++) {
const frame = getFrameMetrics(ii);
const trailingOffset = frame.offset + frame.length;
for (let kk = 0; kk < offsets.length; kk++) {
if (out[kk] == null && trailingOffset >= offsets[kk]) {
out[kk] = ii;
outLength++;
if (kk === offsets.length - 1) {
invariant(
outLength === offsets.length,
'bad offsets input, should be in increasing order: %s',
JSON.stringify(offsets),
);
return out;
}
const itemCount = props.getItemCount(props.data);
const result = [];
for (let offsetIndex = 0; offsetIndex < offsets.length; offsetIndex++) {
const currentOffset = offsets[offsetIndex];
let left = 0;
let right = itemCount - 1;
while (left <= right) {
// eslint-disable-next-line no-bitwise
const mid = left + ((right - left) >>> 1);
const frame = getFrameMetrics(mid, props);
const scaledOffsetStart = frame.offset * zoomScale;
const scaledOffsetEnd = (frame.offset + frame.length) * zoomScale;
// We want the first frame that contains the offset, with inclusive bounds. Thus, for the
// first frame the scaledOffsetStart is inclusive, while for other frames it is exclusive.
if (
(mid === 0 && currentOffset < scaledOffsetStart) ||
(mid !== 0 && currentOffset <= scaledOffsetStart)
) {
right = mid - 1;
} else if (currentOffset > scaledOffsetEnd) {
left = mid + 1;
} else {
result[offsetIndex] = mid;
break;
}
}
}
return out;
return result;
}
/**
@@ -85,16 +99,17 @@ export function newRangeCount(
* biased in the direction of scroll.
*/
export function computeWindowedRenderLimits(
data: any,
getItemCount: (data: any) => number,
props: FrameMetricProps,
maxToRenderPerBatch: number,
windowSize: number,
prev: {
first: number,
last: number,
...
},
getFrameMetricsApprox: (index: number) => {
getFrameMetricsApprox: (
index: number,
props: FrameMetricProps,
) => {
length: number,
offset: number,
...
@@ -104,18 +119,18 @@ export function computeWindowedRenderLimits(
offset: number,
velocity: number,
visibleLength: number,
zoomScale: number,
...
},
): {
first: number,
last: number,
...
} {
const itemCount = getItemCount(data);
const itemCount = props.getItemCount(props.data);
if (itemCount === 0) {
return prev;
return {first: 0, last: -1};
}
const {offset, velocity, visibleLength} = scrollMetrics;
const {offset, velocity, visibleLength, zoomScale = 1} = scrollMetrics;
// Start with visible area, then compute maximum overscan region by expanding from there, biased
// in the direction of scroll. Total overscan area is capped, which should cap memory consumption
@@ -136,7 +151,8 @@ export function computeWindowedRenderLimits(
);
const overscanEnd = Math.max(0, visibleEnd + leadFactor * overscanLength);
const lastItemOffset = getFrameMetricsApprox(itemCount - 1).offset;
const lastItemOffset =
getFrameMetricsApprox(itemCount - 1, props).offset * zoomScale;
if (lastItemOffset < overscanBegin) {
// Entire list is before our overscan window
return {
@@ -148,8 +164,9 @@ export function computeWindowedRenderLimits(
// Find the indices that correspond to the items at the render boundaries we're targeting.
let [overscanFirst, first, last, overscanLast] = elementsThatOverlapOffsets(
[overscanBegin, visibleBegin, visibleEnd, overscanEnd],
itemCount,
props,
getFrameMetricsApprox,
zoomScale,
);
overscanFirst = overscanFirst == null ? 0 : overscanFirst;
first = first == null ? Math.max(0, overscanFirst) : first;
@@ -238,4 +255,4 @@ export function keyExtractor(item: any, index: number): string {
return item.id;
}
return String(index);
}
}

View File

@@ -0,0 +1,155 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
import invariant from 'fbjs/lib/invariant';
export type CellRegion = {
first: number,
last: number,
isSpacer: boolean,
};
export class CellRenderMask {
_numCells: number;
_regions: Array<CellRegion>;
constructor(numCells: number) {
invariant(
numCells >= 0,
'CellRenderMask must contain a non-negative number os cells',
);
this._numCells = numCells;
if (numCells === 0) {
this._regions = [];
} else {
this._regions = [
{
first: 0,
last: numCells - 1,
isSpacer: true,
},
];
}
}
enumerateRegions(): $ReadOnlyArray<CellRegion> {
return this._regions;
}
addCells(cells: {first: number, last: number}): void {
invariant(
cells.first >= 0 &&
cells.first < this._numCells &&
cells.last >= -1 &&
cells.last < this._numCells &&
cells.last >= cells.first - 1,
'CellRenderMask.addCells called with invalid cell range',
);
// VirtualizedList uses inclusive ranges, where zero-count states are
// possible. E.g. [0, -1] for no cells, starting at 0.
if (cells.last < cells.first) {
return;
}
const [firstIntersect, firstIntersectIdx] = this._findRegion(cells.first);
const [lastIntersect, lastIntersectIdx] = this._findRegion(cells.last);
// Fast-path if the cells to add are already all present in the mask. We
// will otherwise need to do some mutation.
if (firstIntersectIdx === lastIntersectIdx && !firstIntersect.isSpacer) {
return;
}
// We need to replace the existing covered regions with 1-3 new regions
// depending whether we need to split spacers out of overlapping regions.
const newLeadRegion: Array<CellRegion> = [];
const newTailRegion: Array<CellRegion> = [];
const newMainRegion: CellRegion = {
...cells,
isSpacer: false,
};
if (firstIntersect.first < newMainRegion.first) {
if (firstIntersect.isSpacer) {
newLeadRegion.push({
first: firstIntersect.first,
last: newMainRegion.first - 1,
isSpacer: true,
});
} else {
newMainRegion.first = firstIntersect.first;
}
}
if (lastIntersect.last > newMainRegion.last) {
if (lastIntersect.isSpacer) {
newTailRegion.push({
first: newMainRegion.last + 1,
last: lastIntersect.last,
isSpacer: true,
});
} else {
newMainRegion.last = lastIntersect.last;
}
}
const replacementRegions: Array<CellRegion> = [
...newLeadRegion,
newMainRegion,
...newTailRegion,
];
const numRegionsToDelete = lastIntersectIdx - firstIntersectIdx + 1;
this._regions.splice(
firstIntersectIdx,
numRegionsToDelete,
...replacementRegions,
);
}
numCells(): number {
return this._numCells;
}
equals(other: CellRenderMask): boolean {
return (
this._numCells === other._numCells &&
this._regions.length === other._regions.length &&
this._regions.every(
(region, i) =>
region.first === other._regions[i].first &&
region.last === other._regions[i].last &&
region.isSpacer === other._regions[i].isSpacer,
)
);
}
_findRegion(cellIdx: number): [CellRegion, number] {
let firstIdx = 0;
let lastIdx = this._regions.length - 1;
while (firstIdx <= lastIdx) {
const middleIdx = Math.floor((firstIdx + lastIdx) / 2);
const middleRegion = this._regions[middleIdx];
if (cellIdx >= middleRegion.first && cellIdx <= middleRegion.last) {
return [middleRegion, middleIdx];
} else if (cellIdx < middleRegion.first) {
lastIdx = middleIdx - 1;
} else if (cellIdx > middleRegion.last) {
firstIdx = middleIdx + 1;
}
}
invariant(false, `A region was not found containing cellIdx ${cellIdx}`);
}
}

View File

@@ -0,0 +1,72 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
import invariant from 'fbjs/lib/invariant';
export default class ChildListCollection<TList> {
_cellKeyToChildren: Map<string, Set<TList>> = new Map();
_childrenToCellKey: Map<TList, string> = new Map();
add(list: TList, cellKey: string): void {
invariant(
!this._childrenToCellKey.has(list),
'Trying to add already present child list',
);
const cellLists = this._cellKeyToChildren.get(cellKey) ?? new Set();
cellLists.add(list);
this._cellKeyToChildren.set(cellKey, cellLists);
this._childrenToCellKey.set(list, cellKey);
}
remove(list: TList): void {
const cellKey = this._childrenToCellKey.get(list);
invariant(cellKey != null, 'Trying to remove non-present child list');
this._childrenToCellKey.delete(list);
const cellLists = this._cellKeyToChildren.get(cellKey);
invariant(cellLists, '_cellKeyToChildren should contain cellKey');
cellLists.delete(list);
if (cellLists.size === 0) {
this._cellKeyToChildren.delete(cellKey);
}
}
forEach(fn: TList => void): void {
for (const listSet of this._cellKeyToChildren.values()) {
for (const list of listSet) {
fn(list);
}
}
}
forEachInCell(cellKey: string, fn: TList => void): void {
const listSet = this._cellKeyToChildren.get(cellKey) ?? [];
for (const list of listSet) {
fn(list);
}
}
anyInCell(cellKey: string, fn: TList => boolean): boolean {
const listSet = this._cellKeyToChildren.get(cellKey) ?? [];
for (const list of listSet) {
if (fn(list)) {
return true;
}
}
return false;
}
size(): number {
return this._childrenToCellKey.size;
}
}

View File

@@ -0,0 +1,85 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
import invariant from 'fbjs/lib/invariant';
import * as React from 'react';
/**
* `setState` is called asynchronously, and should not rely on the value of
* `this.props` or `this.state`:
* https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
*
* SafePureComponent adds runtime enforcement, to catch cases where these
* variables are read in a state updater function, instead of the ones passed
* in.
*/
export default class StateSafePureComponent<
Props,
State: interface {},
> extends React.PureComponent<Props, State> {
_inAsyncStateUpdate = false;
constructor(props: Props) {
super(props);
this._installSetStateHooks();
}
setState(
partialState: ?($Shape<State> | ((State, Props) => ?$Shape<State>)),
callback?: () => mixed,
): void {
if (typeof partialState === 'function') {
super.setState((state, props) => {
this._inAsyncStateUpdate = true;
let ret;
try {
ret = partialState(state, props);
} catch (err) {
throw err;
} finally {
this._inAsyncStateUpdate = false;
}
return ret;
}, callback);
} else {
super.setState(partialState, callback);
}
}
_installSetStateHooks() {
const that = this;
let {props, state} = this;
Object.defineProperty(this, 'props', {
get() {
invariant(
!that._inAsyncStateUpdate,
'"this.props" should not be accessed during state updates',
);
return props;
},
set(newProps: Props) {
props = newProps;
},
});
Object.defineProperty(this, 'state', {
get() {
invariant(
!that._inAsyncStateUpdate,
'"this.state" should not be acceessed during state updates',
);
return state;
},
set(newState: State) {
state = newState;
},
});
}
}

View File

@@ -0,0 +1,247 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
import type { LayoutEvent } from '../../../types';
import type {
FocusEvent,
} from '../Types/CoreEventTypes';
import type {CellRendererProps, RenderItemType} from './VirtualizedListProps';
import View, { type ViewProps } from '../../../exports/View';
import StyleSheet from '../../../exports/StyleSheet';
import {VirtualizedListCellContextProvider} from './VirtualizedListContext.js';
import invariant from 'fbjs/lib/invariant';
import * as React from 'react';
type ViewStyleProp = $PropertyType<ViewProps, 'style'>;
export type Props<ItemT> = {
CellRendererComponent?: ?React.ComponentType<CellRendererProps<ItemT>>,
ItemSeparatorComponent: ?React.ComponentType<
any | {highlighted: boolean, leadingItem: ?ItemT},
>,
ListItemComponent?: ?(React.ComponentType<any> | React.Element<any>),
cellKey: string,
horizontal: ?boolean,
index: number,
inversionStyle: ViewStyleProp,
item: ItemT,
onCellLayout?: (event: LayoutEvent, cellKey: string, index: number) => void,
onCellFocusCapture?: (event: FocusEvent) => void,
onUnmount: (cellKey: string) => void,
onUpdateSeparators: (
cellKeys: Array<?string>,
props: $Shape<SeparatorProps<ItemT>>,
) => void,
prevCellKey: ?string,
renderItem?: ?RenderItemType<ItemT>,
...
};
type SeparatorProps<ItemT> = $ReadOnly<{|
highlighted: boolean,
leadingItem: ?ItemT,
|}>;
type State<ItemT> = {
separatorProps: SeparatorProps<ItemT>,
...
};
export default class CellRenderer<ItemT> extends React.Component<
Props<ItemT>,
State<ItemT>,
> {
state: State<ItemT> = {
separatorProps: {
highlighted: false,
leadingItem: this.props.item,
},
};
static getDerivedStateFromProps(
props: Props<ItemT>,
prevState: State<ItemT>,
): ?State<ItemT> {
return {
separatorProps: {
...prevState.separatorProps,
leadingItem: props.item,
},
};
}
// TODO: consider factoring separator stuff out of VirtualizedList into FlatList since it's not
// reused by SectionList and we can keep VirtualizedList simpler.
// $FlowFixMe[missing-local-annot]
_separators = {
highlight: () => {
const {cellKey, prevCellKey} = this.props;
this.props.onUpdateSeparators([cellKey, prevCellKey], {
highlighted: true,
});
},
unhighlight: () => {
const {cellKey, prevCellKey} = this.props;
this.props.onUpdateSeparators([cellKey, prevCellKey], {
highlighted: false,
});
},
updateProps: (
select: 'leading' | 'trailing',
newProps: SeparatorProps<ItemT>,
) => {
const {cellKey, prevCellKey} = this.props;
this.props.onUpdateSeparators(
[select === 'leading' ? prevCellKey : cellKey],
newProps,
);
},
};
updateSeparatorProps(newProps: SeparatorProps<ItemT>) {
this.setState(state => ({
separatorProps: {...state.separatorProps, ...newProps},
}));
}
componentWillUnmount() {
this.props.onUnmount(this.props.cellKey);
}
_onLayout = (nativeEvent: LayoutEvent): void => {
this.props.onCellLayout &&
this.props.onCellLayout(
nativeEvent,
this.props.cellKey,
this.props.index,
);
};
_renderElement(
renderItem: ?RenderItemType<ItemT>,
ListItemComponent: any,
item: ItemT,
index: number,
): React.Node {
if (renderItem && ListItemComponent) {
console.warn(
'VirtualizedList: Both ListItemComponent and renderItem props are present. ListItemComponent will take' +
' precedence over renderItem.',
);
}
if (ListItemComponent) {
/* $FlowFixMe[not-a-component] (>=0.108.0 site=react_native_fb) This
* comment suppresses an error found when Flow v0.108 was deployed. To
* see the error, delete this comment and run Flow. */
/* $FlowFixMe[incompatible-type-arg] (>=0.108.0 site=react_native_fb)
* This comment suppresses an error found when Flow v0.108 was deployed.
* To see the error, delete this comment and run Flow. */
return React.createElement(ListItemComponent, {
item,
index,
separators: this._separators,
});
}
if (renderItem) {
return renderItem({
item,
index,
separators: this._separators,
});
}
invariant(
false,
'VirtualizedList: Either ListItemComponent or renderItem props are required but none were found.',
);
}
render(): React.Node {
const {
CellRendererComponent,
ItemSeparatorComponent,
ListItemComponent,
cellKey,
horizontal,
item,
index,
inversionStyle,
onCellFocusCapture,
onCellLayout,
renderItem,
} = this.props;
const element = this._renderElement(
renderItem,
ListItemComponent,
item,
index,
);
// NOTE: that when this is a sticky header, `onLayout` will get automatically extracted and
// called explicitly by `ScrollViewStickyHeader`.
const itemSeparator: React.Node = React.isValidElement(
ItemSeparatorComponent,
)
? // $FlowFixMe[incompatible-type]
ItemSeparatorComponent
: // $FlowFixMe[incompatible-type]
ItemSeparatorComponent && (
<ItemSeparatorComponent {...this.state.separatorProps} />
);
const cellStyle = inversionStyle
? horizontal
? [styles.rowReverse, inversionStyle]
: [styles.columnReverse, inversionStyle]
: horizontal
? [styles.row, inversionStyle]
: inversionStyle;
const result = !CellRendererComponent ? (
<View
style={cellStyle}
onFocusCapture={onCellFocusCapture}
{...(onCellLayout && {onLayout: this._onLayout})}>
{element}
{itemSeparator}
</View>
) : (
<CellRendererComponent
cellKey={cellKey}
index={index}
item={item}
style={cellStyle}
onFocusCapture={onCellFocusCapture}
{...(onCellLayout && {onLayout: this._onLayout})}>
{element}
{itemSeparator}
</CellRendererComponent>
);
return (
<VirtualizedListCellContextProvider cellKey={this.props.cellKey}>
{result}
</VirtualizedListCellContextProvider>
);
}
}
const styles = StyleSheet.create({
row: {
flexDirection: 'row',
},
rowReverse: {
flexDirection: 'row-reverse',
},
columnReverse: {
flexDirection: 'column-reverse',
},
});

View File

@@ -8,37 +8,13 @@
* @format
*/
import type VirtualizedList from './';
import typeof VirtualizedList from '../VirtualizedList';
import * as React from 'react';
import {useMemo, useContext} from 'react';
import {useContext, useMemo} from 'react';
const __DEV__ = process.env.NODE_ENV !== 'production';
type Frame = $ReadOnly<{
offset: number,
length: number,
index: number,
inLayout: boolean,
}>;
export type ChildListState = $ReadOnly<{
first: number,
last: number,
frames: {[key: number]: Frame},
}>;
// Data propagated through nested lists (regardless of orientation) that is
// useful for producing diagnostics for usage errors involving nesting (e.g
// missing/duplicate keys).
export type ListDebugInfo = $ReadOnly<{
cellKey: string,
listKey: string,
parent: ?ListDebugInfo,
// We include all ancestors regardless of orientation, so this is not always
// identical to the child's orientation.
horizontal: boolean,
}>;
type Context = $ReadOnly<{
cellKey: ?string,
getScrollMetrics: () => {
@@ -49,26 +25,21 @@ type Context = $ReadOnly<{
timestamp: number,
velocity: number,
visibleLength: number,
zoomScale: number,
},
horizontal: ?boolean,
getOutermostParentListRef: () => VirtualizedList,
getNestedChildState: string => ?ChildListState,
getOutermostParentListRef: () => React.ElementRef<VirtualizedList>,
registerAsNestedChild: ({
cellKey: string,
key: string,
ref: VirtualizedList,
parentDebugInfo: ListDebugInfo,
}) => ?ChildListState,
unregisterAsNestedChild: ({
key: string,
state: ChildListState,
ref: React.ElementRef<VirtualizedList>,
}) => void,
unregisterAsNestedChild: ({
ref: React.ElementRef<VirtualizedList>,
}) => void,
debugInfo: ListDebugInfo,
}>;
export const VirtualizedListContext: React.Context<?Context> = React.createContext(
null,
);
export const VirtualizedListContext: React.Context<?Context> =
React.createContext(null);
if (__DEV__) {
VirtualizedListContext.displayName = 'VirtualizedListContext';
}
@@ -105,27 +76,15 @@ export function VirtualizedListContextProvider({
getScrollMetrics: value.getScrollMetrics,
horizontal: value.horizontal,
getOutermostParentListRef: value.getOutermostParentListRef,
getNestedChildState: value.getNestedChildState,
registerAsNestedChild: value.registerAsNestedChild,
unregisterAsNestedChild: value.unregisterAsNestedChild,
debugInfo: {
cellKey: value.debugInfo.cellKey,
horizontal: value.debugInfo.horizontal,
listKey: value.debugInfo.listKey,
parent: value.debugInfo.parent,
},
}),
[
value.getScrollMetrics,
value.horizontal,
value.getOutermostParentListRef,
value.getNestedChildState,
value.registerAsNestedChild,
value.unregisterAsNestedChild,
value.debugInfo.cellKey,
value.debugInfo.horizontal,
value.debugInfo.listKey,
value.debugInfo.parent,
],
);
return (
@@ -145,10 +104,14 @@ export function VirtualizedListCellContextProvider({
cellKey: string,
children: React.Node,
}): React.Node {
const context = useContext(VirtualizedListContext);
// Avoid setting a newly created context object if the values are identical.
const currContext = useContext(VirtualizedListContext);
const context = useMemo(
() => (currContext == null ? null : {...currContext, cellKey}),
[currContext, cellKey],
);
return (
<VirtualizedListContext.Provider
value={context == null ? null : {...context, cellKey}}>
<VirtualizedListContext.Provider value={context}>
{children}
</VirtualizedListContext.Provider>
);

View File

@@ -0,0 +1,307 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
import ScrollView from '../../../exports/ScrollView';
import type { LayoutEvent } from '../../../types';
import type {
FocusEvent,
} from '../Types/CoreEventTypes';
import { type ViewProps } from '../../../exports/View';
type ViewStyleProp = $PropertyType<ViewProps, 'style'>;
import type {
ViewabilityConfig,
ViewabilityConfigCallbackPair,
ViewToken,
} from '../ViewabilityHelper';
import * as React from 'react';
export type Item = any;
export type Separators = {
highlight: () => void,
unhighlight: () => void,
updateProps: (select: 'leading' | 'trailing', newProps: Object) => void,
...
};
export type RenderItemProps<ItemT> = {
item: ItemT,
index: number,
separators: Separators,
...
};
export type CellRendererProps<ItemT> = $ReadOnly<{
cellKey: string,
children: React.Node,
index: number,
item: ItemT,
onFocusCapture?: (event: FocusEvent) => void,
onLayout?: (event: LayoutEvent) => void,
style: ViewStyleProp,
}>;
export type RenderItemType<ItemT> = (
info: RenderItemProps<ItemT>,
) => React.Node;
type RequiredProps = {|
/**
* The default accessor functions assume this is an Array<{key: string} | {id: string}> but you can override
* getItem, getItemCount, and keyExtractor to handle any type of index-based data.
*/
data?: any,
/**
* A generic accessor for extracting an item from any sort of data blob.
*/
getItem: (data: any, index: number) => ?Item,
/**
* Determines how many items are in the data blob.
*/
getItemCount: (data: any) => number,
|};
type OptionalProps = {|
renderItem?: ?RenderItemType<Item>,
/**
* `debug` will turn on extra logging and visual overlays to aid with debugging both usage and
* implementation, but with a significant perf hit.
*/
debug?: ?boolean,
/**
* DEPRECATED: Virtualization provides significant performance and memory optimizations, but fully
* unmounts react instances that are outside of the render window. You should only need to disable
* this for debugging purposes. Defaults to false.
*/
disableVirtualization?: ?boolean,
/**
* A marker property for telling the list to re-render (since it implements `PureComponent`). If
* any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the
* `data` prop, stick it here and treat it immutably.
*/
extraData?: any,
// e.g. height, y
getItemLayout?: (
data: any,
index: number,
) => {
length: number,
offset: number,
index: number,
...
},
horizontal?: ?boolean,
/**
* How many items to render in the initial batch. This should be enough to fill the screen but not
* much more. Note these items will never be unmounted as part of the windowed rendering in order
* to improve perceived performance of scroll-to-top actions.
*/
initialNumToRender?: ?number,
/**
* Instead of starting at the top with the first item, start at `initialScrollIndex`. This
* disables the "scroll to top" optimization that keeps the first `initialNumToRender` items
* always rendered and immediately renders the items starting at this initial index. Requires
* `getItemLayout` to be implemented.
*/
initialScrollIndex?: ?number,
/**
* Reverses the direction of scroll. Uses scale transforms of -1.
*/
inverted?: ?boolean,
keyExtractor?: ?(item: Item, index: number) => string,
/**
* CellRendererComponent allows customizing how cells rendered by
* `renderItem`/`ListItemComponent` are wrapped when placed into the
* underlying ScrollView. This component must accept event handlers which
* notify VirtualizedList of changes within the cell.
*/
CellRendererComponent?: ?React.ComponentType<CellRendererProps<Item>>,
/**
* Rendered in between each item, but not at the top or bottom. By default, `highlighted` and
* `leadingItem` props are provided. `renderItem` provides `separators.highlight`/`unhighlight`
* which will update the `highlighted` prop, but you can also add custom props with
* `separators.updateProps`.
*/
ItemSeparatorComponent?: ?React.ComponentType<any>,
/**
* Takes an item from `data` and renders it into the list. Example usage:
*
* <FlatList
* ItemSeparatorComponent={Platform.OS !== 'android' && ({highlighted}) => (
* <View style={[style.separator, highlighted && {marginLeft: 0}]} />
* )}
* data={[{title: 'Title Text', key: 'item1'}]}
* ListItemComponent={({item, separators}) => (
* <TouchableHighlight
* onPress={() => this._onPress(item)}
* onShowUnderlay={separators.highlight}
* onHideUnderlay={separators.unhighlight}>
* <View style={{backgroundColor: 'white'}}>
* <Text>{item.title}</Text>
* </View>
* </TouchableHighlight>
* )}
* />
*
* Provides additional metadata like `index` if you need it, as well as a more generic
* `separators.updateProps` function which let's you set whatever props you want to change the
* rendering of either the leading separator or trailing separator in case the more common
* `highlight` and `unhighlight` (which set the `highlighted: boolean` prop) are insufficient for
* your use-case.
*/
ListItemComponent?: ?(React.ComponentType<any> | React.Element<any>),
/**
* Rendered when the list is empty. Can be a React Component Class, a render function, or
* a rendered element.
*/
ListEmptyComponent?: ?(React.ComponentType<any> | React.Element<any>),
/**
* Rendered at the bottom of all the items. Can be a React Component Class, a render function, or
* a rendered element.
*/
ListFooterComponent?: ?(React.ComponentType<any> | React.Element<any>),
/**
* Styling for internal View for ListFooterComponent
*/
ListFooterComponentStyle?: ViewStyleProp,
/**
* Rendered at the top of all the items. Can be a React Component Class, a render function, or
* a rendered element.
*/
ListHeaderComponent?: ?(React.ComponentType<any> | React.Element<any>),
/**
* Styling for internal View for ListHeaderComponent
*/
ListHeaderComponentStyle?: ViewStyleProp,
/**
* The maximum number of items to render in each incremental render batch. The more rendered at
* once, the better the fill rate, but responsiveness may suffer because rendering content may
* interfere with responding to button taps or other interactions.
*/
maxToRenderPerBatch?: ?number,
/**
* Called once when the scroll position gets within within `onEndReachedThreshold`
* from the logical end of the list.
*/
onEndReached?: ?(info: {distanceFromEnd: number, ...}) => void,
/**
* How far from the end (in units of visible length of the list) the trailing edge of the
* list must be from the end of the content to trigger the `onEndReached` callback.
* Thus, a value of 0.5 will trigger `onEndReached` when the end of the content is
* within half the visible length of the list.
*/
onEndReachedThreshold?: ?number,
/**
* If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make
* sure to also set the `refreshing` prop correctly.
*/
onRefresh?: ?() => void,
/**
* Used to handle failures when scrolling to an index that has not been measured yet. Recommended
* action is to either compute your own offset and `scrollTo` it, or scroll as far as possible and
* then try again after more items have been rendered.
*/
onScrollToIndexFailed?: ?(info: {
index: number,
highestMeasuredFrameIndex: number,
averageItemLength: number,
...
}) => void,
/**
* Called once when the scroll position gets within within `onStartReachedThreshold`
* from the logical start of the list.
*/
onStartReached?: ?(info: {distanceFromStart: number, ...}) => void,
/**
* How far from the start (in units of visible length of the list) the leading edge of the
* list must be from the start of the content to trigger the `onStartReached` callback.
* Thus, a value of 0.5 will trigger `onStartReached` when the start of the content is
* within half the visible length of the list.
*/
onStartReachedThreshold?: ?number,
/**
* Called when the viewability of rows changes, as defined by the
* `viewabilityConfig` prop.
*/
onViewableItemsChanged?: ?(info: {
viewableItems: Array<ViewToken>,
changed: Array<ViewToken>,
...
}) => void,
persistentScrollbar?: ?boolean,
/**
* Set this when offset is needed for the loading indicator to show correctly.
*/
progressViewOffset?: number,
/**
* A custom refresh control element. When set, it overrides the default
* <RefreshControl> component built internally. The onRefresh and refreshing
* props are also ignored. Only works for vertical VirtualizedList.
*/
refreshControl?: ?React.Element<any>,
/**
* Set this true while waiting for new data from a refresh.
*/
refreshing?: ?boolean,
/**
* Note: may have bugs (missing content) in some circumstances - use at your own risk.
*
* This may improve scroll performance for large lists.
*/
removeClippedSubviews?: boolean,
/**
* Render a custom scroll component, e.g. with a differently styled `RefreshControl`.
*/
renderScrollComponent?: (props: Object) => React.Element<any>,
/**
* Amount of time between low-pri item render batches, e.g. for rendering items quite a ways off
* screen. Similar fill rate/responsiveness tradeoff as `maxToRenderPerBatch`.
*/
updateCellsBatchingPeriod?: ?number,
/**
* See `ViewabilityHelper` for flow type and further documentation.
*/
viewabilityConfig?: ViewabilityConfig,
/**
* List of ViewabilityConfig/onViewableItemsChanged pairs. A specific onViewableItemsChanged
* will be called when its corresponding ViewabilityConfig's conditions are met.
*/
viewabilityConfigCallbackPairs?: Array<ViewabilityConfigCallbackPair>,
/**
* Determines the maximum number of items rendered outside of the visible area, in units of
* visible lengths. So if your list fills the screen, then `windowSize={21}` (the default) will
* render the visible screen area plus up to 10 screens above and 10 below the viewport. Reducing
* this number will reduce memory consumption and may improve performance, but will increase the
* chance that fast scrolling may reveal momentary blank areas of unrendered content.
*/
windowSize?: ?number,
/**
* The legacy implementation is no longer supported.
*/
legacyImplementation?: empty,
|};
export type Props = {|
...React.ElementConfig<typeof ScrollView>,
...RequiredProps,
...OptionalProps,
|};
/**
* Subset of properties needed to calculate frame metrics
*/
export type FrameMetricProps = {
data: RequiredProps['data'],
getItemCount: RequiredProps['getItemCount'],
getItem: RequiredProps['getItem'],
getItemLayout?: OptionalProps['getItemLayout'],
keyExtractor?: OptionalProps['keyExtractor'],
...
};

File diff suppressed because it is too large Load Diff

View File

@@ -8,15 +8,12 @@
* @format
*/
'use strict';
import type {ViewToken} from '../ViewabilityHelper';
import {keyExtractor as defaultKeyExtractor} from '../VirtualizeUtils';
import View from '../../../exports/View';
import VirtualizedList from '../VirtualizedList';
import * as React from 'react';
import {keyExtractor as defaultKeyExtractor} from '../VirtualizeUtils';
import invariant from 'fbjs/lib/invariant';
import * as React from 'react';
type Item = any;
@@ -141,9 +138,9 @@ class VirtualizedSectionList<
return;
}
if (params.itemIndex > 0 && this.props.stickySectionHeadersEnabled) {
// $FlowFixMe[prop-missing] Cannot access private property
const frame = this._listRef._getFrameMetricsApprox(
const frame = this._listRef.__getFrameMetricsApprox(
index - params.itemIndex,
this._listRef.props,
);
viewOffset += frame.length;
}
@@ -152,6 +149,7 @@ class VirtualizedSectionList<
viewOffset,
index,
};
// $FlowFixMe[incompatible-use]
this._listRef.scrollToIndex(toIndexParams);
}
@@ -174,7 +172,7 @@ class VirtualizedSectionList<
const listHeaderOffset = this.props.ListHeaderComponent ? 1 : 0;
const stickyHeaderIndices = this.props.stickySectionHeadersEnabled
? []
? ([]: Array<number>)
: undefined;
let itemCount = 0;
@@ -239,6 +237,7 @@ class VirtualizedSectionList<
return null;
}
// $FlowFixMe[missing-local-annot]
_keyExtractor = (item: Item, index: number) => {
const info = this._subExtractor(index);
return (info && info.key) || String(index);
@@ -342,7 +341,8 @@ class VirtualizedSectionList<
};
_renderItem =
(listItemCount: number) =>
(listItemCount: number): $FlowFixMe =>
// eslint-disable-next-line react/no-unstable-nested-components
({item, index}: {item: Item, index: number, ...}) => {
const info = this._subExtractor(index);
if (!info) {
@@ -394,29 +394,33 @@ class VirtualizedSectionList<
}
};
_updatePropsFor = (cellKey, value) => {
_updatePropsFor = (cellKey: string, value: any) => {
const updateProps = this._updatePropsMap[cellKey];
if (updateProps != null) {
updateProps(value);
}
};
_updateHighlightFor = (cellKey, value) => {
_updateHighlightFor = (cellKey: string, value: boolean) => {
const updateHighlight = this._updateHighlightMap[cellKey];
if (updateHighlight != null) {
updateHighlight(value);
}
};
_setUpdateHighlightFor = (cellKey, updateHighlightFn) => {
_setUpdateHighlightFor = (
cellKey: string,
updateHighlightFn: ?(boolean) => void,
) => {
if (updateHighlightFn != null) {
this._updateHighlightMap[cellKey] = updateHighlightFn;
} else {
// $FlowFixMe[prop-missing]
delete this._updateHighlightFor[cellKey];
}
};
_setUpdatePropsFor = (cellKey, updatePropsFn) => {
_setUpdatePropsFor = (cellKey: string, updatePropsFn: ?(boolean) => void) => {
if (updatePropsFn != null) {
this._updatePropsMap[cellKey] = updatePropsFn;
} else {
@@ -448,10 +452,10 @@ class VirtualizedSectionList<
return null;
}
_updateHighlightMap = {};
_updatePropsMap = {};
_updateHighlightMap: {[string]: (boolean) => void} = {};
_updatePropsMap: {[string]: void | (boolean => void)} = {};
_listRef: ?React.ElementRef<typeof VirtualizedList>;
_captureRef = ref => {
_captureRef = (ref: null | React$ElementRef<Class<VirtualizedList>>) => {
this._listRef = ref;
};
}
@@ -525,6 +529,7 @@ function ItemWithSeparator(props: ItemWithSeparatorProps): React.Node {
React.useEffect(() => {
setSelfHighlightCallback(cellKey, setSeparatorHighlighted);
// $FlowFixMe[incompatible-call]
setSelfUpdatePropsCallback(cellKey, setSeparatorProps);
return () => {
@@ -598,4 +603,14 @@ function ItemWithSeparator(props: ItemWithSeparatorProps): React.Node {
);
}
export default VirtualizedSectionList;
/* $FlowFixMe[class-object-subtyping] added when improving typing for this
* parameters */
// $FlowFixMe[method-unbinding]
export default (VirtualizedSectionList: React.AbstractComponent<
React.ElementConfig<typeof VirtualizedSectionList>,
$ReadOnly<{
getListRef: () => ?React.ElementRef<typeof VirtualizedList>,
scrollToLocation: (params: ScrollToLocationParamsType) => void,
...
}>,
>);

View File

@@ -3,13 +3,17 @@
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict
*/
'use strict';
/**
* Intentional info-level logging for clear separation from ad-hoc console debug logging.
*/
function infoLog(...args) {
function infoLog(...args: Array<mixed>): void {
return console.log(...args);
}