Reconnect VirtualizedList Source History 2/2 (Apply D41745930 + history, D42805202, D43063551)

Summary:
This change re-applies D41745930 (2e3dbe9c2f) (and D42805202 (1479b2ac26) which was also partially reverted), re-registers additions as moves, then applies D43063551 which has been added to the changes since migration.

Changelog: [Internal]

Reviewed By: hoxyq

Differential Revision: D43068114

fbshipit-source-id: 72997700bf9962d82a988599481e255b69e68a9b
This commit is contained in:
Nick Gerleman 2023-02-06 20:00:19 -08:00 коммит произвёл Facebook GitHub Bot
Родитель ebaa00e327
Коммит 0daf83ac51
49 изменённых файлов: 3755 добавлений и 3513 удалений

Просмотреть файл

@ -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');

Просмотреть файл

@ -10,244 +10,10 @@
'use strict';
import type {FrameMetricProps} from './VirtualizedListProps';
import {typeof FillRateHelper as FillRateHelperType} from '@react-native/virtualized-lists';
export type FillRateInfo = Info;
class Info {
any_blank_count: number = 0;
any_blank_ms: number = 0;
any_blank_speed_sum: number = 0;
mostly_blank_count: number = 0;
mostly_blank_ms: number = 0;
pixels_blank: number = 0;
pixels_sampled: number = 0;
pixels_scrolled: number = 0;
total_time_spent: number = 0;
sample_count: number = 0;
}
type FrameMetrics = {
inLayout?: boolean,
length: number,
offset: number,
...
};
const DEBUG = false;
let _listeners: Array<(Info) => void> = [];
let _minSampleCount = 10;
let _sampleRate = DEBUG ? 1 : null;
/**
* A helper class for detecting when the maximem fill rate of `VirtualizedList` is exceeded.
* By default the sampling rate is set to zero and this will do nothing. If you want to collect
* samples (e.g. to log them), make sure to call `FillRateHelper.setSampleRate(0.0-1.0)`.
*
* Listeners and sample rate are global for all `VirtualizedList`s - typical usage will combine with
* `SceneTracker.getActiveScene` to determine the context of the events.
*/
class FillRateHelper {
_anyBlankStartTime: ?number = null;
_enabled = false;
_getFrameMetrics: (index: number, props: FrameMetricProps) => ?FrameMetrics;
_info: Info = new Info();
_mostlyBlankStartTime: ?number = null;
_samplesStartTime: ?number = null;
static addListener(callback: FillRateInfo => void): {
remove: () => void,
...
} {
if (_sampleRate === null) {
console.warn('Call `FillRateHelper.setSampleRate` before `addListener`.');
}
_listeners.push(callback);
return {
remove: () => {
_listeners = _listeners.filter(listener => callback !== listener);
},
};
}
static setSampleRate(sampleRate: number) {
_sampleRate = sampleRate;
}
static setMinSampleCount(minSampleCount: number) {
_minSampleCount = minSampleCount;
}
constructor(
getFrameMetrics: (index: number, props: FrameMetricProps) => ?FrameMetrics,
) {
this._getFrameMetrics = getFrameMetrics;
this._enabled = (_sampleRate || 0) > Math.random();
this._resetData();
}
activate() {
if (this._enabled && this._samplesStartTime == null) {
DEBUG && console.debug('FillRateHelper: activate');
this._samplesStartTime = global.performance.now();
}
}
deactivateAndFlush() {
if (!this._enabled) {
return;
}
const start = this._samplesStartTime; // const for flow
if (start == null) {
DEBUG &&
console.debug('FillRateHelper: bail on deactivate with no start time');
return;
}
if (this._info.sample_count < _minSampleCount) {
// Don't bother with under-sampled events.
this._resetData();
return;
}
const total_time_spent = global.performance.now() - start;
const info: any = {
...this._info,
total_time_spent,
};
if (DEBUG) {
const derived = {
avg_blankness: this._info.pixels_blank / this._info.pixels_sampled,
avg_speed: this._info.pixels_scrolled / (total_time_spent / 1000),
avg_speed_when_any_blank:
this._info.any_blank_speed_sum / this._info.any_blank_count,
any_blank_per_min:
this._info.any_blank_count / (total_time_spent / 1000 / 60),
any_blank_time_frac: this._info.any_blank_ms / total_time_spent,
mostly_blank_per_min:
this._info.mostly_blank_count / (total_time_spent / 1000 / 60),
mostly_blank_time_frac: this._info.mostly_blank_ms / total_time_spent,
};
for (const key in derived) {
// $FlowFixMe[prop-missing]
derived[key] = Math.round(1000 * derived[key]) / 1000;
}
console.debug('FillRateHelper deactivateAndFlush: ', {derived, info});
}
_listeners.forEach(listener => listener(info));
this._resetData();
}
computeBlankness(
props: {
...FrameMetricProps,
initialNumToRender?: ?number,
...
},
cellsAroundViewport: {
first: number,
last: number,
...
},
scrollMetrics: {
dOffset: number,
offset: number,
velocity: number,
visibleLength: number,
...
},
): number {
if (
!this._enabled ||
props.getItemCount(props.data) === 0 ||
cellsAroundViewport.last < cellsAroundViewport.first ||
this._samplesStartTime == null
) {
return 0;
}
const {dOffset, offset, velocity, visibleLength} = scrollMetrics;
// Denominator metrics that we track for all events - most of the time there is no blankness and
// we want to capture that.
this._info.sample_count++;
this._info.pixels_sampled += Math.round(visibleLength);
this._info.pixels_scrolled += Math.round(Math.abs(dOffset));
const scrollSpeed = Math.round(Math.abs(velocity) * 1000); // px / sec
// Whether blank now or not, record the elapsed time blank if we were blank last time.
const now = global.performance.now();
if (this._anyBlankStartTime != null) {
this._info.any_blank_ms += now - this._anyBlankStartTime;
}
this._anyBlankStartTime = null;
if (this._mostlyBlankStartTime != null) {
this._info.mostly_blank_ms += now - this._mostlyBlankStartTime;
}
this._mostlyBlankStartTime = null;
let blankTop = 0;
let first = cellsAroundViewport.first;
let firstFrame = this._getFrameMetrics(first, props);
while (
first <= cellsAroundViewport.last &&
(!firstFrame || !firstFrame.inLayout)
) {
firstFrame = this._getFrameMetrics(first, props);
first++;
}
// Only count blankTop if we aren't rendering the first item, otherwise we will count the header
// as blank.
if (firstFrame && first > 0) {
blankTop = Math.min(
visibleLength,
Math.max(0, firstFrame.offset - offset),
);
}
let blankBottom = 0;
let last = cellsAroundViewport.last;
let lastFrame = this._getFrameMetrics(last, props);
while (
last >= cellsAroundViewport.first &&
(!lastFrame || !lastFrame.inLayout)
) {
lastFrame = this._getFrameMetrics(last, props);
last--;
}
// Only count blankBottom if we aren't rendering the last item, otherwise we will count the
// footer as blank.
if (lastFrame && last < props.getItemCount(props.data) - 1) {
const bottomEdge = lastFrame.offset + lastFrame.length;
blankBottom = Math.min(
visibleLength,
Math.max(0, offset + visibleLength - bottomEdge),
);
}
const pixels_blank = Math.round(blankTop + blankBottom);
const blankness = pixels_blank / visibleLength;
if (blankness > 0) {
this._anyBlankStartTime = now;
this._info.any_blank_speed_sum += scrollSpeed;
this._info.any_blank_count++;
this._info.pixels_blank += pixels_blank;
if (blankness > 0.5) {
this._mostlyBlankStartTime = now;
this._info.mostly_blank_count++;
}
} else if (scrollSpeed < 0.01 || Math.abs(dOffset) < 1) {
this.deactivateAndFlush();
}
return blankness;
}
enabled(): boolean {
return this._enabled;
}
_resetData() {
this._anyBlankStartTime = null;
this._info = new Info();
this._mostlyBlankStartTime = null;
this._samplesStartTime = null;
}
}
const FillRateHelper: FillRateHelperType =
require('@react-native/virtualized-lists').FillRateHelper;
export type {FillRateInfo} from '@react-native/virtualized-lists';
module.exports = FillRateHelper;

