This commit is contained in:
Yamozha
2021-04-02 02:24:13 +03:00
parent c23950b545
commit 7256d79e2c
31493 changed files with 3036630 additions and 0 deletions

View File

@ -0,0 +1,245 @@
/**
* Copyright (c) Facebook, Inc. and its 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';
const performanceNow = require('fbjs/lib/performanceNow');
const warning = require('fbjs/lib/warning');
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 = (null: ?number);
_enabled = false;
_getFrameMetrics: (index: number) => ?FrameMetrics;
_info = new Info();
_mostlyBlankStartTime = (null: ?number);
_samplesStartTime = (null: ?number);
static addListener(
callback: FillRateInfo => void,
): {remove: () => void, ...} {
warning(
_sampleRate !== null,
'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) => ?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 = performanceNow();
}
}
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 = performanceNow() - 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) {
derived[key] = Math.round(1000 * derived[key]) / 1000;
}
console.debug('FillRateHelper deactivateAndFlush: ', {derived, info});
}
_listeners.forEach(listener => listener(info));
this._resetData();
}
computeBlankness(
props: {
data: any,
getItemCount: (data: any) => number,
initialNumToRender: number,
...
},
state: {
first: number,
last: number,
...
},
scrollMetrics: {
dOffset: number,
offset: number,
velocity: number,
visibleLength: number,
...
},
): number {
if (
!this._enabled ||
props.getItemCount(props.data) === 0 ||
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 = performanceNow();
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 = state.first;
let firstFrame = this._getFrameMetrics(first);
while (first <= state.last && (!firstFrame || !firstFrame.inLayout)) {
firstFrame = this._getFrameMetrics(first);
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 = state.last;
let lastFrame = this._getFrameMetrics(last);
while (last >= state.first && (!lastFrame || !lastFrame.inLayout)) {
lastFrame = this._getFrameMetrics(last);
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;

641
node_modules/react-native/Libraries/Lists/FlatList.js generated vendored Normal file
View File

@ -0,0 +1,641 @@
/**
* Copyright (c) Facebook, Inc. and its 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';
const Platform = require('../Utilities/Platform');
const deepDiffer = require('../Utilities/differ/deepDiffer');
const React = require('react');
const View = require('../Components/View/View');
const VirtualizedList = require('./VirtualizedList');
const StyleSheet = require('../StyleSheet/StyleSheet');
const invariant = require('invariant');
import {type ScrollResponderType} from '../Components/ScrollView/ScrollView';
import type {ScrollViewNativeComponentType} from '../Components/ScrollView/ScrollViewNativeComponentType.js';
import type {ViewStyleProp} from '../StyleSheet/StyleSheet';
import type {
ViewToken,
ViewabilityConfigCallbackPair,
} from './ViewabilityHelper';
import type {RenderItemType, RenderItemProps} from './VirtualizedList';
type RequiredProps<ItemT> = {|
/**
* For simplicity, data is just a plain array. If you want to use something else, like an
* immutable list, use the underlying `VirtualizedList` directly.
*/
data: ?$ReadOnlyArray<ItemT>,
|};
type OptionalProps<ItemT> = {|
/**
* Takes an item from `data` and renders it into the list. Example usage:
*
* <FlatList
* ItemSeparatorComponent={Platform.OS !== 'android' && ({highlighted}) => (
* <View style={[style.separator, highlighted && {marginLeft: 0}]} />
* )}
* data={[{title: 'Title Text', key: 'item1'}]}
* renderItem={({item, separators}) => (
* <TouchableHighlight
* onPress={() => this._onPress(item)}
* onShowUnderlay={separators.highlight}
* onHideUnderlay={separators.unhighlight}>
* <View style={{backgroundColor: 'white'}}>
* <Text>{item.title}</Text>
* </View>
* </TouchableHighlight>
* )}
* />
*
* Provides additional metadata like `index` if you need it, as well as a more generic
* `separators.updateProps` function which let's you set whatever props you want to change the
* rendering of either the leading separator or trailing separator in case the more common
* `highlight` and `unhighlight` (which set the `highlighted: boolean` prop) are insufficient for
* your use-case.
*/
renderItem?: ?RenderItemType<ItemT>,
/**
* Optional custom style for multi-item rows generated when numColumns > 1.
*/
columnWrapperStyle?: ViewStyleProp,
/**
* A marker property for telling the list to re-render (since it implements `PureComponent`). If
* any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the
* `data` prop, stick it here and treat it immutably.
*/
extraData?: any,
/**
* `getItemLayout` is an optional optimizations that let us skip measurement of dynamic content if
* you know the height of items a priori. `getItemLayout` is the most efficient, and is easy to
* use if you have fixed height items, for example:
*
* getItemLayout={(data, index) => (
* {length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index}
* )}
*
* Adding `getItemLayout` can be a great performance boost for lists of several hundred items.
* Remember to include separator length (height or width) in your offset calculation if you
* specify `ItemSeparatorComponent`.
*/
getItemLayout?: (
data: ?Array<ItemT>,
index: number,
) => {
length: number,
offset: number,
index: number,
...
},
/**
* If true, renders items next to each other horizontally instead of stacked vertically.
*/
horizontal?: ?boolean,
/**
* How many items to render in the initial batch. This should be enough to fill the screen but not
* much more. Note these items will never be unmounted as part of the windowed rendering in order
* to improve perceived performance of scroll-to-top actions.
*/
initialNumToRender: number,
/**
* Instead of starting at the top with the first item, start at `initialScrollIndex`. This
* disables the "scroll to top" optimization that keeps the first `initialNumToRender` items
* always rendered and immediately renders the items starting at this initial index. Requires
* `getItemLayout` to be implemented.
*/
initialScrollIndex?: ?number,
/**
* Reverses the direction of scroll. Uses scale transforms of -1.
*/
inverted?: ?boolean,
/**
* Used to extract a unique key for a given item at the specified index. Key is used for caching
* and as the react key to track item re-ordering. The default extractor checks `item.key`, then
* falls back to using the index, like React does.
*/
keyExtractor: (item: ItemT, index: number) => string,
/**
* Multiple columns can only be rendered with `horizontal={false}` and will zig-zag like a
* `flexWrap` layout. Items should all be the same height - masonry layouts are not supported.
*/
numColumns: number,
/**
* See `ScrollView` for flow type and further documentation.
*/
fadingEdgeLength?: ?number,
|};
type FlatListProps<ItemT> = {|
...RequiredProps<ItemT>,
...OptionalProps<ItemT>,
|};
type VirtualizedListProps = React.ElementConfig<typeof VirtualizedList>;
export type Props<ItemT> = {
...$Diff<
VirtualizedListProps,
{
getItem: $PropertyType<VirtualizedListProps, 'getItem'>,
getItemCount: $PropertyType<VirtualizedListProps, 'getItemCount'>,
getItemLayout: $PropertyType<VirtualizedListProps, 'getItemLayout'>,
renderItem: $PropertyType<VirtualizedListProps, 'renderItem'>,
keyExtractor: $PropertyType<VirtualizedListProps, 'keyExtractor'>,
...
},
>,
...FlatListProps<ItemT>,
...
};
const defaultProps = {
...VirtualizedList.defaultProps,
numColumns: 1,
/**
* Enabling this prop on Android greatly improves scrolling performance with no known issues.
* The alternative is that scrolling on Android is unusably bad. Enabling it on iOS has a few
* known issues.
*/
removeClippedSubviews: Platform.OS === 'android',
};
export type DefaultProps = typeof defaultProps;
/**
* A performant interface for rendering simple, flat lists, supporting the most handy features:
*
* - Fully cross-platform.
* - Optional horizontal mode.
* - Configurable viewability callbacks.
* - Header support.
* - Footer support.
* - Separator support.
* - Pull to Refresh.
* - Scroll loading.
* - ScrollToIndex support.
*
* If you need section support, use [`<SectionList>`](docs/sectionlist.html).
*
* Minimal Example:
*
* <FlatList
* data={[{key: 'a'}, {key: 'b'}]}
* renderItem={({item}) => <Text>{item.key}</Text>}
* />
*
* More complex, multi-select example demonstrating `PureComponent` usage for perf optimization and avoiding bugs.
*
* - By binding the `onPressItem` handler, the props will remain `===` and `PureComponent` will
* prevent wasteful re-renders unless the actual `id`, `selected`, or `title` props change, even
* if the components rendered in `MyListItem` did not have such optimizations.
* - By passing `extraData={this.state}` to `FlatList` we make sure `FlatList` itself will re-render
* when the `state.selected` changes. Without setting this prop, `FlatList` would not know it
* needs to re-render any items because it is also a `PureComponent` and the prop comparison will
* not show any changes.
* - `keyExtractor` tells the list to use the `id`s for the react keys instead of the default `key` property.
*
*
* class MyListItem extends React.PureComponent {
* _onPress = () => {
* this.props.onPressItem(this.props.id);
* };
*
* render() {
* const textColor = this.props.selected ? "red" : "black";
* return (
* <TouchableOpacity onPress={this._onPress}>
* <View>
* <Text style={{ color: textColor }}>
* {this.props.title}
* </Text>
* </View>
* </TouchableOpacity>
* );
* }
* }
*
* class MultiSelectList extends React.PureComponent {
* state = {selected: (new Map(): Map<string, boolean>)};
*
* _keyExtractor = (item, index) => item.id;
*
* _onPressItem = (id: string) => {
* // updater functions are preferred for transactional updates
* this.setState((state) => {
* // copy the map rather than modifying state.
* const selected = new Map(state.selected);
* selected.set(id, !selected.get(id)); // toggle
* return {selected};
* });
* };
*
* _renderItem = ({item}) => (
* <MyListItem
* id={item.id}
* onPressItem={this._onPressItem}
* selected={!!this.state.selected.get(item.id)}
* title={item.title}
* />
* );
*
* render() {
* return (
* <FlatList
* data={this.props.data}
* extraData={this.state}
* keyExtractor={this._keyExtractor}
* renderItem={this._renderItem}
* />
* );
* }
* }
*
* This is a convenience wrapper around [`<VirtualizedList>`](docs/virtualizedlist.html),
* and thus inherits its props (as well as those of `ScrollView`) that aren't explicitly listed
* here, along with the following caveats:
*
* - Internal state is not preserved when content scrolls out of the render window. Make sure all
* your data is captured in the item data or external stores like Flux, Redux, or Relay.
* - This is a `PureComponent` which means that it will not re-render if `props` remain shallow-
* equal. Make sure that everything your `renderItem` function depends on is passed as a prop
* (e.g. `extraData`) that is not `===` after updates, otherwise your UI may not update on
* changes. This includes the `data` prop and parent component state.
* - In order to constrain memory and enable smooth scrolling, content is rendered asynchronously
* offscreen. This means it's possible to scroll faster than the fill rate ands momentarily see
* blank content. This is a tradeoff that can be adjusted to suit the needs of each application,
* and we are working on improving it behind the scenes.
* - By default, the list looks for a `key` prop on each item and uses that for the React key.
* Alternatively, you can provide a custom `keyExtractor` prop.
*
* Also inherits [ScrollView Props](docs/scrollview.html#props), unless it is nested in another FlatList of same orientation.
*/
class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
static defaultProps: DefaultProps = defaultProps;
props: Props<ItemT>;
/**
* Scrolls to the end of the content. May be janky without `getItemLayout` prop.
*/
scrollToEnd(params?: ?{animated?: ?boolean, ...}) {
if (this._listRef) {
this._listRef.scrollToEnd(params);
}
}
/**
* Scrolls to the item at the specified index such that it is positioned in the viewable area
* such that `viewPosition` 0 places it at the top, 1 at the bottom, and 0.5 centered in the
* middle. `viewOffset` is a fixed number of pixels to offset the final target position.
*
* Note: cannot scroll to locations outside the render window without specifying the
* `getItemLayout` prop.
*/
scrollToIndex(params: {
animated?: ?boolean,
index: number,
viewOffset?: number,
viewPosition?: number,
...
}) {
if (this._listRef) {
this._listRef.scrollToIndex(params);
}
}
/**
* Requires linear scan through data - use `scrollToIndex` instead if possible.
*
* Note: cannot scroll to locations outside the render window without specifying the
* `getItemLayout` prop.
*/
scrollToItem(params: {
animated?: ?boolean,
item: ItemT,
viewPosition?: number,
...
}) {
if (this._listRef) {
this._listRef.scrollToItem(params);
}
}
/**
* Scroll to a specific content pixel offset in the list.
*
* Check out [scrollToOffset](docs/virtualizedlist.html#scrolltooffset) of VirtualizedList
*/
scrollToOffset(params: {animated?: ?boolean, offset: number, ...}) {
if (this._listRef) {
this._listRef.scrollToOffset(params);
}
}
/**
* Tells the list an interaction has occurred, which should trigger viewability calculations, e.g.
* if `waitForInteractions` is true and the user has not scrolled. This is typically called by
* taps on items or by navigation actions.
*/
recordInteraction() {
if (this._listRef) {
this._listRef.recordInteraction();
}
}
/**
* Displays the scroll indicators momentarily.
*
* @platform ios
*/
flashScrollIndicators() {
if (this._listRef) {
this._listRef.flashScrollIndicators();
}
}
/**
* Provides a handle to the underlying scroll responder.
*/
getScrollResponder(): ?ScrollResponderType {
if (this._listRef) {
return this._listRef.getScrollResponder();
}
}
/**
* Provides a reference to the underlying host component
*/
getNativeScrollRef():
| ?React.ElementRef<typeof View>
| ?React.ElementRef<ScrollViewNativeComponentType> {
if (this._listRef) {
return this._listRef.getScrollRef();
}
}
getScrollableNode(): any {
if (this._listRef) {
return this._listRef.getScrollableNode();
}
}
setNativeProps(props: {[string]: mixed, ...}) {
if (this._listRef) {
this._listRef.setNativeProps(props);
}
}
constructor(props: Props<ItemT>) {
super(props);
this._checkProps(this.props);
if (this.props.viewabilityConfigCallbackPairs) {
this._virtualizedListPairs = this.props.viewabilityConfigCallbackPairs.map(
pair => ({
viewabilityConfig: pair.viewabilityConfig,
onViewableItemsChanged: this._createOnViewableItemsChanged(
pair.onViewableItemsChanged,
),
}),
);
} else if (this.props.onViewableItemsChanged) {
this._virtualizedListPairs.push({
/* $FlowFixMe(>=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. */
viewabilityConfig: this.props.viewabilityConfig,
onViewableItemsChanged: this._createOnViewableItemsChanged(
this.props.onViewableItemsChanged,
),
});
}
}
componentDidUpdate(prevProps: Props<ItemT>) {
invariant(
prevProps.numColumns === this.props.numColumns,
'Changing numColumns on the fly is not supported. Change the key prop on FlatList when ' +
'changing the number of columns to force a fresh render of the component.',
);
invariant(
prevProps.onViewableItemsChanged === this.props.onViewableItemsChanged,
'Changing onViewableItemsChanged on the fly is not supported',
);
invariant(
!deepDiffer(prevProps.viewabilityConfig, this.props.viewabilityConfig),
'Changing viewabilityConfig on the fly is not supported',
);
invariant(
prevProps.viewabilityConfigCallbackPairs ===
this.props.viewabilityConfigCallbackPairs,
'Changing viewabilityConfigCallbackPairs on the fly is not supported',
);
this._checkProps(this.props);
}
_listRef: ?React.ElementRef<typeof VirtualizedList>;
_virtualizedListPairs: Array<ViewabilityConfigCallbackPair> = [];
_captureRef = ref => {
this._listRef = ref;
};
_checkProps(props: Props<ItemT>) {
const {
// $FlowFixMe this prop doesn't exist, is only used for an invariant
getItem,
// $FlowFixMe this prop doesn't exist, is only used for an invariant
getItemCount,
horizontal,
numColumns,
columnWrapperStyle,
onViewableItemsChanged,
viewabilityConfigCallbackPairs,
} = props;
invariant(
!getItem && !getItemCount,
'FlatList does not support custom data formats.',
);
if (numColumns > 1) {
invariant(!horizontal, 'numColumns does not support horizontal.');
} else {
invariant(
!columnWrapperStyle,
'columnWrapperStyle not supported for single column lists',
);
}
invariant(
!(onViewableItemsChanged && viewabilityConfigCallbackPairs),
'FlatList does not support setting both onViewableItemsChanged and ' +
'viewabilityConfigCallbackPairs.',
);
}
_getItem = (data: Array<ItemT>, index: number) => {
const {numColumns} = this.props;
if (numColumns > 1) {
const ret = [];
for (let kk = 0; kk < numColumns; kk++) {
const item = data[index * numColumns + kk];
if (item != null) {
ret.push(item);
}
}
return ret;
} else {
return data[index];
}
};
_getItemCount = (data: ?Array<ItemT>): number => {
if (data) {
const {numColumns} = this.props;
return numColumns > 1 ? Math.ceil(data.length / numColumns) : data.length;
} else {
return 0;
}
};
_keyExtractor = (items: ItemT | Array<ItemT>, index: number) => {
const {keyExtractor, numColumns} = this.props;
if (numColumns > 1) {
invariant(
Array.isArray(items),
'FlatList: Encountered internal consistency error, expected each item to consist of an ' +
'array with 1-%s columns; instead, received a single item.',
numColumns,
);
return items
.map((it, kk) => keyExtractor(it, index * numColumns + kk))
.join(':');
} else {
// $FlowFixMe Can't call keyExtractor with an array
return keyExtractor(items, index);
}
};
_pushMultiColumnViewable(arr: Array<ViewToken>, v: ViewToken): void {
const {numColumns, keyExtractor} = this.props;
v.item.forEach((item, ii) => {
invariant(v.index != null, 'Missing index!');
const index = v.index * numColumns + ii;
arr.push({...v, item, key: keyExtractor(item, index), index});
});
}
_createOnViewableItemsChanged(
onViewableItemsChanged: ?(info: {
viewableItems: Array<ViewToken>,
changed: Array<ViewToken>,
...
}) => void,
) {
return (info: {
viewableItems: Array<ViewToken>,
changed: Array<ViewToken>,
...
}) => {
const {numColumns} = this.props;
if (onViewableItemsChanged) {
if (numColumns > 1) {
const changed = [];
const viewableItems = [];
info.viewableItems.forEach(v =>
this._pushMultiColumnViewable(viewableItems, v),
);
info.changed.forEach(v => this._pushMultiColumnViewable(changed, v));
onViewableItemsChanged({viewableItems, changed});
} else {
onViewableItemsChanged(info);
}
}
};
}
_renderer = () => {
const {
ListItemComponent,
renderItem,
numColumns,
columnWrapperStyle,
} = this.props;
let virtualizedListRenderKey = ListItemComponent
? 'ListItemComponent'
: 'renderItem';
const renderer = (props): React.Node => {
if (ListItemComponent) {
// $FlowFixMe Component isn't valid
return <ListItemComponent {...props} />;
} else if (renderItem) {
return renderItem(props);
} else {
return null;
}
};
return {
/* $FlowFixMe(>=0.111.0 site=react_native_fb) This comment suppresses an
* error found when Flow v0.111 was deployed. To see the error, delete
* this comment and run Flow. */
[virtualizedListRenderKey]: (info: RenderItemProps<ItemT>) => {
if (numColumns > 1) {
const {item, index} = info;
invariant(
Array.isArray(item),
'Expected array of items with numColumns > 1',
);
return (
<View
style={StyleSheet.compose(
styles.row,
columnWrapperStyle,
)}>
{item.map((it, kk) => {
const element = renderer({
item: it,
index: index * numColumns + kk,
separators: info.separators,
});
return element != null ? (
<React.Fragment key={kk}>{element}</React.Fragment>
) : null;
})}
</View>
);
} else {
return renderer(info);
}
},
};
};
render(): React.Node {
const {numColumns, columnWrapperStyle, ...restProps} = this.props;
return (
<VirtualizedList
{...restProps}
getItem={this._getItem}
getItemCount={this._getItemCount}
keyExtractor={this._keyExtractor}
ref={this._captureRef}
viewabilityConfigCallbackPairs={this._virtualizedListPairs}
{...this._renderer()}
/>
);
}
}
const styles = StyleSheet.create({
row: {flexDirection: 'row'},
});
module.exports = FlatList;

