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:
Родитель
ebaa00e327
Коммит
0daf83ac51
|
@ -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;
|
||||
|
|
|
@ -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> = {|
|
||||
/**
|
||||
|
|
|
@ -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');
|
||||
|
|
2
index.js
2
index.js
|
@ -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;
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
Загрузка…
Ссылка в новой задаче