mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
feat: Move virtualized lists to @react-native/virtualized-lists (#35406)
Summary: This PR moves `VirtualizedList`, `VirtualizedSectionList`, and its files to a separate package called `react-native/virtualized-lists` located under `packages/virtualized-lists` as proposed on https://github.com/facebook/react-native/issues/35263 ## Changelog [General] [Changed] - Move virtualized lists to react-native/virtualized-lists package Pull Request resolved: https://github.com/facebook/react-native/pull/35406 Test Plan: 1. Open the RNTester app and navigate to `FlatList` or `SectionList` page 2. Test virtualized lists through the many sections https://user-images.githubusercontent.com/11707729/202878843-2b1322f5-cfee-484e-aaf3-d8d4dc0b96cc.mov Reviewed By: cipolleschi Differential Revision: D41745930 Pulled By: hoxyq fbshipit-source-id: d3d33896801fd69448c6893b86fd5c2363144fd0
This commit is contained in:
1 parent
62fa6d9dac
commit
2e3dbe9c2f
46 files changed
+3100
-2927
No files matched your search
@@ -10,7 +10,7 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import type {RenderItemProps} from '../Lists/VirtualizedList';
|
||||
import type {RenderItemProps} from '@react-native/virtualized-lists';
|
||||
|
||||
const ScrollView = require('../Components/ScrollView/ScrollView');
|
||||
const TouchableHighlight = require('../Components/Touchable/TouchableHighlight');
|
||||
|
||||
Vendored
+1
-1
@@ -12,7 +12,7 @@ import type {
|
||||
ListRenderItem,
|
||||
ViewToken,
|
||||
VirtualizedListProps,
|
||||
} from './VirtualizedList';
|
||||
} from '@react-native/virtualized-lists';
|
||||
import type {ScrollViewComponent} from '../Components/ScrollView/ScrollView';
|
||||
import {StyleProp} from '../StyleSheet/StyleSheet';
|
||||
import {ViewStyle} from '../StyleSheet/StyleSheetTypes';
|
||||
|
||||
@@ -11,14 +11,17 @@
|
||||
import typeof ScrollViewNativeComponent from '../Components/ScrollView/ScrollViewNativeComponent';
|
||||
import type {ViewStyleProp} from '../StyleSheet/StyleSheet';
|
||||
import type {
|
||||
RenderItemProps,
|
||||
RenderItemType,
|
||||
ViewabilityConfigCallbackPair,
|
||||
ViewToken,
|
||||
} from './ViewabilityHelper';
|
||||
import type {RenderItemProps, RenderItemType} from './VirtualizedList';
|
||||
} from '@react-native/virtualized-lists';
|
||||
|
||||
import {type ScrollResponderType} from '../Components/ScrollView/ScrollView';
|
||||
import VirtualizedList from './VirtualizedList';
|
||||
import {keyExtractor as defaultKeyExtractor} from './VirtualizeUtils';
|
||||
import {
|
||||
VirtualizedList,
|
||||
keyExtractor as defaultKeyExtractor,
|
||||
} from '@react-native/virtualized-lists';
|
||||
import memoizeOne from 'memoize-one';
|
||||
|
||||
const View = require('../Components/View/View');
|
||||
|
||||
@@ -14,8 +14,13 @@ const View = require('../Components/View/View');
|
||||
import typeof ScrollViewNativeComponent from '../Components/ScrollView/ScrollViewNativeComponent';
|
||||
import {type ScrollResponderType} from '../Components/ScrollView/ScrollView';
|
||||
import type {ViewStyleProp} from '../StyleSheet/StyleSheet';
|
||||
import type {RenderItemType} from './VirtualizedList';
|
||||
import typeof VirtualizedList from './VirtualizedList';
|
||||
import type {
|
||||
RenderItemType,
|
||||
RenderItemProps,
|
||||
ViewToken,
|
||||
ViewabilityConfigCallbackPair,
|
||||
} from '@react-native/virtualized-lists';
|
||||
import {typeof VirtualizedList} from '@react-native/virtualized-lists';
|
||||
|
||||
type RequiredProps<ItemT> = {|
|
||||
/**
|
||||
|
||||
Vendored
+1
-1
@@ -11,7 +11,7 @@ import type * as React from 'react';
|
||||
import type {
|
||||
ListRenderItemInfo,
|
||||
VirtualizedListWithoutRenderItemProps,
|
||||
} from './VirtualizedList';
|
||||
} from '@react-native/virtualized-lists';
|
||||
import type {
|
||||
ScrollView,
|
||||
ScrollViewProps,
|
||||
|
||||
@@ -12,13 +12,13 @@
|
||||
|
||||
import type {ScrollResponderType} from '../Components/ScrollView/ScrollView';
|
||||
import type {
|
||||
Props as VirtualizedSectionListProps,
|
||||
ScrollToLocationParamsType,
|
||||
SectionBase as _SectionBase,
|
||||
} from './VirtualizedSectionList';
|
||||
VirtualizedSectionListProps,
|
||||
} from '@react-native/virtualized-lists';
|
||||
|
||||
import Platform from '../Utilities/Platform';
|
||||
import VirtualizedSectionList from './VirtualizedSectionList';
|
||||
import {VirtualizedSectionList} from '@react-native/virtualized-lists';
|
||||
import * as React from 'react';
|
||||
|
||||
type Item = any;
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
|
||||
import type {ScrollResponderType} from '../Components/ScrollView/ScrollView';
|
||||
import type {
|
||||
Props as VirtualizedSectionListProps,
|
||||
ScrollToLocationParamsType,
|
||||
SectionBase as _SectionBase,
|
||||
} from './VirtualizedSectionList';
|
||||
VirtualizedSectionListProps,
|
||||
} from '@react-native/virtualized-lists';
|
||||
import type {AbstractComponent, Element, ElementRef} from 'react';
|
||||
|
||||
import Platform from '../Utilities/Platform';
|
||||
import VirtualizedSectionList from './VirtualizedSectionList';
|
||||
import {VirtualizedSectionList} from '@react-native/virtualized-lists';
|
||||
import React, {forwardRef, useImperativeHandle, useRef} from 'react';
|
||||
|
||||
type Item = any;
|
||||
|
||||
@@ -10,351 +10,14 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import type {FrameMetricProps} from './VirtualizedListProps';
|
||||
export type {
|
||||
ViewToken,
|
||||
ViewabilityConfigCallbackPair,
|
||||
} from '@react-native/virtualized-lists';
|
||||
|
||||
const invariant = require('invariant');
|
||||
import {typeof ViewabilityHelper as ViewabilityHelperType} from '@react-native/virtualized-lists';
|
||||
|
||||
export type ViewToken = {
|
||||
item: any,
|
||||
key: string,
|
||||
index: ?number,
|
||||
isViewable: boolean,
|
||||
section?: any,
|
||||
...
|
||||
};
|
||||
|
||||
export type ViewabilityConfigCallbackPair = {
|
||||
viewabilityConfig: ViewabilityConfig,
|
||||
onViewableItemsChanged: (info: {
|
||||
viewableItems: Array<ViewToken>,
|
||||
changed: Array<ViewToken>,
|
||||
...
|
||||
}) => void,
|
||||
...
|
||||
};
|
||||
|
||||
export type ViewabilityConfig = {|
|
||||
/**
|
||||
* Minimum amount of time (in milliseconds) that an item must be physically viewable before the
|
||||
* viewability callback will be fired. A high number means that scrolling through content without
|
||||
* stopping will not mark the content as viewable.
|
||||
*/
|
||||
minimumViewTime?: number,
|
||||
|
||||
/**
|
||||
* Percent of viewport that must be covered for a partially occluded item to count as
|
||||
* "viewable", 0-100. Fully visible items are always considered viewable. A value of 0 means
|
||||
* that a single pixel in the viewport makes the item viewable, and a value of 100 means that
|
||||
* an item must be either entirely visible or cover the entire viewport to count as viewable.
|
||||
*/
|
||||
viewAreaCoveragePercentThreshold?: number,
|
||||
|
||||
/**
|
||||
* Similar to `viewAreaPercentThreshold`, but considers the percent of the item that is visible,
|
||||
* rather than the fraction of the viewable area it covers.
|
||||
*/
|
||||
itemVisiblePercentThreshold?: number,
|
||||
|
||||
/**
|
||||
* Nothing is considered viewable until the user scrolls or `recordInteraction` is called after
|
||||
* render.
|
||||
*/
|
||||
waitForInteraction?: boolean,
|
||||
|};
|
||||
|
||||
/**
|
||||
* A Utility class for calculating viewable items based on current metrics like scroll position and
|
||||
* layout.
|
||||
*
|
||||
* An item is said to be in a "viewable" state when any of the following
|
||||
* is true for longer than `minimumViewTime` milliseconds (after an interaction if `waitForInteraction`
|
||||
* is true):
|
||||
*
|
||||
* - Occupying >= `viewAreaCoveragePercentThreshold` of the view area XOR fraction of the item
|
||||
* visible in the view area >= `itemVisiblePercentThreshold`.
|
||||
* - Entirely visible on screen
|
||||
*/
|
||||
class ViewabilityHelper {
|
||||
_config: ViewabilityConfig;
|
||||
_hasInteracted: boolean = false;
|
||||
_timers: Set<number> = new Set();
|
||||
_viewableIndices: Array<number> = [];
|
||||
_viewableItems: Map<string, ViewToken> = new Map();
|
||||
|
||||
constructor(
|
||||
config: ViewabilityConfig = {viewAreaCoveragePercentThreshold: 0},
|
||||
) {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which items are viewable based on the current metrics and config.
|
||||
*/
|
||||
computeViewableItems(
|
||||
props: FrameMetricProps,
|
||||
scrollOffset: number,
|
||||
viewportHeight: number,
|
||||
getFrameMetrics: (
|
||||
index: number,
|
||||
props: FrameMetricProps,
|
||||
) => ?{
|
||||
length: number,
|
||||
offset: number,
|
||||
...
|
||||
},
|
||||
// Optional optimization to reduce the scan size
|
||||
renderRange?: {
|
||||
first: number,
|
||||
last: number,
|
||||
...
|
||||
},
|
||||
): Array<number> {
|
||||
const itemCount = props.getItemCount(props.data);
|
||||
const {itemVisiblePercentThreshold, viewAreaCoveragePercentThreshold} =
|
||||
this._config;
|
||||
const viewAreaMode = viewAreaCoveragePercentThreshold != null;
|
||||
const viewablePercentThreshold = viewAreaMode
|
||||
? viewAreaCoveragePercentThreshold
|
||||
: itemVisiblePercentThreshold;
|
||||
invariant(
|
||||
viewablePercentThreshold != null &&
|
||||
(itemVisiblePercentThreshold != null) !==
|
||||
(viewAreaCoveragePercentThreshold != null),
|
||||
'Must set exactly one of itemVisiblePercentThreshold or viewAreaCoveragePercentThreshold',
|
||||
);
|
||||
const viewableIndices = [];
|
||||
if (itemCount === 0) {
|
||||
return viewableIndices;
|
||||
}
|
||||
let firstVisible = -1;
|
||||
const {first, last} = renderRange || {first: 0, last: itemCount - 1};
|
||||
if (last >= itemCount) {
|
||||
console.warn(
|
||||
'Invalid render range computing viewability ' +
|
||||
JSON.stringify({renderRange, itemCount}),
|
||||
);
|
||||
return [];
|
||||
}
|
||||
for (let idx = first; idx <= last; idx++) {
|
||||
const metrics = getFrameMetrics(idx, props);
|
||||
if (!metrics) {
|
||||
continue;
|
||||
}
|
||||
const top = metrics.offset - scrollOffset;
|
||||
const bottom = top + metrics.length;
|
||||
if (top < viewportHeight && bottom > 0) {
|
||||
firstVisible = idx;
|
||||
if (
|
||||
_isViewable(
|
||||
viewAreaMode,
|
||||
viewablePercentThreshold,
|
||||
top,
|
||||
bottom,
|
||||
viewportHeight,
|
||||
metrics.length,
|
||||
)
|
||||
) {
|
||||
viewableIndices.push(idx);
|
||||
}
|
||||
} else if (firstVisible >= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return viewableIndices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Figures out which items are viewable and how that has changed from before and calls
|
||||
* `onViewableItemsChanged` as appropriate.
|
||||
*/
|
||||
onUpdate(
|
||||
props: FrameMetricProps,
|
||||
scrollOffset: number,
|
||||
viewportHeight: number,
|
||||
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,
|
||||
// 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, props)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let viewableIndices: Array<number> = [];
|
||||
if (itemCount) {
|
||||
viewableIndices = this.computeViewableItems(
|
||||
props,
|
||||
scrollOffset,
|
||||
viewportHeight,
|
||||
getFrameMetrics,
|
||||
renderRange,
|
||||
);
|
||||
}
|
||||
if (
|
||||
this._viewableIndices.length === viewableIndices.length &&
|
||||
this._viewableIndices.every((v, ii) => v === viewableIndices[ii])
|
||||
) {
|
||||
// We might get a lot of scroll events where visibility doesn't change and we don't want to do
|
||||
// extra work in those cases.
|
||||
return;
|
||||
}
|
||||
this._viewableIndices = viewableIndices;
|
||||
if (this._config.minimumViewTime) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* clean-up cached _viewableIndices to evaluate changed items on next update
|
||||
*/
|
||||
resetViewableIndices() {
|
||||
this._viewableIndices = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Records that an interaction has happened even if there has been no scroll.
|
||||
*/
|
||||
recordInteraction() {
|
||||
this._hasInteracted = true;
|
||||
}
|
||||
|
||||
_onUpdateSync(
|
||||
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 =>
|
||||
this._viewableIndices.includes(ii),
|
||||
);
|
||||
const prevItems = this._viewableItems;
|
||||
const nextItems = new Map(
|
||||
viewableIndicesToCheck.map(ii => {
|
||||
const viewable = createViewToken(ii, true, props);
|
||||
return [viewable.key, viewable];
|
||||
}),
|
||||
);
|
||||
|
||||
const changed = [];
|
||||
for (const [key, viewable] of nextItems) {
|
||||
if (!prevItems.has(key)) {
|
||||
changed.push(viewable);
|
||||
}
|
||||
}
|
||||
for (const [key, viewable] of prevItems) {
|
||||
if (!nextItems.has(key)) {
|
||||
changed.push({...viewable, isViewable: false});
|
||||
}
|
||||
}
|
||||
if (changed.length > 0) {
|
||||
this._viewableItems = nextItems;
|
||||
onViewableItemsChanged({
|
||||
viewableItems: Array.from(nextItems.values()),
|
||||
changed,
|
||||
viewabilityConfig: this._config,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _isViewable(
|
||||
viewAreaMode: boolean,
|
||||
viewablePercentThreshold: number,
|
||||
top: number,
|
||||
bottom: number,
|
||||
viewportHeight: number,
|
||||
itemLength: number,
|
||||
): boolean {
|
||||
if (_isEntirelyVisible(top, bottom, viewportHeight)) {
|
||||
return true;
|
||||
} else {
|
||||
const pixels = _getPixelsVisible(top, bottom, viewportHeight);
|
||||
const percent =
|
||||
100 * (viewAreaMode ? pixels / viewportHeight : pixels / itemLength);
|
||||
return percent >= viewablePercentThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
function _getPixelsVisible(
|
||||
top: number,
|
||||
bottom: number,
|
||||
viewportHeight: number,
|
||||
): number {
|
||||
const visibleHeight = Math.min(bottom, viewportHeight) - Math.max(top, 0);
|
||||
return Math.max(0, visibleHeight);
|
||||
}
|
||||
|
||||
function _isEntirelyVisible(
|
||||
top: number,
|
||||
bottom: number,
|
||||
viewportHeight: number,
|
||||
): boolean {
|
||||
return top >= 0 && bottom <= viewportHeight && bottom > top;
|
||||
}
|
||||
const ViewabilityHelper: ViewabilityHelperType =
|
||||
require('@react-native/virtualized-lists').ViewabilityHelper;
|
||||
|
||||
module.exports = ViewabilityHelper;
|
||||
+10
-1939
File diff suppressed because it is too large.
Load diff
@@ -8,610 +8,11 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import type {ViewToken} from './ViewabilityHelper';
|
||||
'use strict';
|
||||
|
||||
import View from '../Components/View/View';
|
||||
import VirtualizedList from './VirtualizedList';
|
||||
import {keyExtractor as defaultKeyExtractor} from './VirtualizeUtils';
|
||||
import invariant from 'invariant';
|
||||
import * as React from 'react';
|
||||
import {typeof VirtualizedSectionList as VirtualizedSectionListType} from '@react-native/virtualized-lists';
|
||||
|
||||
type Item = any;
|
||||
const VirtualizedSectionList: VirtualizedSectionListType =
|
||||
require('@react-native/virtualized-lists').VirtualizedSectionList;
|
||||
|
||||
export type SectionBase<SectionItemT> = {
|
||||
/**
|
||||
* The data for rendering items in this section.
|
||||
*/
|
||||
data: $ReadOnlyArray<SectionItemT>,
|
||||
/**
|
||||
* Optional key to keep track of section re-ordering. If you don't plan on re-ordering sections,
|
||||
* the array index will be used by default.
|
||||
*/
|
||||
key?: string,
|
||||
// Optional props will override list-wide props just for this section.
|
||||
renderItem?: ?(info: {
|
||||
item: SectionItemT,
|
||||
index: number,
|
||||
section: SectionBase<SectionItemT>,
|
||||
separators: {
|
||||
highlight: () => void,
|
||||
unhighlight: () => void,
|
||||
updateProps: (select: 'leading' | 'trailing', newProps: Object) => void,
|
||||
...
|
||||
},
|
||||
...
|
||||
}) => null | React.Element<any>,
|
||||
ItemSeparatorComponent?: ?React.ComponentType<any>,
|
||||
keyExtractor?: (item: SectionItemT, index?: ?number) => string,
|
||||
...
|
||||
};
|
||||
|
||||
type RequiredProps<SectionT: SectionBase<any>> = {|
|
||||
sections: $ReadOnlyArray<SectionT>,
|
||||
|};
|
||||
|
||||
type OptionalProps<SectionT: SectionBase<any>> = {|
|
||||
/**
|
||||
* Default renderer for every item in every section.
|
||||
*/
|
||||
renderItem?: (info: {
|
||||
item: Item,
|
||||
index: number,
|
||||
section: SectionT,
|
||||
separators: {
|
||||
highlight: () => void,
|
||||
unhighlight: () => void,
|
||||
updateProps: (select: 'leading' | 'trailing', newProps: Object) => void,
|
||||
...
|
||||
},
|
||||
...
|
||||
}) => null | React.Element<any>,
|
||||
/**
|
||||
* Rendered at the top of each section. These stick to the top of the `ScrollView` by default on
|
||||
* iOS. See `stickySectionHeadersEnabled`.
|
||||
*/
|
||||
renderSectionHeader?: ?(info: {
|
||||
section: SectionT,
|
||||
...
|
||||
}) => null | React.Element<any>,
|
||||
/**
|
||||
* Rendered at the bottom of each section.
|
||||
*/
|
||||
renderSectionFooter?: ?(info: {
|
||||
section: SectionT,
|
||||
...
|
||||
}) => null | React.Element<any>,
|
||||
/**
|
||||
* Rendered at the top and bottom of each section (note this is different from
|
||||
* `ItemSeparatorComponent` which is only rendered between items). These are intended to separate
|
||||
* sections from the headers above and below and typically have the same highlight response as
|
||||
* `ItemSeparatorComponent`. Also receives `highlighted`, `[leading/trailing][Item/Separator]`,
|
||||
* and any custom props from `separators.updateProps`.
|
||||
*/
|
||||
SectionSeparatorComponent?: ?React.ComponentType<any>,
|
||||
/**
|
||||
* Makes section headers stick to the top of the screen until the next one pushes it off. Only
|
||||
* enabled by default on iOS because that is the platform standard there.
|
||||
*/
|
||||
stickySectionHeadersEnabled?: boolean,
|
||||
onEndReached?: ?({distanceFromEnd: number, ...}) => void,
|
||||
|};
|
||||
|
||||
type VirtualizedListProps = React.ElementConfig<typeof VirtualizedList>;
|
||||
|
||||
export type Props<SectionT> = {|
|
||||
...RequiredProps<SectionT>,
|
||||
...OptionalProps<SectionT>,
|
||||
...$Diff<
|
||||
VirtualizedListProps,
|
||||
{
|
||||
renderItem: $PropertyType<VirtualizedListProps, 'renderItem'>,
|
||||
data: $PropertyType<VirtualizedListProps, 'data'>,
|
||||
...
|
||||
},
|
||||
>,
|
||||
|};
|
||||
export type ScrollToLocationParamsType = {|
|
||||
animated?: ?boolean,
|
||||
itemIndex: number,
|
||||
sectionIndex: number,
|
||||
viewOffset?: number,
|
||||
viewPosition?: number,
|
||||
|};
|
||||
|
||||
type State = {childProps: VirtualizedListProps, ...};
|
||||
|
||||
/**
|
||||
* Right now this just flattens everything into one list and uses VirtualizedList under the
|
||||
* hood. The only operation that might not scale well is concatting the data arrays of all the
|
||||
* sections when new props are received, which should be plenty fast for up to ~10,000 items.
|
||||
*/
|
||||
class VirtualizedSectionList<
|
||||
SectionT: SectionBase<any>,
|
||||
> extends React.PureComponent<Props<SectionT>, State> {
|
||||
scrollToLocation(params: ScrollToLocationParamsType) {
|
||||
let index = params.itemIndex;
|
||||
for (let i = 0; i < params.sectionIndex; i++) {
|
||||
index += this.props.getItemCount(this.props.sections[i].data) + 2;
|
||||
}
|
||||
let viewOffset = params.viewOffset || 0;
|
||||
if (this._listRef == null) {
|
||||
return;
|
||||
}
|
||||
if (params.itemIndex > 0 && this.props.stickySectionHeadersEnabled) {
|
||||
const frame = this._listRef.__getFrameMetricsApprox(
|
||||
index - params.itemIndex,
|
||||
this._listRef.props,
|
||||
);
|
||||
viewOffset += frame.length;
|
||||
}
|
||||
const toIndexParams = {
|
||||
...params,
|
||||
viewOffset,
|
||||
index,
|
||||
};
|
||||
// $FlowFixMe[incompatible-use]
|
||||
this._listRef.scrollToIndex(toIndexParams);
|
||||
}
|
||||
|
||||
getListRef(): ?React.ElementRef<typeof VirtualizedList> {
|
||||
return this._listRef;
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
const {
|
||||
ItemSeparatorComponent, // don't pass through, rendered with renderItem
|
||||
SectionSeparatorComponent,
|
||||
renderItem: _renderItem,
|
||||
renderSectionFooter,
|
||||
renderSectionHeader,
|
||||
sections: _sections,
|
||||
stickySectionHeadersEnabled,
|
||||
...passThroughProps
|
||||
} = this.props;
|
||||
|
||||
const listHeaderOffset = this.props.ListHeaderComponent ? 1 : 0;
|
||||
|
||||
const stickyHeaderIndices = this.props.stickySectionHeadersEnabled
|
||||
? ([]: Array<number>)
|
||||
: undefined;
|
||||
|
||||
let itemCount = 0;
|
||||
for (const section of this.props.sections) {
|
||||
// Track the section header indices
|
||||
if (stickyHeaderIndices != null) {
|
||||
stickyHeaderIndices.push(itemCount + listHeaderOffset);
|
||||
}
|
||||
|
||||
// Add two for the section header and footer.
|
||||
itemCount += 2;
|
||||
itemCount += this.props.getItemCount(section.data);
|
||||
}
|
||||
const renderItem = this._renderItem(itemCount);
|
||||
|
||||
return (
|
||||
<VirtualizedList
|
||||
{...passThroughProps}
|
||||
keyExtractor={this._keyExtractor}
|
||||
stickyHeaderIndices={stickyHeaderIndices}
|
||||
renderItem={renderItem}
|
||||
data={this.props.sections}
|
||||
getItem={(sections, index) =>
|
||||
this._getItem(this.props, sections, index)
|
||||
}
|
||||
getItemCount={() => itemCount}
|
||||
onViewableItemsChanged={
|
||||
this.props.onViewableItemsChanged
|
||||
? this._onViewableItemsChanged
|
||||
: undefined
|
||||
}
|
||||
ref={this._captureRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_getItem(
|
||||
props: Props<SectionT>,
|
||||
sections: ?$ReadOnlyArray<Item>,
|
||||
index: number,
|
||||
): ?Item {
|
||||
if (!sections) {
|
||||
return null;
|
||||
}
|
||||
let itemIdx = index - 1;
|
||||
for (let i = 0; i < sections.length; i++) {
|
||||
const section = sections[i];
|
||||
const sectionData = section.data;
|
||||
const itemCount = props.getItemCount(sectionData);
|
||||
if (itemIdx === -1 || itemIdx === itemCount) {
|
||||
// We intend for there to be overflow by one on both ends of the list.
|
||||
// This will be for headers and footers. When returning a header or footer
|
||||
// item the section itself is the item.
|
||||
return section;
|
||||
} else if (itemIdx < itemCount) {
|
||||
// If we are in the bounds of the list's data then return the item.
|
||||
return props.getItem(sectionData, itemIdx);
|
||||
} else {
|
||||
itemIdx -= itemCount + 2; // Add two for the header and footer
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// $FlowFixMe[missing-local-annot]
|
||||
_keyExtractor = (item: Item, index: number) => {
|
||||
const info = this._subExtractor(index);
|
||||
return (info && info.key) || String(index);
|
||||
};
|
||||
|
||||
_subExtractor(index: number): ?{
|
||||
section: SectionT,
|
||||
// Key of the section or combined key for section + item
|
||||
key: string,
|
||||
// Relative index within the section
|
||||
index: ?number,
|
||||
// True if this is the section header
|
||||
header?: ?boolean,
|
||||
leadingItem?: ?Item,
|
||||
leadingSection?: ?SectionT,
|
||||
trailingItem?: ?Item,
|
||||
trailingSection?: ?SectionT,
|
||||
...
|
||||
} {
|
||||
let itemIndex = index;
|
||||
const {getItem, getItemCount, keyExtractor, sections} = this.props;
|
||||
for (let i = 0; i < sections.length; i++) {
|
||||
const section = sections[i];
|
||||
const sectionData = section.data;
|
||||
const key = section.key || String(i);
|
||||
itemIndex -= 1; // The section adds an item for the header
|
||||
if (itemIndex >= getItemCount(sectionData) + 1) {
|
||||
itemIndex -= getItemCount(sectionData) + 1; // The section adds an item for the footer.
|
||||
} else if (itemIndex === -1) {
|
||||
return {
|
||||
section,
|
||||
key: key + ':header',
|
||||
index: null,
|
||||
header: true,
|
||||
trailingSection: sections[i + 1],
|
||||
};
|
||||
} else if (itemIndex === getItemCount(sectionData)) {
|
||||
return {
|
||||
section,
|
||||
key: key + ':footer',
|
||||
index: null,
|
||||
header: false,
|
||||
trailingSection: sections[i + 1],
|
||||
};
|
||||
} else {
|
||||
const extractor =
|
||||
section.keyExtractor || keyExtractor || defaultKeyExtractor;
|
||||
return {
|
||||
section,
|
||||
key:
|
||||
key + ':' + extractor(getItem(sectionData, itemIndex), itemIndex),
|
||||
index: itemIndex,
|
||||
leadingItem: getItem(sectionData, itemIndex - 1),
|
||||
leadingSection: sections[i - 1],
|
||||
trailingItem: getItem(sectionData, itemIndex + 1),
|
||||
trailingSection: sections[i + 1],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_convertViewable = (viewable: ViewToken): ?ViewToken => {
|
||||
invariant(viewable.index != null, 'Received a broken ViewToken');
|
||||
const info = this._subExtractor(viewable.index);
|
||||
if (!info) {
|
||||
return null;
|
||||
}
|
||||
const keyExtractorWithNullableIndex = info.section.keyExtractor;
|
||||
const keyExtractorWithNonNullableIndex =
|
||||
this.props.keyExtractor || defaultKeyExtractor;
|
||||
const key =
|
||||
keyExtractorWithNullableIndex != null
|
||||
? keyExtractorWithNullableIndex(viewable.item, info.index)
|
||||
: keyExtractorWithNonNullableIndex(viewable.item, info.index ?? 0);
|
||||
|
||||
return {
|
||||
...viewable,
|
||||
index: info.index,
|
||||
key,
|
||||
section: info.section,
|
||||
};
|
||||
};
|
||||
|
||||
_onViewableItemsChanged = ({
|
||||
viewableItems,
|
||||
changed,
|
||||
}: {
|
||||
viewableItems: Array<ViewToken>,
|
||||
changed: Array<ViewToken>,
|
||||
...
|
||||
}) => {
|
||||
const onViewableItemsChanged = this.props.onViewableItemsChanged;
|
||||
if (onViewableItemsChanged != null) {
|
||||
onViewableItemsChanged({
|
||||
viewableItems: viewableItems
|
||||
.map(this._convertViewable, this)
|
||||
.filter(Boolean),
|
||||
changed: changed.map(this._convertViewable, this).filter(Boolean),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_renderItem =
|
||||
(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) {
|
||||
return null;
|
||||
}
|
||||
const infoIndex = info.index;
|
||||
if (infoIndex == null) {
|
||||
const {section} = info;
|
||||
if (info.header === true) {
|
||||
const {renderSectionHeader} = this.props;
|
||||
return renderSectionHeader ? renderSectionHeader({section}) : null;
|
||||
} else {
|
||||
const {renderSectionFooter} = this.props;
|
||||
return renderSectionFooter ? renderSectionFooter({section}) : null;
|
||||
}
|
||||
} else {
|
||||
const renderItem = info.section.renderItem || this.props.renderItem;
|
||||
const SeparatorComponent = this._getSeparatorComponent(
|
||||
index,
|
||||
info,
|
||||
listItemCount,
|
||||
);
|
||||
invariant(renderItem, 'no renderItem!');
|
||||
return (
|
||||
<ItemWithSeparator
|
||||
SeparatorComponent={SeparatorComponent}
|
||||
LeadingSeparatorComponent={
|
||||
infoIndex === 0 ? this.props.SectionSeparatorComponent : undefined
|
||||
}
|
||||
cellKey={info.key}
|
||||
index={infoIndex}
|
||||
item={item}
|
||||
leadingItem={info.leadingItem}
|
||||
leadingSection={info.leadingSection}
|
||||
prevCellKey={(this._subExtractor(index - 1) || {}).key}
|
||||
// Callback to provide updateHighlight for this item
|
||||
setSelfHighlightCallback={this._setUpdateHighlightFor}
|
||||
setSelfUpdatePropsCallback={this._setUpdatePropsFor}
|
||||
// Provide child ability to set highlight/updateProps for previous item using prevCellKey
|
||||
updateHighlightFor={this._updateHighlightFor}
|
||||
updatePropsFor={this._updatePropsFor}
|
||||
renderItem={renderItem}
|
||||
section={info.section}
|
||||
trailingItem={info.trailingItem}
|
||||
trailingSection={info.trailingSection}
|
||||
inverted={!!this.props.inverted}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
_updatePropsFor = (cellKey: string, value: any) => {
|
||||
const updateProps = this._updatePropsMap[cellKey];
|
||||
if (updateProps != null) {
|
||||
updateProps(value);
|
||||
}
|
||||
};
|
||||
|
||||
_updateHighlightFor = (cellKey: string, value: boolean) => {
|
||||
const updateHighlight = this._updateHighlightMap[cellKey];
|
||||
if (updateHighlight != null) {
|
||||
updateHighlight(value);
|
||||
}
|
||||
};
|
||||
|
||||
_setUpdateHighlightFor = (
|
||||
cellKey: string,
|
||||
updateHighlightFn: ?(boolean) => void,
|
||||
) => {
|
||||
if (updateHighlightFn != null) {
|
||||
this._updateHighlightMap[cellKey] = updateHighlightFn;
|
||||
} else {
|
||||
// $FlowFixMe[prop-missing]
|
||||
delete this._updateHighlightFor[cellKey];
|
||||
}
|
||||
};
|
||||
|
||||
_setUpdatePropsFor = (cellKey: string, updatePropsFn: ?(boolean) => void) => {
|
||||
if (updatePropsFn != null) {
|
||||
this._updatePropsMap[cellKey] = updatePropsFn;
|
||||
} else {
|
||||
delete this._updatePropsMap[cellKey];
|
||||
}
|
||||
};
|
||||
|
||||
_getSeparatorComponent(
|
||||
index: number,
|
||||
info?: ?Object,
|
||||
listItemCount: number,
|
||||
): ?React.ComponentType<any> {
|
||||
info = info || this._subExtractor(index);
|
||||
if (!info) {
|
||||
return null;
|
||||
}
|
||||
const ItemSeparatorComponent =
|
||||
info.section.ItemSeparatorComponent || this.props.ItemSeparatorComponent;
|
||||
const {SectionSeparatorComponent} = this.props;
|
||||
const isLastItemInList = index === listItemCount - 1;
|
||||
const isLastItemInSection =
|
||||
info.index === this.props.getItemCount(info.section.data) - 1;
|
||||
if (SectionSeparatorComponent && isLastItemInSection) {
|
||||
return SectionSeparatorComponent;
|
||||
}
|
||||
if (ItemSeparatorComponent && !isLastItemInSection && !isLastItemInList) {
|
||||
return ItemSeparatorComponent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_updateHighlightMap: {[string]: (boolean) => void} = {};
|
||||
_updatePropsMap: {[string]: void | (boolean => void)} = {};
|
||||
_listRef: ?React.ElementRef<typeof VirtualizedList>;
|
||||
_captureRef = (ref: null | React$ElementRef<Class<VirtualizedList>>) => {
|
||||
this._listRef = ref;
|
||||
};
|
||||
}
|
||||
|
||||
type ItemWithSeparatorCommonProps = $ReadOnly<{|
|
||||
leadingItem: ?Item,
|
||||
leadingSection: ?Object,
|
||||
section: Object,
|
||||
trailingItem: ?Item,
|
||||
trailingSection: ?Object,
|
||||
|}>;
|
||||
|
||||
type ItemWithSeparatorProps = $ReadOnly<{|
|
||||
...ItemWithSeparatorCommonProps,
|
||||
LeadingSeparatorComponent: ?React.ComponentType<any>,
|
||||
SeparatorComponent: ?React.ComponentType<any>,
|
||||
cellKey: string,
|
||||
index: number,
|
||||
item: Item,
|
||||
setSelfHighlightCallback: (
|
||||
cellKey: string,
|
||||
updateFn: ?(boolean) => void,
|
||||
) => void,
|
||||
setSelfUpdatePropsCallback: (
|
||||
cellKey: string,
|
||||
updateFn: ?(boolean) => void,
|
||||
) => void,
|
||||
prevCellKey?: ?string,
|
||||
updateHighlightFor: (prevCellKey: string, value: boolean) => void,
|
||||
updatePropsFor: (prevCellKey: string, value: Object) => void,
|
||||
renderItem: Function,
|
||||
inverted: boolean,
|
||||
|}>;
|
||||
|
||||
function ItemWithSeparator(props: ItemWithSeparatorProps): React.Node {
|
||||
const {
|
||||
LeadingSeparatorComponent,
|
||||
// this is the trailing separator and is associated with this item
|
||||
SeparatorComponent,
|
||||
cellKey,
|
||||
prevCellKey,
|
||||
setSelfHighlightCallback,
|
||||
updateHighlightFor,
|
||||
setSelfUpdatePropsCallback,
|
||||
updatePropsFor,
|
||||
item,
|
||||
index,
|
||||
section,
|
||||
inverted,
|
||||
} = props;
|
||||
|
||||
const [leadingSeparatorHiglighted, setLeadingSeparatorHighlighted] =
|
||||
React.useState(false);
|
||||
|
||||
const [separatorHighlighted, setSeparatorHighlighted] = React.useState(false);
|
||||
|
||||
const [leadingSeparatorProps, setLeadingSeparatorProps] = React.useState({
|
||||
leadingItem: props.leadingItem,
|
||||
leadingSection: props.leadingSection,
|
||||
section: props.section,
|
||||
trailingItem: props.item,
|
||||
trailingSection: props.trailingSection,
|
||||
});
|
||||
const [separatorProps, setSeparatorProps] = React.useState({
|
||||
leadingItem: props.item,
|
||||
leadingSection: props.leadingSection,
|
||||
section: props.section,
|
||||
trailingItem: props.trailingItem,
|
||||
trailingSection: props.trailingSection,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
setSelfHighlightCallback(cellKey, setSeparatorHighlighted);
|
||||
// $FlowFixMe[incompatible-call]
|
||||
setSelfUpdatePropsCallback(cellKey, setSeparatorProps);
|
||||
|
||||
return () => {
|
||||
setSelfUpdatePropsCallback(cellKey, null);
|
||||
setSelfHighlightCallback(cellKey, null);
|
||||
};
|
||||
}, [
|
||||
cellKey,
|
||||
setSelfHighlightCallback,
|
||||
setSeparatorProps,
|
||||
setSelfUpdatePropsCallback,
|
||||
]);
|
||||
|
||||
const separators = {
|
||||
highlight: () => {
|
||||
setLeadingSeparatorHighlighted(true);
|
||||
setSeparatorHighlighted(true);
|
||||
if (prevCellKey != null) {
|
||||
updateHighlightFor(prevCellKey, true);
|
||||
}
|
||||
},
|
||||
unhighlight: () => {
|
||||
setLeadingSeparatorHighlighted(false);
|
||||
setSeparatorHighlighted(false);
|
||||
if (prevCellKey != null) {
|
||||
updateHighlightFor(prevCellKey, false);
|
||||
}
|
||||
},
|
||||
updateProps: (
|
||||
select: 'leading' | 'trailing',
|
||||
newProps: $Shape<ItemWithSeparatorCommonProps>,
|
||||
) => {
|
||||
if (select === 'leading') {
|
||||
if (LeadingSeparatorComponent != null) {
|
||||
setLeadingSeparatorProps({...leadingSeparatorProps, ...newProps});
|
||||
} else if (prevCellKey != null) {
|
||||
// update the previous item's separator
|
||||
updatePropsFor(prevCellKey, {...leadingSeparatorProps, ...newProps});
|
||||
}
|
||||
} else if (select === 'trailing' && SeparatorComponent != null) {
|
||||
setSeparatorProps({...separatorProps, ...newProps});
|
||||
}
|
||||
},
|
||||
};
|
||||
const element = props.renderItem({
|
||||
item,
|
||||
index,
|
||||
section,
|
||||
separators,
|
||||
});
|
||||
const leadingSeparator = LeadingSeparatorComponent != null && (
|
||||
<LeadingSeparatorComponent
|
||||
highlighted={leadingSeparatorHiglighted}
|
||||
{...leadingSeparatorProps}
|
||||
/>
|
||||
);
|
||||
const separator = SeparatorComponent != null && (
|
||||
<SeparatorComponent
|
||||
highlighted={separatorHighlighted}
|
||||
{...separatorProps}
|
||||
/>
|
||||
);
|
||||
return leadingSeparator || separator ? (
|
||||
<View>
|
||||
{inverted === false ? leadingSeparator : separator}
|
||||
{element}
|
||||
{inverted === false ? separator : leadingSeparator}
|
||||
</View>
|
||||
) : (
|
||||
element
|
||||
);
|
||||
}
|
||||
|
||||
/* $FlowFixMe[class-object-subtyping] added when improving typing for this
|
||||
* parameters */
|
||||
// $FlowFixMe[method-unbinding]
|
||||
module.exports = (VirtualizedSectionList: React.AbstractComponent<
|
||||
React.ElementConfig<typeof VirtualizedSectionList>,
|
||||
$ReadOnly<{
|
||||
getListRef: () => ?React.ElementRef<typeof VirtualizedList>,
|
||||
scrollToLocation: (params: ScrollToLocationParamsType) => void,
|
||||
...
|
||||
}>,
|
||||
>);
|
||||
module.exports = VirtualizedSectionList;
|
||||
@@ -13,11 +13,11 @@ import type {RootTag} from '../ReactNative/RootTag';
|
||||
import type {DirectEventHandler} from '../Types/CodegenTypes';
|
||||
|
||||
import NativeEventEmitter from '../EventEmitter/NativeEventEmitter';
|
||||
import {VirtualizedListContextResetter} from '../Lists/VirtualizedListContext.js';
|
||||
import {type EventSubscription} from '../vendor/emitter/EventEmitter';
|
||||
import ModalInjection from './ModalInjection';
|
||||
import NativeModalManager from './NativeModalManager';
|
||||
import RCTModalHostView from './RCTModalHostViewNativeComponent';
|
||||
import {VirtualizedListContextResetter} from '@react-native/virtualized-lists';
|
||||
|
||||
const ScrollView = require('../Components/ScrollView/ScrollView');
|
||||
const View = require('../Components/View/View');
|
||||
|
||||
@@ -15,8 +15,8 @@ import type {ReactTestRenderer as ReactTestRendererType} from 'react-test-render
|
||||
const Switch = require('../Components/Switch/Switch').default;
|
||||
const TextInput = require('../Components/TextInput/TextInput');
|
||||
const View = require('../Components/View/View');
|
||||
const VirtualizedList = require('../Lists/VirtualizedList').default;
|
||||
const Text = require('../Text/Text');
|
||||
const {VirtualizedList} = require('@react-native/virtualized-lists');
|
||||
const React = require('react');
|
||||
const ShallowRenderer = require('react-shallow-renderer');
|
||||
const ReactTestRenderer = require('react-test-renderer');
|
||||
|
||||
@@ -191,7 +191,7 @@ module.exports = {
|
||||
return require('./Libraries/Components/View/View');
|
||||
},
|
||||
get VirtualizedList(): VirtualizedList {
|
||||
return require('./Libraries/Lists/VirtualizedList').default;
|
||||
return require('./Libraries/Lists/VirtualizedList');
|
||||
},
|
||||
get VirtualizedSectionList(): VirtualizedSectionList {
|
||||
return require('./Libraries/Lists/VirtualizedSectionList');
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
"@react-native/gradle-plugin": "^0.72.2",
|
||||
"@react-native/js-polyfills": "^0.72.0",
|
||||
"@react-native/normalize-colors": "^0.72.0",
|
||||
"@react-native/virtualized-lists": "0.72.0",
|
||||
"abort-controller": "^3.0.0",
|
||||
"anser": "^1.4.9",
|
||||
"base64-js": "^1.1.2",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
import type {AnimatedComponentType} from 'react-native/Libraries/Animated/createAnimatedComponent';
|
||||
import typeof FlatListType from 'react-native/Libraries/Lists/FlatList';
|
||||
import type {RenderItemProps} from 'react-native/Libraries/Lists/VirtualizedListProps';
|
||||
import type {RenderItemProps} from 'react-native/Libraries/Lists/VirtualizedList';
|
||||
|
||||
import type {RNTesterModuleExample} from '../../types/RNTesterTypes';
|
||||
import * as React from 'react';
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
import type {ViewToken} from '../../../../../Libraries/Lists/ViewabilityHelper';
|
||||
import type {RenderItemProps} from '../../../../../Libraries/Lists/VirtualizedListProps';
|
||||
import type {ViewToken} from 'react-native/Libraries/Lists/ViewabilityHelper';
|
||||
import type {RenderItemProps} from 'react-native/Libraries/Lists/VirtualizedList';
|
||||
import type {RNTesterModuleExample} from '../../types/RNTesterTypes';
|
||||
|
||||
import RNTesterPage from '../../components/RNTesterPage';
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import type {ViewToken} from '../../../../../Libraries/Lists/ViewabilityHelper';
|
||||
import type {ViewToken} from 'react-native/Libraries/Lists/ViewabilityHelper';
|
||||
import type {RNTesterModuleExample} from '../../types/RNTesterTypes';
|
||||
|
||||
import BaseFlatListExample from './BaseFlatListExample';
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
const InteractionManager = require('./InteractionManager');
|
||||
const {InteractionManager} = require('react-native');
|
||||
|
||||
/**
|
||||
* A simple class for batching up invocations of a low-pri callback. A timeout is set to run the
|
||||
-4
@@ -10,10 +10,6 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
jest
|
||||
.mock('../../vendor/core/ErrorUtils')
|
||||
.mock('../../BatchedBridge/BatchedBridge');
|
||||
|
||||
function expectToBeCalledOnce(fn) {
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,360 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import type {FrameMetricProps} from './VirtualizedListProps';
|
||||
|
||||
const invariant = require('invariant');
|
||||
|
||||
export type ViewToken = {
|
||||
item: any,
|
||||
key: string,
|
||||
index: ?number,
|
||||
isViewable: boolean,
|
||||
section?: any,
|
||||
...
|
||||
};
|
||||
|
||||
export type ViewabilityConfigCallbackPair = {
|
||||
viewabilityConfig: ViewabilityConfig,
|
||||
onViewableItemsChanged: (info: {
|
||||
viewableItems: Array<ViewToken>,
|
||||
changed: Array<ViewToken>,
|
||||
...
|
||||
}) => void,
|
||||
...
|
||||
};
|
||||
|
||||
export type ViewabilityConfig = {|
|
||||
/**
|
||||
* Minimum amount of time (in milliseconds) that an item must be physically viewable before the
|
||||
* viewability callback will be fired. A high number means that scrolling through content without
|
||||
* stopping will not mark the content as viewable.
|
||||
*/
|
||||
minimumViewTime?: number,
|
||||
|
||||
/**
|
||||
* Percent of viewport that must be covered for a partially occluded item to count as
|
||||
* "viewable", 0-100. Fully visible items are always considered viewable. A value of 0 means
|
||||
* that a single pixel in the viewport makes the item viewable, and a value of 100 means that
|
||||
* an item must be either entirely visible or cover the entire viewport to count as viewable.
|
||||
*/
|
||||
viewAreaCoveragePercentThreshold?: number,
|
||||
|
||||
/**
|
||||
* Similar to `viewAreaPercentThreshold`, but considers the percent of the item that is visible,
|
||||
* rather than the fraction of the viewable area it covers.
|
||||
*/
|
||||
itemVisiblePercentThreshold?: number,
|
||||
|
||||
/**
|
||||
* Nothing is considered viewable until the user scrolls or `recordInteraction` is called after
|
||||
* render.
|
||||
*/
|
||||
waitForInteraction?: boolean,
|
||||
|};
|
||||
|
||||
/**
|
||||
* A Utility class for calculating viewable items based on current metrics like scroll position and
|
||||
* layout.
|
||||
*
|
||||
* An item is said to be in a "viewable" state when any of the following
|
||||
* is true for longer than `minimumViewTime` milliseconds (after an interaction if `waitForInteraction`
|
||||
* is true):
|
||||
*
|
||||
* - Occupying >= `viewAreaCoveragePercentThreshold` of the view area XOR fraction of the item
|
||||
* visible in the view area >= `itemVisiblePercentThreshold`.
|
||||
* - Entirely visible on screen
|
||||
*/
|
||||
class ViewabilityHelper {
|
||||
_config: ViewabilityConfig;
|
||||
_hasInteracted: boolean = false;
|
||||
_timers: Set<number> = new Set();
|
||||
_viewableIndices: Array<number> = [];
|
||||
_viewableItems: Map<string, ViewToken> = new Map();
|
||||
|
||||
constructor(
|
||||
config: ViewabilityConfig = {viewAreaCoveragePercentThreshold: 0},
|
||||
) {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which items are viewable based on the current metrics and config.
|
||||
*/
|
||||
computeViewableItems(
|
||||
props: FrameMetricProps,
|
||||
scrollOffset: number,
|
||||
viewportHeight: number,
|
||||
getFrameMetrics: (
|
||||
index: number,
|
||||
props: FrameMetricProps,
|
||||
) => ?{
|
||||
length: number,
|
||||
offset: number,
|
||||
...
|
||||
},
|
||||
// Optional optimization to reduce the scan size
|
||||
renderRange?: {
|
||||
first: number,
|
||||
last: number,
|
||||
...
|
||||
},
|
||||
): Array<number> {
|
||||
const itemCount = props.getItemCount(props.data);
|
||||
const {itemVisiblePercentThreshold, viewAreaCoveragePercentThreshold} =
|
||||
this._config;
|
||||
const viewAreaMode = viewAreaCoveragePercentThreshold != null;
|
||||
const viewablePercentThreshold = viewAreaMode
|
||||
? viewAreaCoveragePercentThreshold
|
||||
: itemVisiblePercentThreshold;
|
||||
invariant(
|
||||
viewablePercentThreshold != null &&
|
||||
(itemVisiblePercentThreshold != null) !==
|
||||
(viewAreaCoveragePercentThreshold != null),
|
||||
'Must set exactly one of itemVisiblePercentThreshold or viewAreaCoveragePercentThreshold',
|
||||
);
|
||||
const viewableIndices = [];
|
||||
if (itemCount === 0) {
|
||||
return viewableIndices;
|
||||
}
|
||||
let firstVisible = -1;
|
||||
const {first, last} = renderRange || {first: 0, last: itemCount - 1};
|
||||
if (last >= itemCount) {
|
||||
console.warn(
|
||||
'Invalid render range computing viewability ' +
|
||||
JSON.stringify({renderRange, itemCount}),
|
||||
);
|
||||
return [];
|
||||
}
|
||||
for (let idx = first; idx <= last; idx++) {
|
||||
const metrics = getFrameMetrics(idx, props);
|
||||
if (!metrics) {
|
||||
continue;
|
||||
}
|
||||
const top = metrics.offset - scrollOffset;
|
||||
const bottom = top + metrics.length;
|
||||
if (top < viewportHeight && bottom > 0) {
|
||||
firstVisible = idx;
|
||||
if (
|
||||
_isViewable(
|
||||
viewAreaMode,
|
||||
viewablePercentThreshold,
|
||||
top,
|
||||
bottom,
|
||||
viewportHeight,
|
||||
metrics.length,
|
||||
)
|
||||
) {
|
||||
viewableIndices.push(idx);
|
||||
}
|
||||
} else if (firstVisible >= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return viewableIndices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Figures out which items are viewable and how that has changed from before and calls
|
||||
* `onViewableItemsChanged` as appropriate.
|
||||
*/
|
||||
onUpdate(
|
||||
props: FrameMetricProps,
|
||||
scrollOffset: number,
|
||||
viewportHeight: number,
|
||||
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,
|
||||
// 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, props)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let viewableIndices: Array<number> = [];
|
||||
if (itemCount) {
|
||||
viewableIndices = this.computeViewableItems(
|
||||
props,
|
||||
scrollOffset,
|
||||
viewportHeight,
|
||||
getFrameMetrics,
|
||||
renderRange,
|
||||
);
|
||||
}
|
||||
if (
|
||||
this._viewableIndices.length === viewableIndices.length &&
|
||||
this._viewableIndices.every((v, ii) => v === viewableIndices[ii])
|
||||
) {
|
||||
// We might get a lot of scroll events where visibility doesn't change and we don't want to do
|
||||
// extra work in those cases.
|
||||
return;
|
||||
}
|
||||
this._viewableIndices = viewableIndices;
|
||||
if (this._config.minimumViewTime) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* clean-up cached _viewableIndices to evaluate changed items on next update
|
||||
*/
|
||||
resetViewableIndices() {
|
||||
this._viewableIndices = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Records that an interaction has happened even if there has been no scroll.
|
||||
*/
|
||||
recordInteraction() {
|
||||
this._hasInteracted = true;
|
||||
}
|
||||
|
||||
_onUpdateSync(
|
||||
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 =>
|
||||
this._viewableIndices.includes(ii),
|
||||
);
|
||||
const prevItems = this._viewableItems;
|
||||
const nextItems = new Map(
|
||||
viewableIndicesToCheck.map(ii => {
|
||||
const viewable = createViewToken(ii, true, props);
|
||||
return [viewable.key, viewable];
|
||||
}),
|
||||
);
|
||||
|
||||
const changed = [];
|
||||
for (const [key, viewable] of nextItems) {
|
||||
if (!prevItems.has(key)) {
|
||||
changed.push(viewable);
|
||||
}
|
||||
}
|
||||
for (const [key, viewable] of prevItems) {
|
||||
if (!nextItems.has(key)) {
|
||||
changed.push({...viewable, isViewable: false});
|
||||
}
|
||||
}
|
||||
if (changed.length > 0) {
|
||||
this._viewableItems = nextItems;
|
||||
onViewableItemsChanged({
|
||||
viewableItems: Array.from(nextItems.values()),
|
||||
changed,
|
||||
viewabilityConfig: this._config,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _isViewable(
|
||||
viewAreaMode: boolean,
|
||||
viewablePercentThreshold: number,
|
||||
top: number,
|
||||
bottom: number,
|
||||
viewportHeight: number,
|
||||
itemLength: number,
|
||||
): boolean {
|
||||
if (_isEntirelyVisible(top, bottom, viewportHeight)) {
|
||||
return true;
|
||||
} else {
|
||||
const pixels = _getPixelsVisible(top, bottom, viewportHeight);
|
||||
const percent =
|
||||
100 * (viewAreaMode ? pixels / viewportHeight : pixels / itemLength);
|
||||
return percent >= viewablePercentThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
function _getPixelsVisible(
|
||||
top: number,
|
||||
bottom: number,
|
||||
viewportHeight: number,
|
||||
): number {
|
||||
const visibleHeight = Math.min(bottom, viewportHeight) - Math.max(top, 0);
|
||||
return Math.max(0, visibleHeight);
|
||||
}
|
||||
|
||||
function _isEntirelyVisible(
|
||||
top: number,
|
||||
bottom: number,
|
||||
viewportHeight: number,
|
||||
): boolean {
|
||||
return top >= 0 && bottom <= viewportHeight && bottom > top;
|
||||
}
|
||||
|
||||
module.exports = ViewabilityHelper;
|
||||
File renamed without changes.
Vendored
+6
-6
@@ -8,15 +8,15 @@
|
||||
*/
|
||||
|
||||
import type * as React from 'react';
|
||||
import type {LayoutChangeEvent} from '../../types';
|
||||
import {StyleProp} from '../StyleSheet/StyleSheet';
|
||||
import {ViewStyle} from '../StyleSheet/StyleSheetTypes';
|
||||
import type {
|
||||
StyleProp,
|
||||
ViewStyle,
|
||||
ScrollViewProps,
|
||||
LayoutChangeEvent,
|
||||
View,
|
||||
ScrollResponderMixin,
|
||||
ScrollView,
|
||||
ScrollViewProps,
|
||||
} from '../Components/ScrollView/ScrollView';
|
||||
import type {View} from '../Components/View/View';
|
||||
} from 'react-native';
|
||||
|
||||
export interface ViewToken {
|
||||
item: any;
|
||||
File diff suppressed because it is too large.
Load diff
+6
-4
@@ -8,13 +8,15 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import type {ViewStyleProp} from '../StyleSheet/StyleSheet';
|
||||
import type {FocusEvent, LayoutEvent} from '../Types/CoreEventTypes';
|
||||
import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet';
|
||||
import type {
|
||||
FocusEvent,
|
||||
LayoutEvent,
|
||||
} from 'react-native/Libraries/Types/CoreEventTypes';
|
||||
import type FillRateHelper from './FillRateHelper';
|
||||
import type {RenderItemType} from './VirtualizedListProps';
|
||||
|
||||
import View from '../Components/View/View';
|
||||
import StyleSheet from '../StyleSheet/StyleSheet';
|
||||
import {View, StyleSheet} from 'react-native';
|
||||
import {VirtualizedListCellContextProvider} from './VirtualizedListContext.js';
|
||||
import invariant from 'invariant';
|
||||
import * as React from 'react';
|
||||
File renamed without changes.
+2
-2
@@ -8,8 +8,8 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import typeof ScrollView from '../Components/ScrollView/ScrollView';
|
||||
import type {ViewStyleProp} from '../StyleSheet/StyleSheet';
|
||||
import {typeof ScrollView} from 'react-native';
|
||||
import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet';
|
||||
import type {
|
||||
ViewabilityConfig,
|
||||
ViewabilityConfigCallbackPair,
|
||||
@@ -0,0 +1,617 @@
|
||||
/**
|
||||
* 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 {ViewToken} from './ViewabilityHelper';
|
||||
|
||||
import {View} from 'react-native';
|
||||
import VirtualizedList from './VirtualizedList';
|
||||
import {keyExtractor as defaultKeyExtractor} from './VirtualizeUtils';
|
||||
import invariant from 'invariant';
|
||||
import * as React from 'react';
|
||||
|
||||
type Item = any;
|
||||
|
||||
export type SectionBase<SectionItemT> = {
|
||||
/**
|
||||
* The data for rendering items in this section.
|
||||
*/
|
||||
data: $ReadOnlyArray<SectionItemT>,
|
||||
/**
|
||||
* Optional key to keep track of section re-ordering. If you don't plan on re-ordering sections,
|
||||
* the array index will be used by default.
|
||||
*/
|
||||
key?: string,
|
||||
// Optional props will override list-wide props just for this section.
|
||||
renderItem?: ?(info: {
|
||||
item: SectionItemT,
|
||||
index: number,
|
||||
section: SectionBase<SectionItemT>,
|
||||
separators: {
|
||||
highlight: () => void,
|
||||
unhighlight: () => void,
|
||||
updateProps: (select: 'leading' | 'trailing', newProps: Object) => void,
|
||||
...
|
||||
},
|
||||
...
|
||||
}) => null | React.Element<any>,
|
||||
ItemSeparatorComponent?: ?React.ComponentType<any>,
|
||||
keyExtractor?: (item: SectionItemT, index?: ?number) => string,
|
||||
...
|
||||
};
|
||||
|
||||
type RequiredProps<SectionT: SectionBase<any>> = {|
|
||||
sections: $ReadOnlyArray<SectionT>,
|
||||
|};
|
||||
|
||||
type OptionalProps<SectionT: SectionBase<any>> = {|
|
||||
/**
|
||||
* Default renderer for every item in every section.
|
||||
*/
|
||||
renderItem?: (info: {
|
||||
item: Item,
|
||||
index: number,
|
||||
section: SectionT,
|
||||
separators: {
|
||||
highlight: () => void,
|
||||
unhighlight: () => void,
|
||||
updateProps: (select: 'leading' | 'trailing', newProps: Object) => void,
|
||||
...
|
||||
},
|
||||
...
|
||||
}) => null | React.Element<any>,
|
||||
/**
|
||||
* Rendered at the top of each section. These stick to the top of the `ScrollView` by default on
|
||||
* iOS. See `stickySectionHeadersEnabled`.
|
||||
*/
|
||||
renderSectionHeader?: ?(info: {
|
||||
section: SectionT,
|
||||
...
|
||||
}) => null | React.Element<any>,
|
||||
/**
|
||||
* Rendered at the bottom of each section.
|
||||
*/
|
||||
renderSectionFooter?: ?(info: {
|
||||
section: SectionT,
|
||||
...
|
||||
}) => null | React.Element<any>,
|
||||
/**
|
||||
* Rendered at the top and bottom of each section (note this is different from
|
||||
* `ItemSeparatorComponent` which is only rendered between items). These are intended to separate
|
||||
* sections from the headers above and below and typically have the same highlight response as
|
||||
* `ItemSeparatorComponent`. Also receives `highlighted`, `[leading/trailing][Item/Separator]`,
|
||||
* and any custom props from `separators.updateProps`.
|
||||
*/
|
||||
SectionSeparatorComponent?: ?React.ComponentType<any>,
|
||||
/**
|
||||
* Makes section headers stick to the top of the screen until the next one pushes it off. Only
|
||||
* enabled by default on iOS because that is the platform standard there.
|
||||
*/
|
||||
stickySectionHeadersEnabled?: boolean,
|
||||
onEndReached?: ?({distanceFromEnd: number, ...}) => void,
|
||||
|};
|
||||
|
||||
type VirtualizedListProps = React.ElementConfig<typeof VirtualizedList>;
|
||||
|
||||
export type Props<SectionT> = {|
|
||||
...RequiredProps<SectionT>,
|
||||
...OptionalProps<SectionT>,
|
||||
...$Diff<
|
||||
VirtualizedListProps,
|
||||
{
|
||||
renderItem: $PropertyType<VirtualizedListProps, 'renderItem'>,
|
||||
data: $PropertyType<VirtualizedListProps, 'data'>,
|
||||
...
|
||||
},
|
||||
>,
|
||||
|};
|
||||
export type ScrollToLocationParamsType = {|
|
||||
animated?: ?boolean,
|
||||
itemIndex: number,
|
||||
sectionIndex: number,
|
||||
viewOffset?: number,
|
||||
viewPosition?: number,
|
||||
|};
|
||||
|
||||
type State = {childProps: VirtualizedListProps, ...};
|
||||
|
||||
/**
|
||||
* Right now this just flattens everything into one list and uses VirtualizedList under the
|
||||
* hood. The only operation that might not scale well is concatting the data arrays of all the
|
||||
* sections when new props are received, which should be plenty fast for up to ~10,000 items.
|
||||
*/
|
||||
class VirtualizedSectionList<
|
||||
SectionT: SectionBase<any>,
|
||||
> extends React.PureComponent<Props<SectionT>, State> {
|
||||
scrollToLocation(params: ScrollToLocationParamsType) {
|
||||
let index = params.itemIndex;
|
||||
for (let i = 0; i < params.sectionIndex; i++) {
|
||||
index += this.props.getItemCount(this.props.sections[i].data) + 2;
|
||||
}
|
||||
let viewOffset = params.viewOffset || 0;
|
||||
if (this._listRef == null) {
|
||||
return;
|
||||
}
|
||||
if (params.itemIndex > 0 && this.props.stickySectionHeadersEnabled) {
|
||||
const frame = this._listRef.__getFrameMetricsApprox(
|
||||
index - params.itemIndex,
|
||||
this._listRef.props,
|
||||
);
|
||||
viewOffset += frame.length;
|
||||
}
|
||||
const toIndexParams = {
|
||||
...params,
|
||||
viewOffset,
|
||||
index,
|
||||
};
|
||||
// $FlowFixMe[incompatible-use]
|
||||
this._listRef.scrollToIndex(toIndexParams);
|
||||
}
|
||||
|
||||
getListRef(): ?React.ElementRef<typeof VirtualizedList> {
|
||||
return this._listRef;
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
const {
|
||||
ItemSeparatorComponent, // don't pass through, rendered with renderItem
|
||||
SectionSeparatorComponent,
|
||||
renderItem: _renderItem,
|
||||
renderSectionFooter,
|
||||
renderSectionHeader,
|
||||
sections: _sections,
|
||||
stickySectionHeadersEnabled,
|
||||
...passThroughProps
|
||||
} = this.props;
|
||||
|
||||
const listHeaderOffset = this.props.ListHeaderComponent ? 1 : 0;
|
||||
|
||||
const stickyHeaderIndices = this.props.stickySectionHeadersEnabled
|
||||
? ([]: Array<number>)
|
||||
: undefined;
|
||||
|
||||
let itemCount = 0;
|
||||
for (const section of this.props.sections) {
|
||||
// Track the section header indices
|
||||
if (stickyHeaderIndices != null) {
|
||||
stickyHeaderIndices.push(itemCount + listHeaderOffset);
|
||||
}
|
||||
|
||||
// Add two for the section header and footer.
|
||||
itemCount += 2;
|
||||
itemCount += this.props.getItemCount(section.data);
|
||||
}
|
||||
const renderItem = this._renderItem(itemCount);
|
||||
|
||||
return (
|
||||
<VirtualizedList
|
||||
{...passThroughProps}
|
||||
keyExtractor={this._keyExtractor}
|
||||
stickyHeaderIndices={stickyHeaderIndices}
|
||||
renderItem={renderItem}
|
||||
data={this.props.sections}
|
||||
getItem={(sections, index) =>
|
||||
this._getItem(this.props, sections, index)
|
||||
}
|
||||
getItemCount={() => itemCount}
|
||||
onViewableItemsChanged={
|
||||
this.props.onViewableItemsChanged
|
||||
? this._onViewableItemsChanged
|
||||
: undefined
|
||||
}
|
||||
ref={this._captureRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_getItem(
|
||||
props: Props<SectionT>,
|
||||
sections: ?$ReadOnlyArray<Item>,
|
||||
index: number,
|
||||
): ?Item {
|
||||
if (!sections) {
|
||||
return null;
|
||||
}
|
||||
let itemIdx = index - 1;
|
||||
for (let i = 0; i < sections.length; i++) {
|
||||
const section = sections[i];
|
||||
const sectionData = section.data;
|
||||
const itemCount = props.getItemCount(sectionData);
|
||||
if (itemIdx === -1 || itemIdx === itemCount) {
|
||||
// We intend for there to be overflow by one on both ends of the list.
|
||||
// This will be for headers and footers. When returning a header or footer
|
||||
// item the section itself is the item.
|
||||
return section;
|
||||
} else if (itemIdx < itemCount) {
|
||||
// If we are in the bounds of the list's data then return the item.
|
||||
return props.getItem(sectionData, itemIdx);
|
||||
} else {
|
||||
itemIdx -= itemCount + 2; // Add two for the header and footer
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// $FlowFixMe[missing-local-annot]
|
||||
_keyExtractor = (item: Item, index: number) => {
|
||||
const info = this._subExtractor(index);
|
||||
return (info && info.key) || String(index);
|
||||
};
|
||||
|
||||
_subExtractor(index: number): ?{
|
||||
section: SectionT,
|
||||
// Key of the section or combined key for section + item
|
||||
key: string,
|
||||
// Relative index within the section
|
||||
index: ?number,
|
||||
// True if this is the section header
|
||||
header?: ?boolean,
|
||||
leadingItem?: ?Item,
|
||||
leadingSection?: ?SectionT,
|
||||
trailingItem?: ?Item,
|
||||
trailingSection?: ?SectionT,
|
||||
...
|
||||
} {
|
||||
let itemIndex = index;
|
||||
const {getItem, getItemCount, keyExtractor, sections} = this.props;
|
||||
for (let i = 0; i < sections.length; i++) {
|
||||
const section = sections[i];
|
||||
const sectionData = section.data;
|
||||
const key = section.key || String(i);
|
||||
itemIndex -= 1; // The section adds an item for the header
|
||||
if (itemIndex >= getItemCount(sectionData) + 1) {
|
||||
itemIndex -= getItemCount(sectionData) + 1; // The section adds an item for the footer.
|
||||
} else if (itemIndex === -1) {
|
||||
return {
|
||||
section,
|
||||
key: key + ':header',
|
||||
index: null,
|
||||
header: true,
|
||||
trailingSection: sections[i + 1],
|
||||
};
|
||||
} else if (itemIndex === getItemCount(sectionData)) {
|
||||
return {
|
||||
section,
|
||||
key: key + ':footer',
|
||||
index: null,
|
||||
header: false,
|
||||
trailingSection: sections[i + 1],
|
||||
};
|
||||
} else {
|
||||
const extractor =
|
||||
section.keyExtractor || keyExtractor || defaultKeyExtractor;
|
||||
return {
|
||||
section,
|
||||
key:
|
||||
key + ':' + extractor(getItem(sectionData, itemIndex), itemIndex),
|
||||
index: itemIndex,
|
||||
leadingItem: getItem(sectionData, itemIndex - 1),
|
||||
leadingSection: sections[i - 1],
|
||||
trailingItem: getItem(sectionData, itemIndex + 1),
|
||||
trailingSection: sections[i + 1],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_convertViewable = (viewable: ViewToken): ?ViewToken => {
|
||||
invariant(viewable.index != null, 'Received a broken ViewToken');
|
||||
const info = this._subExtractor(viewable.index);
|
||||
if (!info) {
|
||||
return null;
|
||||
}
|
||||
const keyExtractorWithNullableIndex = info.section.keyExtractor;
|
||||
const keyExtractorWithNonNullableIndex =
|
||||
this.props.keyExtractor || defaultKeyExtractor;
|
||||
const key =
|
||||
keyExtractorWithNullableIndex != null
|
||||
? keyExtractorWithNullableIndex(viewable.item, info.index)
|
||||
: keyExtractorWithNonNullableIndex(viewable.item, info.index ?? 0);
|
||||
|
||||
return {
|
||||
...viewable,
|
||||
index: info.index,
|
||||
key,
|
||||
section: info.section,
|
||||
};
|
||||
};
|
||||
|
||||
_onViewableItemsChanged = ({
|
||||
viewableItems,
|
||||
changed,
|
||||
}: {
|
||||
viewableItems: Array<ViewToken>,
|
||||
changed: Array<ViewToken>,
|
||||
...
|
||||
}) => {
|
||||
const onViewableItemsChanged = this.props.onViewableItemsChanged;
|
||||
if (onViewableItemsChanged != null) {
|
||||
onViewableItemsChanged({
|
||||
viewableItems: viewableItems
|
||||
.map(this._convertViewable, this)
|
||||
.filter(Boolean),
|
||||
changed: changed.map(this._convertViewable, this).filter(Boolean),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_renderItem =
|
||||
(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) {
|
||||
return null;
|
||||
}
|
||||
const infoIndex = info.index;
|
||||
if (infoIndex == null) {
|
||||
const {section} = info;
|
||||
if (info.header === true) {
|
||||
const {renderSectionHeader} = this.props;
|
||||
return renderSectionHeader ? renderSectionHeader({section}) : null;
|
||||
} else {
|
||||
const {renderSectionFooter} = this.props;
|
||||
return renderSectionFooter ? renderSectionFooter({section}) : null;
|
||||
}
|
||||
} else {
|
||||
const renderItem = info.section.renderItem || this.props.renderItem;
|
||||
const SeparatorComponent = this._getSeparatorComponent(
|
||||
index,
|
||||
info,
|
||||
listItemCount,
|
||||
);
|
||||
invariant(renderItem, 'no renderItem!');
|
||||
return (
|
||||
<ItemWithSeparator
|
||||
SeparatorComponent={SeparatorComponent}
|
||||
LeadingSeparatorComponent={
|
||||
infoIndex === 0 ? this.props.SectionSeparatorComponent : undefined
|
||||
}
|
||||
cellKey={info.key}
|
||||
index={infoIndex}
|
||||
item={item}
|
||||
leadingItem={info.leadingItem}
|
||||
leadingSection={info.leadingSection}
|
||||
prevCellKey={(this._subExtractor(index - 1) || {}).key}
|
||||
// Callback to provide updateHighlight for this item
|
||||
setSelfHighlightCallback={this._setUpdateHighlightFor}
|
||||
setSelfUpdatePropsCallback={this._setUpdatePropsFor}
|
||||
// Provide child ability to set highlight/updateProps for previous item using prevCellKey
|
||||
updateHighlightFor={this._updateHighlightFor}
|
||||
updatePropsFor={this._updatePropsFor}
|
||||
renderItem={renderItem}
|
||||
section={info.section}
|
||||
trailingItem={info.trailingItem}
|
||||
trailingSection={info.trailingSection}
|
||||
inverted={!!this.props.inverted}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
_updatePropsFor = (cellKey: string, value: any) => {
|
||||
const updateProps = this._updatePropsMap[cellKey];
|
||||
if (updateProps != null) {
|
||||
updateProps(value);
|
||||
}
|
||||
};
|
||||
|
||||
_updateHighlightFor = (cellKey: string, value: boolean) => {
|
||||
const updateHighlight = this._updateHighlightMap[cellKey];
|
||||
if (updateHighlight != null) {
|
||||
updateHighlight(value);
|
||||
}
|
||||
};
|
||||
|
||||
_setUpdateHighlightFor = (
|
||||
cellKey: string,
|
||||
updateHighlightFn: ?(boolean) => void,
|
||||
) => {
|
||||
if (updateHighlightFn != null) {
|
||||
this._updateHighlightMap[cellKey] = updateHighlightFn;
|
||||
} else {
|
||||
// $FlowFixMe[prop-missing]
|
||||
delete this._updateHighlightFor[cellKey];
|
||||
}
|
||||
};
|
||||
|
||||
_setUpdatePropsFor = (cellKey: string, updatePropsFn: ?(boolean) => void) => {
|
||||
if (updatePropsFn != null) {
|
||||
this._updatePropsMap[cellKey] = updatePropsFn;
|
||||
} else {
|
||||
delete this._updatePropsMap[cellKey];
|
||||
}
|
||||
};
|
||||
|
||||
_getSeparatorComponent(
|
||||
index: number,
|
||||
info?: ?Object,
|
||||
listItemCount: number,
|
||||
): ?React.ComponentType<any> {
|
||||
info = info || this._subExtractor(index);
|
||||
if (!info) {
|
||||
return null;
|
||||
}
|
||||
const ItemSeparatorComponent =
|
||||
info.section.ItemSeparatorComponent || this.props.ItemSeparatorComponent;
|
||||
const {SectionSeparatorComponent} = this.props;
|
||||
const isLastItemInList = index === listItemCount - 1;
|
||||
const isLastItemInSection =
|
||||
info.index === this.props.getItemCount(info.section.data) - 1;
|
||||
if (SectionSeparatorComponent && isLastItemInSection) {
|
||||
return SectionSeparatorComponent;
|
||||
}
|
||||
if (ItemSeparatorComponent && !isLastItemInSection && !isLastItemInList) {
|
||||
return ItemSeparatorComponent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_updateHighlightMap: {[string]: (boolean) => void} = {};
|
||||
_updatePropsMap: {[string]: void | (boolean => void)} = {};
|
||||
_listRef: ?React.ElementRef<typeof VirtualizedList>;
|
||||
_captureRef = (ref: null | React$ElementRef<Class<VirtualizedList>>) => {
|
||||
this._listRef = ref;
|
||||
};
|
||||
}
|
||||
|
||||
type ItemWithSeparatorCommonProps = $ReadOnly<{|
|
||||
leadingItem: ?Item,
|
||||
leadingSection: ?Object,
|
||||
section: Object,
|
||||
trailingItem: ?Item,
|
||||
trailingSection: ?Object,
|
||||
|}>;
|
||||
|
||||
type ItemWithSeparatorProps = $ReadOnly<{|
|
||||
...ItemWithSeparatorCommonProps,
|
||||
LeadingSeparatorComponent: ?React.ComponentType<any>,
|
||||
SeparatorComponent: ?React.ComponentType<any>,
|
||||
cellKey: string,
|
||||
index: number,
|
||||
item: Item,
|
||||
setSelfHighlightCallback: (
|
||||
cellKey: string,
|
||||
updateFn: ?(boolean) => void,
|
||||
) => void,
|
||||
setSelfUpdatePropsCallback: (
|
||||
cellKey: string,
|
||||
updateFn: ?(boolean) => void,
|
||||
) => void,
|
||||
prevCellKey?: ?string,
|
||||
updateHighlightFor: (prevCellKey: string, value: boolean) => void,
|
||||
updatePropsFor: (prevCellKey: string, value: Object) => void,
|
||||
renderItem: Function,
|
||||
inverted: boolean,
|
||||
|}>;
|
||||
|
||||
function ItemWithSeparator(props: ItemWithSeparatorProps): React.Node {
|
||||
const {
|
||||
LeadingSeparatorComponent,
|
||||
// this is the trailing separator and is associated with this item
|
||||
SeparatorComponent,
|
||||
cellKey,
|
||||
prevCellKey,
|
||||
setSelfHighlightCallback,
|
||||
updateHighlightFor,
|
||||
setSelfUpdatePropsCallback,
|
||||
updatePropsFor,
|
||||
item,
|
||||
index,
|
||||
section,
|
||||
inverted,
|
||||
} = props;
|
||||
|
||||
const [leadingSeparatorHiglighted, setLeadingSeparatorHighlighted] =
|
||||
React.useState(false);
|
||||
|
||||
const [separatorHighlighted, setSeparatorHighlighted] = React.useState(false);
|
||||
|
||||
const [leadingSeparatorProps, setLeadingSeparatorProps] = React.useState({
|
||||
leadingItem: props.leadingItem,
|
||||
leadingSection: props.leadingSection,
|
||||
section: props.section,
|
||||
trailingItem: props.item,
|
||||
trailingSection: props.trailingSection,
|
||||
});
|
||||
const [separatorProps, setSeparatorProps] = React.useState({
|
||||
leadingItem: props.item,
|
||||
leadingSection: props.leadingSection,
|
||||
section: props.section,
|
||||
trailingItem: props.trailingItem,
|
||||
trailingSection: props.trailingSection,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
setSelfHighlightCallback(cellKey, setSeparatorHighlighted);
|
||||
// $FlowFixMe[incompatible-call]
|
||||
setSelfUpdatePropsCallback(cellKey, setSeparatorProps);
|
||||
|
||||
return () => {
|
||||
setSelfUpdatePropsCallback(cellKey, null);
|
||||
setSelfHighlightCallback(cellKey, null);
|
||||
};
|
||||
}, [
|
||||
cellKey,
|
||||
setSelfHighlightCallback,
|
||||
setSeparatorProps,
|
||||
setSelfUpdatePropsCallback,
|
||||
]);
|
||||
|
||||
const separators = {
|
||||
highlight: () => {
|
||||
setLeadingSeparatorHighlighted(true);
|
||||
setSeparatorHighlighted(true);
|
||||
if (prevCellKey != null) {
|
||||
updateHighlightFor(prevCellKey, true);
|
||||
}
|
||||
},
|
||||
unhighlight: () => {
|
||||
setLeadingSeparatorHighlighted(false);
|
||||
setSeparatorHighlighted(false);
|
||||
if (prevCellKey != null) {
|
||||
updateHighlightFor(prevCellKey, false);
|
||||
}
|
||||
},
|
||||
updateProps: (
|
||||
select: 'leading' | 'trailing',
|
||||
newProps: $Shape<ItemWithSeparatorCommonProps>,
|
||||
) => {
|
||||
if (select === 'leading') {
|
||||
if (LeadingSeparatorComponent != null) {
|
||||
setLeadingSeparatorProps({...leadingSeparatorProps, ...newProps});
|
||||
} else if (prevCellKey != null) {
|
||||
// update the previous item's separator
|
||||
updatePropsFor(prevCellKey, {...leadingSeparatorProps, ...newProps});
|
||||
}
|
||||
} else if (select === 'trailing' && SeparatorComponent != null) {
|
||||
setSeparatorProps({...separatorProps, ...newProps});
|
||||
}
|
||||
},
|
||||
};
|
||||
const element = props.renderItem({
|
||||
item,
|
||||
index,
|
||||
section,
|
||||
separators,
|
||||
});
|
||||
const leadingSeparator = LeadingSeparatorComponent != null && (
|
||||
<LeadingSeparatorComponent
|
||||
highlighted={leadingSeparatorHiglighted}
|
||||
{...leadingSeparatorProps}
|
||||
/>
|
||||
);
|
||||
const separator = SeparatorComponent != null && (
|
||||
<SeparatorComponent
|
||||
highlighted={separatorHighlighted}
|
||||
{...separatorProps}
|
||||
/>
|
||||
);
|
||||
return leadingSeparator || separator ? (
|
||||
<View>
|
||||
{inverted === false ? leadingSeparator : separator}
|
||||
{element}
|
||||
{inverted === false ? separator : leadingSeparator}
|
||||
</View>
|
||||
) : (
|
||||
element
|
||||
);
|
||||
}
|
||||
|
||||
/* $FlowFixMe[class-object-subtyping] added when improving typing for this
|
||||
* parameters */
|
||||
// $FlowFixMe[method-unbinding]
|
||||
module.exports = (VirtualizedSectionList: React.AbstractComponent<
|
||||
React.ElementConfig<typeof VirtualizedSectionList>,
|
||||
$ReadOnly<{
|
||||
getListRef: () => ?React.ElementRef<typeof VirtualizedList>,
|
||||
scrollToLocation: (params: ScrollToLocationParamsType) => void,
|
||||
...
|
||||
}>,
|
||||
>);
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
/**
|
||||
* Intentional info-level logging for clear separation from ad-hoc console debug logging.
|
||||
*/
|
||||
function infoLog(...args: Array<mixed>): void {
|
||||
return console.log(...args);
|
||||
}
|
||||
|
||||
module.exports = infoLog;
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
export * from './Lists/VirtualizedList';
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import {keyExtractor} from './Lists/VirtualizeUtils';
|
||||
|
||||
import typeof VirtualizedList from './Lists/VirtualizedList';
|
||||
import typeof VirtualizedSectionList from './Lists/VirtualizedSectionList';
|
||||
import {typeof VirtualizedListContextResetter} from './Lists/VirtualizedListContext';
|
||||
import typeof ViewabilityHelper from './Lists/ViewabilityHelper';
|
||||
|
||||
export type {
|
||||
ViewToken,
|
||||
ViewabilityConfigCallbackPair,
|
||||
} from './Lists/ViewabilityHelper';
|
||||
export type {
|
||||
RenderItemProps,
|
||||
RenderItemType,
|
||||
Separators,
|
||||
} from './Lists/VirtualizedListProps';
|
||||
export type {
|
||||
Props as VirtualizedSectionListProps,
|
||||
ScrollToLocationParamsType,
|
||||
SectionBase,
|
||||
} from './Lists/VirtualizedSectionList';
|
||||
|
||||
module.exports = {
|
||||
keyExtractor,
|
||||
|
||||
get VirtualizedList(): VirtualizedList {
|
||||
return require('./Lists/VirtualizedList');
|
||||
},
|
||||
get VirtualizedSectionList(): VirtualizedSectionList {
|
||||
return require('./Lists/VirtualizedSectionList');
|
||||
},
|
||||
get VirtualizedListContextResetter(): VirtualizedListContextResetter {
|
||||
const VirtualizedListContext = require('./Lists/VirtualizedListContext');
|
||||
return VirtualizedListContext.VirtualizedListContextResetter;
|
||||
},
|
||||
get ViewabilityHelper(): ViewabilityHelper {
|
||||
return require('./Lists/ViewabilityHelper');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@react-native/virtualized-lists",
|
||||
"version": "0.72.0",
|
||||
"description": "Virtualized lists for React Native.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:facebook/react-native.git",
|
||||
"directory": "packages/virtualized-lists"
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"react-test-renderer": "18.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-native": "*",
|
||||
"react-test-renderer": "18.2.0"
|
||||
}
|
||||
}
|
||||
Vendored
+1
-1
@@ -115,7 +115,7 @@ export * from '../Libraries/LayoutAnimation/LayoutAnimation';
|
||||
export * from '../Libraries/Linking/Linking';
|
||||
export * from '../Libraries/Lists/FlatList';
|
||||
export * from '../Libraries/Lists/SectionList';
|
||||
export * from '../Libraries/Lists/VirtualizedList';
|
||||
export * from '@react-native/virtualized-lists';
|
||||
export * from '../Libraries/LogBox/LogBox';
|
||||
export * from '../Libraries/Modal/Modal';
|
||||
export * as Systrace from '../Libraries/Performance/Systrace';
|
||||
|
||||
Reference in new issue
Block a user