2
Libraries/Lists/FlatList.d.ts поставляемый
Просмотреть файл

@ -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> = {|
/**

2
Libraries/Lists/SectionList.d.ts поставляемый
Просмотреть файл

@ -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,15 @@
'use strict';
import type {FrameMetricProps} from './VirtualizedListProps';
export type {
ViewToken,
ViewabilityConfig,
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,249 +10,9 @@
'use strict';
import type {FrameMetricProps} from './VirtualizedListProps';
import {typeof keyExtractor as KeyExtractorType} from '@react-native/virtualized-lists';
/**
* Used to find the indices of the frames that overlap the given offsets. Useful for finding the
* items that bound different windows of content, such as the visible area or the buffered overscan
* area.
*/
export function elementsThatOverlapOffsets(
offsets: Array<number>,
props: FrameMetricProps,
getFrameMetrics: (
index: number,
props: FrameMetricProps,
) => {
length: number,
offset: number,
...
},
zoomScale: number = 1,
): Array<number> {
const itemCount = props.getItemCount(props.data);
const result = [];
for (let offsetIndex = 0; offsetIndex < offsets.length; offsetIndex++) {
const currentOffset = offsets[offsetIndex];
let left = 0;
let right = itemCount - 1;
const keyExtractor: KeyExtractorType =
require('@react-native/virtualized-lists').keyExtractor;
while (left <= right) {
// eslint-disable-next-line no-bitwise
const mid = left + ((right - left) >>> 1);
const frame = getFrameMetrics(mid, props);
const scaledOffsetStart = frame.offset * zoomScale;
const scaledOffsetEnd = (frame.offset + frame.length) * zoomScale;
// We want the first frame that contains the offset, with inclusive bounds. Thus, for the
// first frame the scaledOffsetStart is inclusive, while for other frames it is exclusive.
if (
(mid === 0 && currentOffset < scaledOffsetStart) ||
(mid !== 0 && currentOffset <= scaledOffsetStart)
) {
right = mid - 1;
} else if (currentOffset > scaledOffsetEnd) {
left = mid + 1;
} else {
result[offsetIndex] = mid;
break;
}
}
}
return result;
}
/**
* Computes the number of elements in the `next` range that are new compared to the `prev` range.
* Handy for calculating how many new items will be rendered when the render window changes so we
* can restrict the number of new items render at once so that content can appear on the screen
* faster.
*/
export function newRangeCount(
prev: {
first: number,
last: number,
...
},
next: {
first: number,
last: number,
...
},
): number {
return (
next.last -
next.first +
1 -
Math.max(
0,
1 + Math.min(next.last, prev.last) - Math.max(next.first, prev.first),
)
);
}
/**
* Custom logic for determining which items should be rendered given the current frame and scroll
* metrics, as well as the previous render state. The algorithm may evolve over time, but generally
* prioritizes the visible area first, then expands that with overscan regions ahead and behind,
* biased in the direction of scroll.
*/
export function computeWindowedRenderLimits(
props: FrameMetricProps,
maxToRenderPerBatch: number,
windowSize: number,
prev: {
first: number,
last: number,
},
getFrameMetricsApprox: (
index: number,
props: FrameMetricProps,
) => {
length: number,
offset: number,
...
},
scrollMetrics: {
dt: number,
offset: number,
velocity: number,
visibleLength: number,
zoomScale: number,
...
},
): {
first: number,
last: number,
} {
const itemCount = props.getItemCount(props.data);
if (itemCount === 0) {
return {first: 0, last: -1};
}
const {offset, velocity, visibleLength, zoomScale = 1} = scrollMetrics;
// Start with visible area, then compute maximum overscan region by expanding from there, biased
// in the direction of scroll. Total overscan area is capped, which should cap memory consumption
// too.
const visibleBegin = Math.max(0, offset);
const visibleEnd = visibleBegin + visibleLength;
const overscanLength = (windowSize - 1) * visibleLength;
// Considering velocity seems to introduce more churn than it's worth.
const leadFactor = 0.5; // Math.max(0, Math.min(1, velocity / 25 + 0.5));
const fillPreference =
velocity > 1 ? 'after' : velocity < -1 ? 'before' : 'none';
const overscanBegin = Math.max(
0,
visibleBegin - (1 - leadFactor) * overscanLength,
);
const overscanEnd = Math.max(0, visibleEnd + leadFactor * overscanLength);
const lastItemOffset =
getFrameMetricsApprox(itemCount - 1, props).offset * zoomScale;
if (lastItemOffset < overscanBegin) {
// Entire list is before our overscan window
return {
first: Math.max(0, itemCount - 1 - maxToRenderPerBatch),
last: itemCount - 1,
};
}
// Find the indices that correspond to the items at the render boundaries we're targeting.
let [overscanFirst, first, last, overscanLast] = elementsThatOverlapOffsets(
[overscanBegin, visibleBegin, visibleEnd, overscanEnd],
props,
getFrameMetricsApprox,
zoomScale,
);
overscanFirst = overscanFirst == null ? 0 : overscanFirst;
first = first == null ? Math.max(0, overscanFirst) : first;
overscanLast = overscanLast == null ? itemCount - 1 : overscanLast;
last =
last == null
? Math.min(overscanLast, first + maxToRenderPerBatch - 1)
: last;
const visible = {first, last};
// We want to limit the number of new cells we're rendering per batch so that we can fill the
// content on the screen quickly. If we rendered the entire overscan window at once, the user
// could be staring at white space for a long time waiting for a bunch of offscreen content to
// render.
let newCellCount = newRangeCount(prev, visible);
while (true) {
if (first <= overscanFirst && last >= overscanLast) {
// If we fill the entire overscan range, we're done.
break;
}
const maxNewCells = newCellCount >= maxToRenderPerBatch;
const firstWillAddMore = first <= prev.first || first > prev.last;
const firstShouldIncrement =
first > overscanFirst && (!maxNewCells || !firstWillAddMore);
const lastWillAddMore = last >= prev.last || last < prev.first;
const lastShouldIncrement =
last < overscanLast && (!maxNewCells || !lastWillAddMore);
if (maxNewCells && !firstShouldIncrement && !lastShouldIncrement) {
// We only want to stop if we've hit maxNewCells AND we cannot increment first or last
// without rendering new items. This let's us preserve as many already rendered items as
// possible, reducing render churn and keeping the rendered overscan range as large as
// possible.
break;
}
if (
firstShouldIncrement &&
!(fillPreference === 'after' && lastShouldIncrement && lastWillAddMore)
) {
if (firstWillAddMore) {
newCellCount++;
}
first--;
}
if (
lastShouldIncrement &&
!(fillPreference === 'before' && firstShouldIncrement && firstWillAddMore)
) {
if (lastWillAddMore) {
newCellCount++;
}
last++;
}
}
if (
!(
last >= first &&
first >= 0 &&
last < itemCount &&
first >= overscanFirst &&
last <= overscanLast &&
first <= visible.first &&
last >= visible.last
)
) {
throw new Error(
'Bad window calculation ' +
JSON.stringify({
first,
last,
itemCount,
overscanFirst,
overscanLast,
visible,
}),
);
}
return {first, last};
}
export function keyExtractor(item: any, index: number): string {
if (typeof item === 'object' && item?.key != null) {
return item.key;
}
if (typeof item === 'object' && item?.id != null) {
return item.id;
}
return String(index);
}
module.exports = {keyExtractor};

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -4,113 +4,15 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @flow
* @format
*/
import typeof VirtualizedList from './VirtualizedList';
'use strict';
import * as React from 'react';
import {useContext, useMemo} from 'react';
import {typeof VirtualizedListContextResetter as VirtualizedListContextResetterType} from '@react-native/virtualized-lists';
type Context = $ReadOnly<{
cellKey: ?string,
getScrollMetrics: () => {
contentLength: number,
dOffset: number,
dt: number,
offset: number,
timestamp: number,
velocity: number,
visibleLength: number,
zoomScale: number,
},
horizontal: ?boolean,
getOutermostParentListRef: () => React.ElementRef<VirtualizedList>,
registerAsNestedChild: ({
cellKey: string,
ref: React.ElementRef<VirtualizedList>,
}) => void,
unregisterAsNestedChild: ({
ref: React.ElementRef<VirtualizedList>,
}) => void,
}>;
const VirtualizedListContextResetter: VirtualizedListContextResetterType =
require('@react-native/virtualized-lists').VirtualizedListContextResetter;
export const VirtualizedListContext: React.Context<?Context> =
React.createContext(null);
if (__DEV__) {
VirtualizedListContext.displayName = 'VirtualizedListContext';
}
/**
* Resets the context. Intended for use by portal-like components (e.g. Modal).
*/
export function VirtualizedListContextResetter({
children,
}: {
children: React.Node,
}): React.Node {
return (
<VirtualizedListContext.Provider value={null}>
{children}
</VirtualizedListContext.Provider>
);
}
/**
* Sets the context with memoization. Intended to be used by `VirtualizedList`.
*/
export function VirtualizedListContextProvider({
children,
value,
}: {
children: React.Node,
value: Context,
}): React.Node {
// Avoid setting a newly created context object if the values are identical.
const context = useMemo(
() => ({
cellKey: null,
getScrollMetrics: value.getScrollMetrics,
horizontal: value.horizontal,
getOutermostParentListRef: value.getOutermostParentListRef,
registerAsNestedChild: value.registerAsNestedChild,
unregisterAsNestedChild: value.unregisterAsNestedChild,
}),
[
value.getScrollMetrics,
value.horizontal,
value.getOutermostParentListRef,
value.registerAsNestedChild,
value.unregisterAsNestedChild,
],
);
return (
<VirtualizedListContext.Provider value={context}>
{children}
</VirtualizedListContext.Provider>
);
}
/**
* Sets the `cellKey`. Intended to be used by `VirtualizedList` for each cell.
*/
export function VirtualizedListCellContextProvider({
cellKey,
children,
}: {
cellKey: string,
children: React.Node,
}): React.Node {
// Avoid setting a newly created context object if the values are identical.
const currContext = useContext(VirtualizedListContext);
const context = useMemo(
() => (currContext == null ? null : {...currContext, cellKey}),
[currContext, cellKey],
);
return (
<VirtualizedListContext.Provider value={context}>
{children}
</VirtualizedListContext.Provider>
);
}
module.exports = {VirtualizedListContextResetter};

Просмотреть файл

@ -8,610 +8,15 @@
* @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,
...
}>,
>);
export type {
SectionBase,
ScrollToLocationParamsType,
} from '@react-native/virtualized-lists';
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';