View File

@ -0,0 +1,264 @@
/**
* Copyright (c) Facebook, Inc. and its 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';
const Platform = require('../Utilities/Platform');
const React = require('react');
const VirtualizedSectionList = require('./VirtualizedSectionList');
import type {ScrollResponderType} from '../Components/ScrollView/ScrollView';
import type {
SectionBase as _SectionBase,
Props as VirtualizedSectionListProps,
ScrollToLocationParamsType,
} from './VirtualizedSectionList';
type Item = any;
export type SectionBase<SectionItemT> = _SectionBase<SectionItemT>;
type RequiredProps<SectionT: SectionBase<any>> = {|
/**
* The actual data to render, akin to the `data` prop in [`<FlatList>`](https://reactnative.dev/docs/flatlist.html).
*
* General shape:
*
* sections: $ReadOnlyArray<{
* data: $ReadOnlyArray<SectionItem>,
* renderItem?: ({item: SectionItem, ...}) => ?React.Element<*>,
* ItemSeparatorComponent?: ?ReactClass<{highlighted: boolean, ...}>,
* }>
*/
sections: $ReadOnlyArray<SectionT>,
|};
type OptionalProps<SectionT: SectionBase<any>> = {|
/**
* Default renderer for every item in every section. Can be over-ridden on a per-section basis.
*/
renderItem?: (info: {
item: Item,
index: number,
section: SectionT,
separators: {
highlight: () => void,
unhighlight: () => void,
updateProps: (select: 'leading' | 'trailing', newProps: Object) => void,
...
},
...
}) => null | React.Element<any>,
/**
* A marker property for telling the list to re-render (since it implements `PureComponent`). If
* any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the
* `data` prop, stick it here and treat it immutably.
*/
extraData?: any,
/**
* How many items to render in the initial batch. This should be enough to fill the screen but not
* much more. Note these items will never be unmounted as part of the windowed rendering in order
* to improve perceived performance of scroll-to-top actions.
*/
initialNumToRender: number,
/**
* Reverses the direction of scroll. Uses scale transforms of -1.
*/
inverted?: ?boolean,
/**
* Used to extract a unique key for a given item at the specified index. Key is used for caching
* and as the react key to track item re-ordering. The default extractor checks item.key, then
* falls back to using the index, like react does. Note that this sets keys for each item, but
* each overall section still needs its own key.
*/
keyExtractor: (item: Item, index: number) => string,
/**
* Called once when the scroll position gets within `onEndReachedThreshold` of the rendered
* content.
*/
onEndReached?: ?(info: {distanceFromEnd: number, ...}) => void,
/**
* Note: may have bugs (missing content) in some circumstances - use at your own risk.
*
* This may improve scroll performance for large lists.
*/
removeClippedSubviews?: boolean,
|};
export type Props<SectionT> = {|
...$Diff<
VirtualizedSectionListProps<SectionT>,
{
getItem: $PropertyType<VirtualizedSectionListProps<SectionT>, 'getItem'>,
getItemCount: $PropertyType<
VirtualizedSectionListProps<SectionT>,
'getItemCount',
>,
renderItem: $PropertyType<
VirtualizedSectionListProps<SectionT>,
'renderItem',
>,
...
},
>,
...RequiredProps<SectionT>,
...OptionalProps<SectionT>,
|};
const defaultProps = {
...VirtualizedSectionList.defaultProps,
stickySectionHeadersEnabled: Platform.OS === 'ios',
};
type DefaultProps = typeof defaultProps;
/**
* A performant interface for rendering sectioned lists, supporting the most handy features:
*
* - Fully cross-platform.
* - Configurable viewability callbacks.
* - List header support.
* - List footer support.
* - Item separator support.
* - Section header support.
* - Section separator support.
* - Heterogeneous data and item rendering support.
* - Pull to Refresh.
* - Scroll loading.
*
* If you don't need section support and want a simpler interface, use
* [`<FlatList>`](https://reactnative.dev/docs/flatlist.html).
*
* Simple Examples:
*
* <SectionList
* renderItem={({item}) => <ListItem title={item} />}
* renderSectionHeader={({section}) => <Header title={section.title} />}
* sections={[ // homogeneous rendering between sections
* {data: [...], title: ...},
* {data: [...], title: ...},
* {data: [...], title: ...},
* ]}
* />
*
* <SectionList
* sections={[ // heterogeneous rendering between sections
* {data: [...], renderItem: ...},
* {data: [...], renderItem: ...},
* {data: [...], renderItem: ...},
* ]}
* />
*
* This is a convenience wrapper around [`<VirtualizedList>`](docs/virtualizedlist.html),
* and thus inherits its props (as well as those of `ScrollView`) that aren't explicitly listed
* here, along with the following caveats:
*
* - Internal state is not preserved when content scrolls out of the render window. Make sure all
* your data is captured in the item data or external stores like Flux, Redux, or Relay.
* - This is a `PureComponent` which means that it will not re-render if `props` remain shallow-
* equal. Make sure that everything your `renderItem` function depends on is passed as a prop
* (e.g. `extraData`) that is not `===` after updates, otherwise your UI may not update on
* changes. This includes the `data` prop and parent component state.
* - In order to constrain memory and enable smooth scrolling, content is rendered asynchronously
* offscreen. This means it's possible to scroll faster than the fill rate and momentarily see
* blank content. This is a tradeoff that can be adjusted to suit the needs of each application,
* and we are working on improving it behind the scenes.
* - By default, the list looks for a `key` prop on each item and uses that for the React key.
* Alternatively, you can provide a custom `keyExtractor` prop.
*
*/
class SectionList<SectionT: SectionBase<any>> extends React.PureComponent<
Props<SectionT>,
void,
> {
props: Props<SectionT>;
static defaultProps: DefaultProps = defaultProps;
/**
* Scrolls to the item at the specified `sectionIndex` and `itemIndex` (within the section)
* positioned in the viewable area such that `viewPosition` 0 places it at the top (and may be
* covered by a sticky header), 1 at the bottom, and 0.5 centered in the middle. `viewOffset` is a
* fixed number of pixels to offset the final target position, e.g. to compensate for sticky
* headers.
*
* Note: cannot scroll to locations outside the render window without specifying the
* `getItemLayout` prop.
*/
scrollToLocation(params: ScrollToLocationParamsType) {
if (this._wrapperListRef != null) {
this._wrapperListRef.scrollToLocation(params);
}
}
/**
* Tells the list an interaction has occurred, which should trigger viewability calculations, e.g.
* if `waitForInteractions` is true and the user has not scrolled. This is typically called by
* taps on items or by navigation actions.
*/
recordInteraction() {
const listRef = this._wrapperListRef && this._wrapperListRef.getListRef();
listRef && listRef.recordInteraction();
}
/**
* Displays the scroll indicators momentarily.
*
* @platform ios
*/
flashScrollIndicators() {
const listRef = this._wrapperListRef && this._wrapperListRef.getListRef();
listRef && listRef.flashScrollIndicators();
}
/**
* Provides a handle to the underlying scroll responder.
*/
getScrollResponder(): ?ScrollResponderType {
const listRef = this._wrapperListRef && this._wrapperListRef.getListRef();
if (listRef) {
return listRef.getScrollResponder();
}
}
getScrollableNode(): any {
const listRef = this._wrapperListRef && this._wrapperListRef.getListRef();
if (listRef) {
return listRef.getScrollableNode();
}
}
setNativeProps(props: Object) {
const listRef = this._wrapperListRef && this._wrapperListRef.getListRef();
if (listRef) {
listRef.setNativeProps(props);
}
}
render(): React.Node {
return (
<VirtualizedSectionList
{...this.props}
ref={this._captureRef}
getItemCount={items => items.length}
getItem={(items, index) => items[index]}
/>
);
}
_wrapperListRef: ?React.ElementRef<typeof VirtualizedSectionList>;
_captureRef = ref => {
/* $FlowFixMe(>=0.99.0 site=react_native_fb) This comment suppresses an
* error found when Flow v0.99 was deployed. To see the error, delete this
* comment and run Flow. */
this._wrapperListRef = ref;
};
}
module.exports = SectionList;

