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:
committed by
Facebook GitHub Bot
parent
62fa6d9dac
commit
2e3dbe9c2f
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
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';
|
||||
+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,
|
||||
...
|
||||
}>,
|
||||
>);
|
||||
@@ -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