Просмотреть файл

@ -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

Просмотреть файл

@ -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,253 @@
/**
* 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';
export type FillRateInfo = Info;
class Info {
any_blank_count: number = 0;
any_blank_ms: number = 0;
any_blank_speed_sum: number = 0;
mostly_blank_count: number = 0;
mostly_blank_ms: number = 0;
pixels_blank: number = 0;
pixels_sampled: number = 0;
pixels_scrolled: number = 0;
total_time_spent: number = 0;
sample_count: number = 0;
}
type FrameMetrics = {
inLayout?: boolean,
length: number,
offset: number,
...
};
const DEBUG = false;
let _listeners: Array<(Info) => void> = [];
let _minSampleCount = 10;
let _sampleRate = DEBUG ? 1 : null;
/**
* A helper class for detecting when the maximem fill rate of `VirtualizedList` is exceeded.
* By default the sampling rate is set to zero and this will do nothing. If you want to collect
* samples (e.g. to log them), make sure to call `FillRateHelper.setSampleRate(0.0-1.0)`.
*
* Listeners and sample rate are global for all `VirtualizedList`s - typical usage will combine with
* `SceneTracker.getActiveScene` to determine the context of the events.
*/
class FillRateHelper {
_anyBlankStartTime: ?number = null;
_enabled = false;
_getFrameMetrics: (index: number, props: FrameMetricProps) => ?FrameMetrics;
_info: Info = new Info();
_mostlyBlankStartTime: ?number = null;
_samplesStartTime: ?number = null;
static addListener(callback: FillRateInfo => void): {
remove: () => void,
...
} {
if (_sampleRate === null) {
console.warn('Call `FillRateHelper.setSampleRate` before `addListener`.');
}
_listeners.push(callback);
return {
remove: () => {
_listeners = _listeners.filter(listener => callback !== listener);
},
};
}
static setSampleRate(sampleRate: number) {
_sampleRate = sampleRate;
}
static setMinSampleCount(minSampleCount: number) {
_minSampleCount = minSampleCount;
}
constructor(
getFrameMetrics: (index: number, props: FrameMetricProps) => ?FrameMetrics,
) {
this._getFrameMetrics = getFrameMetrics;
this._enabled = (_sampleRate || 0) > Math.random();
this._resetData();
}
activate() {
if (this._enabled && this._samplesStartTime == null) {
DEBUG && console.debug('FillRateHelper: activate');
this._samplesStartTime = global.performance.now();
}
}
deactivateAndFlush() {
if (!this._enabled) {
return;
}
const start = this._samplesStartTime; // const for flow
if (start == null) {
DEBUG &&
console.debug('FillRateHelper: bail on deactivate with no start time');
return;
}
if (this._info.sample_count < _minSampleCount) {
// Don't bother with under-sampled events.
this._resetData();
return;
}
const total_time_spent = global.performance.now() - start;
const info: any = {
...this._info,
total_time_spent,
};
if (DEBUG) {
const derived = {
avg_blankness: this._info.pixels_blank / this._info.pixels_sampled,
avg_speed: this._info.pixels_scrolled / (total_time_spent / 1000),
avg_speed_when_any_blank:
this._info.any_blank_speed_sum / this._info.any_blank_count,
any_blank_per_min:
this._info.any_blank_count / (total_time_spent / 1000 / 60),
any_blank_time_frac: this._info.any_blank_ms / total_time_spent,
mostly_blank_per_min:
this._info.mostly_blank_count / (total_time_spent / 1000 / 60),
mostly_blank_time_frac: this._info.mostly_blank_ms / total_time_spent,
};
for (const key in derived) {
// $FlowFixMe[prop-missing]
derived[key] = Math.round(1000 * derived[key]) / 1000;
}
console.debug('FillRateHelper deactivateAndFlush: ', {derived, info});
}
_listeners.forEach(listener => listener(info));
this._resetData();
}
computeBlankness(
props: {
...FrameMetricProps,
initialNumToRender?: ?number,
...
},
cellsAroundViewport: {
first: number,
last: number,
...
},
scrollMetrics: {
dOffset: number,
offset: number,
velocity: number,
visibleLength: number,
...
},
): number {
if (
!this._enabled ||
props.getItemCount(props.data) === 0 ||
cellsAroundViewport.last < cellsAroundViewport.first ||
this._samplesStartTime == null
) {
return 0;
}
const {dOffset, offset, velocity, visibleLength} = scrollMetrics;
// Denominator metrics that we track for all events - most of the time there is no blankness and
// we want to capture that.
this._info.sample_count++;
this._info.pixels_sampled += Math.round(visibleLength);
this._info.pixels_scrolled += Math.round(Math.abs(dOffset));
const scrollSpeed = Math.round(Math.abs(velocity) * 1000); // px / sec
// Whether blank now or not, record the elapsed time blank if we were blank last time.
const now = global.performance.now();
if (this._anyBlankStartTime != null) {
this._info.any_blank_ms += now - this._anyBlankStartTime;
}
this._anyBlankStartTime = null;
if (this._mostlyBlankStartTime != null) {
this._info.mostly_blank_ms += now - this._mostlyBlankStartTime;
}
this._mostlyBlankStartTime = null;
let blankTop = 0;
let first = cellsAroundViewport.first;
let firstFrame = this._getFrameMetrics(first, props);
while (
first <= cellsAroundViewport.last &&
(!firstFrame || !firstFrame.inLayout)
) {
firstFrame = this._getFrameMetrics(first, props);
first++;
}
// Only count blankTop if we aren't rendering the first item, otherwise we will count the header
// as blank.
if (firstFrame && first > 0) {
blankTop = Math.min(
visibleLength,
Math.max(0, firstFrame.offset - offset),
);
}
let blankBottom = 0;
let last = cellsAroundViewport.last;
let lastFrame = this._getFrameMetrics(last, props);
while (
last >= cellsAroundViewport.first &&
(!lastFrame || !lastFrame.inLayout)
) {
lastFrame = this._getFrameMetrics(last, props);
last--;
}
// Only count blankBottom if we aren't rendering the last item, otherwise we will count the
// footer as blank.
if (lastFrame && last < props.getItemCount(props.data) - 1) {
const bottomEdge = lastFrame.offset + lastFrame.length;
blankBottom = Math.min(
visibleLength,
Math.max(0, offset + visibleLength - bottomEdge),
);
}
const pixels_blank = Math.round(blankTop + blankBottom);
const blankness = pixels_blank / visibleLength;
if (blankness > 0) {
this._anyBlankStartTime = now;
this._info.any_blank_speed_sum += scrollSpeed;
this._info.any_blank_count++;
this._info.pixels_blank += pixels_blank;
if (blankness > 0.5) {
this._mostlyBlankStartTime = now;
this._info.mostly_blank_count++;
}
} else if (scrollSpeed < 0.01 || Math.abs(dOffset) < 1) {
this.deactivateAndFlush();
}
return blankness;
}
enabled(): boolean {
return this._enabled;
}
_resetData() {
this._anyBlankStartTime = null;
this._info = new Info();
this._mostlyBlankStartTime = null;
this._samplesStartTime = null;
}
}
module.exports = FillRateHelper;