View File

@ -0,0 +1,341 @@
/**
* Copyright (c) Facebook, Inc. and its 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';
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(>=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(
itemCount: number,
scrollOffset: number,
viewportHeight: number,
getFrameMetrics: (
index: number,
) => ?{
length: number,
offset: number,
...
},
// Optional optimization to reduce the scan size
renderRange?: {
first: number,
last: number,
...
},
): Array<number> {
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);
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(
itemCount: number,
scrollOffset: number,
viewportHeight: number,
getFrameMetrics: (
index: number,
) => ?{
length: number,
offset: number,
...
},
createViewToken: (index: number, isViewable: boolean) => ViewToken,
onViewableItemsChanged: ({
viewableItems: Array<ViewToken>,
changed: Array<ViewToken>,
...
}) => void,
// Optional optimization to reduce the scan size
renderRange?: {
first: number,
last: number,
...
},
): void {
if (
(this._config.waitForInteraction && !this._hasInteracted) ||
itemCount === 0 ||
!getFrameMetrics(0)
) {
return;
}
let viewableIndices = [];
if (itemCount) {
viewableIndices = this.computeViewableItems(
itemCount,
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 = setTimeout(() => {
/* $FlowFixMe(>=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(
viewableIndices,
onViewableItemsChanged,
createViewToken,
);
}, this._config.minimumViewTime);
/* $FlowFixMe(>=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(
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(
viewableIndicesToCheck,
onViewableItemsChanged,
createViewToken,
) {
// 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);
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;

View File

@ -0,0 +1,247 @@
/**
* Copyright (c) Facebook, Inc. and its 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';
const invariant = require('invariant');
/**
* 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.
*/
function elementsThatOverlapOffsets(
offsets: Array<number>,
itemCount: number,
getFrameMetrics: (
index: number,
) => {
length: number,
offset: number,
...
},
): Array<number> {
const out = [];
let outLength = 0;
for (let ii = 0; ii < itemCount; ii++) {
const frame = getFrameMetrics(ii);
const trailingOffset = frame.offset + frame.length;
for (let kk = 0; kk < offsets.length; kk++) {
if (out[kk] == null && trailingOffset >= offsets[kk]) {
out[kk] = ii;
outLength++;
if (kk === offsets.length - 1) {
invariant(
outLength === offsets.length,
'bad offsets input, should be in increasing order: %s',
JSON.stringify(offsets),
);
return out;
}
}
}
}
return out;
}
/**
* 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.
*/
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.
*/
function computeWindowedRenderLimits(
props: {
data: any,
getItemCount: (data: any) => number,
maxToRenderPerBatch: number,
windowSize: number,
...
},
prev: {
first: number,
last: number,
...
},
getFrameMetricsApprox: (
index: number,
) => {
length: number,
offset: number,
...
},
scrollMetrics: {
dt: number,
offset: number,
velocity: number,
visibleLength: number,
...
},
): {
first: number,
last: number,
...
} {
const {data, getItemCount, maxToRenderPerBatch, windowSize} = props;
const itemCount = getItemCount(data);
if (itemCount === 0) {
return prev;
}
const {offset, velocity, visibleLength} = 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).offset;
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.getItemCount(props.data),
getFrameMetricsApprox,
);
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};
}
const VirtualizeUtils = {
computeWindowedRenderLimits,
elementsThatOverlapOffsets,
newRangeCount,
};
module.exports = VirtualizeUtils;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,578 @@
/**
* Copyright (c) Facebook, Inc. and its 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';
const React = require('react');
const View = require('../Components/View/View');
const VirtualizedList = require('./VirtualizedList');
const invariant = require('invariant');
import type {ViewToken} from './ViewabilityHelper';
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.ElementProps<typeof VirtualizedList>;
export type Props<SectionT> = {|
...RequiredProps<SectionT>,
...OptionalProps<SectionT>,
...$Diff<
VirtualizedListProps,
{renderItem: $PropertyType<VirtualizedListProps, 'renderItem'>, ...},
>,
|};
export type ScrollToLocationParamsType = {|
animated?: ?boolean,
itemIndex: number,
sectionIndex: number,
viewOffset?: number,
viewPosition?: number,
|};
type DefaultProps = {|
...typeof VirtualizedList.defaultProps,
data: $ReadOnlyArray<Item>,
|};
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> {
static defaultProps: DefaultProps = {
...VirtualizedList.defaultProps,
data: [],
};
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 (params.itemIndex > 0 && this.props.stickySectionHeadersEnabled) {
// $FlowFixMe Cannot access private property
const frame = this._listRef._getFrameMetricsApprox(
index - params.itemIndex,
);
viewOffset += frame.length;
}
const toIndexParams = {
...params,
viewOffset,
index,
};
this._listRef.scrollToIndex(toIndexParams);
}
getListRef(): VirtualizedList {
return this._listRef;
}
constructor(props: Props<SectionT>, context: Object) {
super(props, context);
this.state = this._computeState(props);
}
UNSAFE_componentWillReceiveProps(nextProps: Props<SectionT>) {
this.setState(this._computeState(nextProps));
}
_computeState(props: Props<SectionT>): State {
const offset = props.ListHeaderComponent ? 1 : 0;
const stickyHeaderIndices = [];
const itemCount = props.sections
? props.sections.reduce((v, section) => {
stickyHeaderIndices.push(v + offset);
return v + props.getItemCount(section.data) + 2; // Add two for the section header and footer.
}, 0)
: 0;
const {
SectionSeparatorComponent,
renderItem,
renderSectionFooter,
renderSectionHeader,
sections: _sections,
stickySectionHeadersEnabled,
...restProps
} = props;
return {
childProps: {
...restProps,
renderItem: this._renderItem,
ItemSeparatorComponent: undefined, // Rendered with renderItem
data: props.sections,
getItemCount: () => itemCount,
// $FlowFixMe
getItem: (sections, index) => this._getItem(props, sections, index),
keyExtractor: this._keyExtractor,
onViewableItemsChanged: props.onViewableItemsChanged
? this._onViewableItemsChanged
: undefined,
stickyHeaderIndices: props.stickySectionHeadersEnabled
? stickyHeaderIndices
: undefined,
},
};
}
render(): React.Node {
return (
<VirtualizedList {...this.state.childProps} 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;
};
_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;
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 keyExtractor = info.section.keyExtractor || this.props.keyExtractor;
return {
...viewable,
index: info.index,
/* $FlowFixMe(>=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. */
key: keyExtractor(viewable.item, info.index),
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 = ({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);
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}
onUpdateSeparator={this._onUpdateSeparator}
prevCellKey={(this._subExtractor(index - 1) || {}).key}
ref={ref => {
this._cellRefs[info.key] = ref;
}}
renderItem={renderItem}
section={info.section}
trailingItem={info.trailingItem}
trailingSection={info.trailingSection}
inverted={!!this.props.inverted}
/>
);
}
};
_onUpdateSeparator = (key: string, newProps: Object) => {
const ref = this._cellRefs[key];
ref && ref.updateSeparatorProps(newProps);
};
_getSeparatorComponent(
index: number,
info?: ?Object,
): ?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 === this.state.childProps.getItemCount() - 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;
}
_cellRefs = {};
_listRef: VirtualizedList;
_captureRef = ref => {
/* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This comment
* suppresses an error when upgrading Flow's support for React. To see the
* error delete this comment and run Flow. */
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,
onUpdateSeparator: (cellKey: string, newProps: Object) => void,
prevCellKey?: ?string,
renderItem: Function,
inverted: boolean,
|}>;
type ItemWithSeparatorState = {
separatorProps: $ReadOnly<{|
highlighted: false,
...ItemWithSeparatorCommonProps,
|}>,
leadingSeparatorProps: $ReadOnly<{|
highlighted: false,
...ItemWithSeparatorCommonProps,
|}>,
...
};
class ItemWithSeparator extends React.Component<
ItemWithSeparatorProps,
ItemWithSeparatorState,
> {
state = {
separatorProps: {
highlighted: false,
leadingItem: this.props.item,
leadingSection: this.props.leadingSection,
section: this.props.section,
trailingItem: this.props.trailingItem,
trailingSection: this.props.trailingSection,
},
leadingSeparatorProps: {
highlighted: false,
leadingItem: this.props.leadingItem,
leadingSection: this.props.leadingSection,
section: this.props.section,
trailingItem: this.props.item,
trailingSection: this.props.trailingSection,
},
};
_separators = {
highlight: () => {
['leading', 'trailing'].forEach(s =>
this._separators.updateProps(s, {highlighted: true}),
);
},
unhighlight: () => {
['leading', 'trailing'].forEach(s =>
this._separators.updateProps(s, {highlighted: false}),
);
},
updateProps: (select: 'leading' | 'trailing', newProps: Object) => {
const {LeadingSeparatorComponent, cellKey, prevCellKey} = this.props;
if (select === 'leading' && LeadingSeparatorComponent != null) {
this.setState(state => ({
leadingSeparatorProps: {...state.leadingSeparatorProps, ...newProps},
}));
} else {
this.props.onUpdateSeparator(
(select === 'leading' && prevCellKey) || cellKey,
newProps,
);
}
},
};
static getDerivedStateFromProps(
props: ItemWithSeparatorProps,
prevState: ItemWithSeparatorState,
): ?ItemWithSeparatorState {
return {
separatorProps: {
...prevState.separatorProps,
leadingItem: props.item,
leadingSection: props.leadingSection,
section: props.section,
trailingItem: props.trailingItem,
trailingSection: props.trailingSection,
},
leadingSeparatorProps: {
...prevState.leadingSeparatorProps,
leadingItem: props.leadingItem,
leadingSection: props.leadingSection,
section: props.section,
trailingItem: props.item,
trailingSection: props.trailingSection,
},
};
}
updateSeparatorProps(newProps: Object) {
this.setState(state => ({
separatorProps: {...state.separatorProps, ...newProps},
}));
}
render() {
const {
LeadingSeparatorComponent,
SeparatorComponent,
item,
index,
section,
inverted,
} = this.props;
const element = this.props.renderItem({
item,
index,
section,
separators: this._separators,
});
const leadingSeparator = LeadingSeparatorComponent && (
<LeadingSeparatorComponent {...this.state.leadingSeparatorProps} />
);
const separator = SeparatorComponent && (
<SeparatorComponent {...this.state.separatorProps} />
);
return leadingSeparator || separator ? (
/* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses an
* error found when Flow v0.89 was deployed. To see the error, delete
* this comment and run Flow. */
<View>
{!inverted ? leadingSeparator : separator}
{element}
{!inverted ? separator : leadingSeparator}
</View>
) : (
element
);
}
}
module.exports = VirtualizedSectionList;