Просмотреть файл

@ -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;

Просмотреть файл

@ -0,0 +1,258 @@
/**
* 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';
/**
* Used to find the indices of the frames that overlap the given offsets. Useful for finding the
* items that bound different windows of content, such as the visible area or the buffered overscan
* area.
*/
export function elementsThatOverlapOffsets(
offsets: Array<number>,
props: FrameMetricProps,
getFrameMetrics: (
index: number,
props: FrameMetricProps,
) => {
length: number,
offset: number,
...
},
zoomScale: number = 1,
): Array<number> {
const itemCount = props.getItemCount(props.data);
const result = [];
for (let offsetIndex = 0; offsetIndex < offsets.length; offsetIndex++) {
const currentOffset = offsets[offsetIndex];
let left = 0;
let right = itemCount - 1;
while (left <= right) {
// eslint-disable-next-line no-bitwise
const mid = left + ((right - left) >>> 1);
const frame = getFrameMetrics(mid, props);
const scaledOffsetStart = frame.offset * zoomScale;
const scaledOffsetEnd = (frame.offset + frame.length) * zoomScale;
// We want the first frame that contains the offset, with inclusive bounds. Thus, for the
// first frame the scaledOffsetStart is inclusive, while for other frames it is exclusive.
if (
(mid === 0 && currentOffset < scaledOffsetStart) ||
(mid !== 0 && currentOffset <= scaledOffsetStart)
) {
right = mid - 1;
} else if (currentOffset > scaledOffsetEnd) {
left = mid + 1;
} else {
result[offsetIndex] = mid;
break;
}
}
}
return result;
}
/**
* Computes the number of elements in the `next` range that are new compared to the `prev` range.
* Handy for calculating how many new items will be rendered when the render window changes so we
* can restrict the number of new items render at once so that content can appear on the screen
* faster.
*/
export function newRangeCount(
prev: {
first: number,
last: number,
...
},
next: {
first: number,
last: number,
...
},
): number {
return (
next.last -
next.first +
1 -
Math.max(
0,
1 + Math.min(next.last, prev.last) - Math.max(next.first, prev.first),
)
);
}
/**
* Custom logic for determining which items should be rendered given the current frame and scroll
* metrics, as well as the previous render state. The algorithm may evolve over time, but generally
* prioritizes the visible area first, then expands that with overscan regions ahead and behind,
* biased in the direction of scroll.
*/
export function computeWindowedRenderLimits(
props: FrameMetricProps,
maxToRenderPerBatch: number,
windowSize: number,
prev: {
first: number,
last: number,
},
getFrameMetricsApprox: (
index: number,
props: FrameMetricProps,
) => {
length: number,
offset: number,
...
},
scrollMetrics: {
dt: number,
offset: number,
velocity: number,
visibleLength: number,
zoomScale: number,
...
},
): {
first: number,
last: number,
} {
const itemCount = props.getItemCount(props.data);
if (itemCount === 0) {
return {first: 0, last: -1};
}
const {offset, velocity, visibleLength, zoomScale = 1} = scrollMetrics;
// Start with visible area, then compute maximum overscan region by expanding from there, biased
// in the direction of scroll. Total overscan area is capped, which should cap memory consumption
// too.
const visibleBegin = Math.max(0, offset);
const visibleEnd = visibleBegin + visibleLength;
const overscanLength = (windowSize - 1) * visibleLength;
// Considering velocity seems to introduce more churn than it's worth.
const leadFactor = 0.5; // Math.max(0, Math.min(1, velocity / 25 + 0.5));
const fillPreference =
velocity > 1 ? 'after' : velocity < -1 ? 'before' : 'none';
const overscanBegin = Math.max(
0,
visibleBegin - (1 - leadFactor) * overscanLength,
);
const overscanEnd = Math.max(0, visibleEnd + leadFactor * overscanLength);
const lastItemOffset =
getFrameMetricsApprox(itemCount - 1, props).offset * zoomScale;
if (lastItemOffset < overscanBegin) {
// Entire list is before our overscan window
return {
first: Math.max(0, itemCount - 1 - maxToRenderPerBatch),
last: itemCount - 1,
};
}
// Find the indices that correspond to the items at the render boundaries we're targeting.
let [overscanFirst, first, last, overscanLast] = elementsThatOverlapOffsets(
[overscanBegin, visibleBegin, visibleEnd, overscanEnd],
props,
getFrameMetricsApprox,
zoomScale,
);
overscanFirst = overscanFirst == null ? 0 : overscanFirst;
first = first == null ? Math.max(0, overscanFirst) : first;
overscanLast = overscanLast == null ? itemCount - 1 : overscanLast;
last =
last == null
? Math.min(overscanLast, first + maxToRenderPerBatch - 1)
: last;
const visible = {first, last};
// We want to limit the number of new cells we're rendering per batch so that we can fill the
// content on the screen quickly. If we rendered the entire overscan window at once, the user
// could be staring at white space for a long time waiting for a bunch of offscreen content to
// render.
let newCellCount = newRangeCount(prev, visible);
while (true) {
if (first <= overscanFirst && last >= overscanLast) {
// If we fill the entire overscan range, we're done.
break;
}
const maxNewCells = newCellCount >= maxToRenderPerBatch;
const firstWillAddMore = first <= prev.first || first > prev.last;
const firstShouldIncrement =
first > overscanFirst && (!maxNewCells || !firstWillAddMore);
const lastWillAddMore = last >= prev.last || last < prev.first;
const lastShouldIncrement =
last < overscanLast && (!maxNewCells || !lastWillAddMore);
if (maxNewCells && !firstShouldIncrement && !lastShouldIncrement) {
// We only want to stop if we've hit maxNewCells AND we cannot increment first or last
// without rendering new items. This let's us preserve as many already rendered items as
// possible, reducing render churn and keeping the rendered overscan range as large as
// possible.
break;
}
if (
firstShouldIncrement &&
!(fillPreference === 'after' && lastShouldIncrement && lastWillAddMore)
) {
if (firstWillAddMore) {
newCellCount++;
}
first--;
}
if (
lastShouldIncrement &&
!(fillPreference === 'before' && firstShouldIncrement && firstWillAddMore)
) {
if (lastWillAddMore) {
newCellCount++;
}
last++;
}
}
if (
!(
last >= first &&
first >= 0 &&
last < itemCount &&
first >= overscanFirst &&
last <= overscanLast &&
first <= visible.first &&
last >= visible.last
)
) {
throw new Error(
'Bad window calculation ' +
JSON.stringify({
first,
last,
itemCount,
overscanFirst,
overscanLast,
visible,
}),
);
}
return {first, last};
}
export function keyExtractor(item: any, index: number): string {
if (typeof item === 'object' && item?.key != null) {
return item.key;
}
if (typeof item === 'object' && item?.id != null) {
return item.id;
}
return String(index);
}