View File

@ -0,0 +1,117 @@
/**
* Copyright (c) Facebook, Inc. and its 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
*/
'use strict';
const FlatList = require('../FlatList');
const React = require('react');
function renderMyListItem(info: {
item: {title: string, ...},
index: number,
...
}) {
return <span />;
}
module.exports = {
testEverythingIsFine(): React.Node {
const data = [
{
title: 'Title Text',
key: 1,
},
];
return <FlatList renderItem={renderMyListItem} data={data} />;
},
testBadDataWithTypicalItem(): React.Node {
const data = [
{
title: 6,
key: 1,
},
];
// $FlowExpectedError - bad title type 6, should be string
return <FlatList renderItem={renderMyListItem} data={data} />;
},
testMissingFieldWithTypicalItem(): React.Node {
const data = [
{
key: 1,
},
];
// $FlowExpectedError - missing title
return <FlatList renderItem={renderMyListItem} data={data} />;
},
testGoodDataWithBadCustomRenderItemFunction(): React.Node {
const data = [
{
widget: 6,
key: 1,
},
];
return (
<FlatList
renderItem={info => (
<span>
{
// $FlowExpectedError - bad widgetCount type 6, should be Object
info.item.widget.missingProp
}
</span>
)}
data={data}
/>
);
},
testBadRenderItemFunction(): $TEMPORARY$array<React.Node> {
const data = [
{
title: 'foo',
key: 1,
},
];
return [
// $FlowExpectedError - title should be inside `item`
<FlatList renderItem={(info: {title: string}) => <span />} data={data} />,
<FlatList
// $FlowExpectedError - bad index type string, should be number
renderItem={(info: {item: any, index: string}) => <span />}
data={data}
/>,
<FlatList
// $FlowExpectedError - bad title type number, should be string
renderItem={(info: {item: {title: number}}) => <span />}
data={data}
/>,
// EverythingIsFine
<FlatList
// $FlowExpectedError - bad title type number, should be string
renderItem={(info: {item: {title: string, ...}, ...}) => <span />}
data={data}
/>,
];
},
testOtherBadProps(): $TEMPORARY$array<React.Node> {
return [
// $FlowExpectedError - bad numColumns type "lots"
<FlatList renderItem={renderMyListItem} data={[]} numColumns="lots" />,
// $FlowExpectedError - bad windowSize type "big"
<FlatList renderItem={renderMyListItem} data={[]} windowSize="big" />,
// $FlowExpectedError - missing `data` prop
<FlatList renderItem={renderMyListItem} />,
];
},
};

View File

@ -0,0 +1,134 @@
/**
* Copyright (c) Facebook, Inc. and its 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';
const React = require('react');
const SectionList = require('../SectionList');
function renderMyListItem(info: {
item: {title: string, ...},
index: number,
...
}) {
return <span />;
}
const renderMyHeader = ({
section,
}: {
section: {fooNumber: number, ...} & Object,
...
}) => <span />;
module.exports = {
testGoodDataWithGoodItem(): React.Node {
const sections = [
{
key: 'a',
data: [
{
title: 'foo',
key: 1,
},
],
},
];
return <SectionList renderItem={renderMyListItem} sections={sections} />;
},
testBadRenderItemFunction(): $TEMPORARY$array<React.Node> {
const sections = [
{
key: 'a',
data: [
{
title: 'foo',
key: 1,
},
],
},
];
return [
<SectionList
// $FlowExpectedError - title should be inside `item`
renderItem={(info: {title: string, ...}) => <span />}
sections={sections}
/>,
<SectionList
// $FlowExpectedError - bad index type string, should be number
renderItem={(info: {index: string}) => <span />}
sections={sections}
/>,
// EverythingIsFine
<SectionList
renderItem={(info: {item: {title: string, ...}, ...}) => <span />}
sections={sections}
/>,
];
},
testBadInheritedDefaultProp(): React.Element<*> {
const sections = [];
return (
<SectionList
renderItem={renderMyListItem}
sections={sections}
// $FlowExpectedError - bad windowSize type "big"
windowSize="big"
/>
);
},
testMissingData(): React.Element<*> {
// $FlowExpectedError - missing `sections` prop
return <SectionList renderItem={renderMyListItem} />;
},
testBadSectionsShape(): React.Element<*> {
const sections = [
{
key: 'a',
items: [
{
title: 'foo',
key: 1,
},
],
},
];
// $FlowExpectedError - section missing `data` field
return <SectionList renderItem={renderMyListItem} sections={sections} />;
},
testBadSectionsMetadata(): React.Element<*> {
const sections = [
{
key: 'a',
fooNumber: 'string',
data: [
{
title: 'foo',
key: 1,
},
],
},
];
return (
<SectionList
renderSectionHeader={renderMyHeader}
renderItem={renderMyListItem}
/* $FlowExpectedError - section has bad meta data `fooNumber` field of
* type string */
sections={sections}
/>
);
},
};