Просмотреть файл

@ -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;

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -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';

Просмотреть файл

@ -0,0 +1,116 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import typeof VirtualizedList from './VirtualizedList';
import * as React from 'react';
import {useContext, useMemo} from 'react';
type Context = $ReadOnly<{
cellKey: ?string,
getScrollMetrics: () => {
contentLength: number,
dOffset: number,
dt: number,
offset: number,
timestamp: number,
velocity: number,
visibleLength: number,
zoomScale: number,
},
horizontal: ?boolean,
getOutermostParentListRef: () => React.ElementRef<VirtualizedList>,
registerAsNestedChild: ({
cellKey: string,
ref: React.ElementRef<VirtualizedList>,
}) => void,
unregisterAsNestedChild: ({
ref: React.ElementRef<VirtualizedList>,
}) => void,
}>;
export const VirtualizedListContext: React.Context<?Context> =
React.createContext(null);
if (__DEV__) {
VirtualizedListContext.displayName = 'VirtualizedListContext';
}
/**
* Resets the context. Intended for use by portal-like components (e.g. Modal).
*/
export function VirtualizedListContextResetter({
children,
}: {
children: React.Node,
}): React.Node {
return (
<VirtualizedListContext.Provider value={null}>
{children}
</VirtualizedListContext.Provider>
);
}
/**
* Sets the context with memoization. Intended to be used by `VirtualizedList`.
*/
export function VirtualizedListContextProvider({
children,
value,
}: {
children: React.Node,
value: Context,
}): React.Node {
// Avoid setting a newly created context object if the values are identical.
const context = useMemo(
() => ({
cellKey: null,
getScrollMetrics: value.getScrollMetrics,
horizontal: value.horizontal,
getOutermostParentListRef: value.getOutermostParentListRef,
registerAsNestedChild: value.registerAsNestedChild,
unregisterAsNestedChild: value.unregisterAsNestedChild,
}),
[
value.getScrollMetrics,
value.horizontal,
value.getOutermostParentListRef,
value.registerAsNestedChild,
value.unregisterAsNestedChild,
],
);
return (
<VirtualizedListContext.Provider value={context}>
{children}
</VirtualizedListContext.Provider>
);
}
/**
* Sets the `cellKey`. Intended to be used by `VirtualizedList` for each cell.
*/
export function VirtualizedListCellContextProvider({
cellKey,
children,
}: {
cellKey: string,
children: React.Node,
}): React.Node {
// Avoid setting a newly created context object if the values are identical.
const currContext = useContext(VirtualizedListContext);
const context = useMemo(
() => (currContext == null ? null : {...currContext, cellKey}),
[currContext, cellKey],
);
return (
<VirtualizedListContext.Provider value={context}>
{children}
</VirtualizedListContext.Provider>
);
}

Просмотреть файл

@ -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
packages/virtualized-lists/index.d.ts поставляемый Normal file
Просмотреть файл

@ -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,57 @@
/**
* 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';
import typeof FillRateHelper from './Lists/FillRateHelper';
export type {
ViewToken,
ViewabilityConfig,
ViewabilityConfigCallbackPair,
} from './Lists/ViewabilityHelper';
export type {
RenderItemProps,
RenderItemType,
Separators,
} from './Lists/VirtualizedListProps';
export type {
Props as VirtualizedSectionListProps,
ScrollToLocationParamsType,
SectionBase,
} from './Lists/VirtualizedSectionList';
export type {FillRateInfo} from './Lists/FillRateHelper';
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');
},
get FillRateHelper(): FillRateHelper {
return require('./Lists/FillRateHelper');
},
};

Просмотреть файл

@ -0,0 +1,21 @@
{
"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",
"dependencies": {
"invariant": "^2.2.4"
},
"devDependencies": {
"react-test-renderer": "18.2.0"
},
"peerDependencies": {
"react-native": "*",
"react-test-renderer": "18.2.0"
}
}

2
types/index.d.ts поставляемый
Просмотреть файл

@ -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';