Add basic nested VirtualizedList support
Summary: This uses `context` to render inner lists of the same orientation to a plain `View` without virtualization instead of rendering nested `ScrollView`s trying to scroll in the same direction, which can cause problems. Reviewed By: bvaughn Differential Revision: D5174942 fbshipit-source-id: 989150294098de837b0ffb401c7f5679a3928a03
This commit is contained in:
parent
2c32acb755
commit
63f7efcd32
|
@ -13,6 +13,7 @@
|
|||
|
||||
const Batchinator = require('Batchinator');
|
||||
const FillRateHelper = require('FillRateHelper');
|
||||
const PropTypes = require('prop-types');
|
||||
const React = require('React');
|
||||
const ReactNative = require('ReactNative');
|
||||
const RefreshControl = require('RefreshControl');
|
||||
|
@ -139,7 +140,7 @@ type OptionalProps = {
|
|||
/**
|
||||
* Render a custom scroll component, e.g. with a differently styled `RefreshControl`.
|
||||
*/
|
||||
renderScrollComponent: (props: Object) => React.Element<any>,
|
||||
renderScrollComponent?: (props: Object) => React.Element<any>,
|
||||
/**
|
||||
* Amount of time between low-pri item render batches, e.g. for rendering items quite a ways off
|
||||
* screen. Similar fill rate/responsiveness tradeoff as `maxToRenderPerBatch`.
|
||||
|
@ -301,35 +302,32 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
|
|||
},
|
||||
maxToRenderPerBatch: 10,
|
||||
onEndReachedThreshold: 2, // multiples of length
|
||||
renderScrollComponent: (props: Props) => {
|
||||
if (props.onRefresh) {
|
||||
invariant(
|
||||
typeof props.refreshing === 'boolean',
|
||||
'`refreshing` prop must be set as a boolean in order to use `onRefresh`, but got `' +
|
||||
JSON.stringify(props.refreshing) + '`',
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
{...props}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={props.refreshing}
|
||||
onRefresh={props.onRefresh}
|
||||
progressViewOffset={props.progressViewOffset}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <ScrollView {...props} />;
|
||||
}
|
||||
},
|
||||
scrollEventThrottle: 50,
|
||||
updateCellsBatchingPeriod: 50,
|
||||
windowSize: 21, // multiples of length
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
virtualizedList: PropTypes.shape({
|
||||
horizontal: PropTypes.bool,
|
||||
}),
|
||||
};
|
||||
|
||||
static childContextTypes = {
|
||||
virtualizedList: PropTypes.shape({
|
||||
horizontal: PropTypes.bool,
|
||||
}),
|
||||
};
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
virtualizedList: {
|
||||
horizontal: this.props.horizontal,
|
||||
// TODO: support nested virtualization and onViewableItemsChanged
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
state: State;
|
||||
|
||||
constructor(props: Props, context: Object) {
|
||||
|
@ -339,6 +337,11 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
|
|||
'Components based on VirtualizedList must be wrapped with Animated.createAnimatedComponent ' +
|
||||
'to support native onScroll events with useNativeDriver',
|
||||
);
|
||||
invariant(
|
||||
!(this._isNestedWithSameOrientation() && props.onViewableItemsChanged),
|
||||
'Nesting lists that scroll in the same direction does not support onViewableItemsChanged' +
|
||||
'on the inner list.'
|
||||
);
|
||||
|
||||
this._fillRateHelper = new FillRateHelper(this._getFrameMetrics);
|
||||
this._updateCellsToRenderBatcher = new Batchinator(
|
||||
|
@ -431,6 +434,15 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
|
|||
});
|
||||
};
|
||||
|
||||
_isVirtualizationDisabled(): bool {
|
||||
return this.props.disableVirtualization || this._isNestedWithSameOrientation();
|
||||
}
|
||||
|
||||
_isNestedWithSameOrientation(): bool {
|
||||
const nestedContext = this.context.virtualizedList;
|
||||
return !!(nestedContext && !!nestedContext.horizontal === !!this.props.horizontal);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (__DEV__) {
|
||||
const flatStyles = flattenStyle(this.props.contentContainerStyle);
|
||||
|
@ -442,7 +454,8 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
|
|||
}
|
||||
|
||||
const {ListEmptyComponent, ListFooterComponent, ListHeaderComponent} = this.props;
|
||||
const {data, disableVirtualization, horizontal} = this.props;
|
||||
const {data, horizontal} = this.props;
|
||||
const isVirtualizationDisabled = this._isVirtualizationDisabled();
|
||||
const cells = [];
|
||||
const stickyIndicesFromProps = new Set(this.props.stickyHeaderIndices);
|
||||
const stickyHeaderIndices = [];
|
||||
|
@ -466,7 +479,7 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
|
|||
const {first, last} = this.state;
|
||||
this._pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, 0, lastInitialIndex);
|
||||
const firstAfterInitial = Math.max(lastInitialIndex + 1, first);
|
||||
if (!disableVirtualization && first > lastInitialIndex + 1) {
|
||||
if (!isVirtualizationDisabled && first > lastInitialIndex + 1) {
|
||||
let insertedStickySpacer = false;
|
||||
if (stickyIndicesFromProps.size > 0) {
|
||||
const stickyOffset = ListHeaderComponent ? 1 : 0;
|
||||
|
@ -507,7 +520,7 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
|
|||
);
|
||||
this._hasWarned.keys = true;
|
||||
}
|
||||
if (!disableVirtualization && last < itemCount - 1) {
|
||||
if (!isVirtualizationDisabled && last < itemCount - 1) {
|
||||
const lastFrame = this._getFrameMetricsApprox(last);
|
||||
// Without getItemLayout, we limit our tail spacer to the _highestMeasuredFrameIndex to
|
||||
// prevent the user for hyperscrolling into un-measured area because otherwise content will
|
||||
|
@ -543,18 +556,21 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
|
|||
</View>
|
||||
);
|
||||
}
|
||||
const ret = React.cloneElement(
|
||||
this.props.renderScrollComponent(this.props),
|
||||
{
|
||||
const scrollProps = {
|
||||
...this.props,
|
||||
onContentSizeChange: this._onContentSizeChange,
|
||||
onLayout: this._onLayout,
|
||||
onScroll: this._onScroll,
|
||||
onScrollBeginDrag: this._onScrollBeginDrag,
|
||||
onScrollEndDrag: this._onScrollEndDrag,
|
||||
onMomentumScrollEnd: this._onMomentumScrollEnd,
|
||||
ref: this._captureScrollRef,
|
||||
scrollEventThrottle: this.props.scrollEventThrottle, // TODO: Android support
|
||||
stickyHeaderIndices,
|
||||
};
|
||||
const ret = React.cloneElement(
|
||||
(this.props.renderScrollComponent || this._defaultRenderScrollComponent)(scrollProps),
|
||||
{
|
||||
ref: this._captureScrollRef,
|
||||
},
|
||||
cells,
|
||||
);
|
||||
|
@ -601,6 +617,32 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
_defaultRenderScrollComponent = (props) => {
|
||||
if (this._isNestedWithSameOrientation()) {
|
||||
return <View {...props} />;
|
||||
} else if (props.onRefresh) {
|
||||
invariant(
|
||||
typeof props.refreshing === 'boolean',
|
||||
'`refreshing` prop must be set as a boolean in order to use `onRefresh`, but got `' +
|
||||
JSON.stringify(props.refreshing) + '`',
|
||||
);
|
||||
return (
|
||||
<ScrollView
|
||||
{...props}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={props.refreshing}
|
||||
onRefresh={props.onRefresh}
|
||||
progressViewOffset={props.progressViewOffset}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <ScrollView {...props} />;
|
||||
}
|
||||
};
|
||||
|
||||
_onCellLayout(e, cellKey, index) {
|
||||
const layout = e.nativeEvent.layout;
|
||||
const next = {
|
||||
|
@ -816,14 +858,15 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
|
|||
};
|
||||
|
||||
_updateCellsToRender = () => {
|
||||
const {data, disableVirtualization, getItemCount, onEndReachedThreshold} = this.props;
|
||||
const {data, getItemCount, onEndReachedThreshold} = this.props;
|
||||
const isVirtualizationDisabled = this._isVirtualizationDisabled();
|
||||
this._updateViewableItems(data);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
this.setState((state) => {
|
||||
let newState;
|
||||
if (!disableVirtualization) {
|
||||
if (!isVirtualizationDisabled) {
|
||||
newState = computeWindowedRenderLimits(
|
||||
this.props, state, this._getFrameMetricsApprox, this._scrollMetrics,
|
||||
);
|
||||
|
|
|
@ -135,4 +135,26 @@ describe('VirtualizedList', () => {
|
|||
expect(component).toMatchSnapshot();
|
||||
infos[1].separators.unhighlight();
|
||||
});
|
||||
|
||||
it('handles nested lists', () => {
|
||||
const component = ReactTestRenderer.create(
|
||||
<VirtualizedList
|
||||
data={[{key: 'outer0'}, {key: 'outer1'}]}
|
||||
renderItem={(outerInfo) => (
|
||||
<VirtualizedList
|
||||
data={[{key: outerInfo.item.key + ':inner0'}, {key: outerInfo.item.key + ':inner1'}]}
|
||||
horizontal={outerInfo.item.key === 'outer1'}
|
||||
renderItem={(innerInfo) => {
|
||||
return <item title={innerInfo.item.key} />;
|
||||
}}
|
||||
getItem={(data, index) => data[index]}
|
||||
getItemCount={(data) => data.length}
|
||||
/>
|
||||
)}
|
||||
getItem={(data, index) => data[index]}
|
||||
getItemCount={(data) => data.length}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -52,7 +52,6 @@ exports[`FlatList renders all the bells and whistles 1`] = `
|
|||
}
|
||||
refreshing={false}
|
||||
renderItem={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
|
@ -156,7 +155,6 @@ exports[`FlatList renders empty list 1`] = `
|
|||
onScrollEndDrag={[Function]}
|
||||
onViewableItemsChanged={undefined}
|
||||
renderItem={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
|
@ -186,7 +184,6 @@ exports[`FlatList renders null list 1`] = `
|
|||
onScrollEndDrag={[Function]}
|
||||
onViewableItemsChanged={undefined}
|
||||
renderItem={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
|
@ -228,7 +225,6 @@ exports[`FlatList renders simple list 1`] = `
|
|||
onScrollEndDrag={[Function]}
|
||||
onViewableItemsChanged={undefined}
|
||||
renderItem={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
|
|
|
@ -34,7 +34,6 @@ exports[`SectionList rendering empty section headers is fine 1`] = `
|
|||
onScrollEndDrag={[Function]}
|
||||
onViewableItemsChanged={undefined}
|
||||
renderItem={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
renderSectionHeader={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
sections={
|
||||
|
@ -113,7 +112,6 @@ exports[`SectionList renders a footer when there is no data 1`] = `
|
|||
onScrollEndDrag={[Function]}
|
||||
onViewableItemsChanged={undefined}
|
||||
renderItem={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
renderSectionFooter={[Function]}
|
||||
renderSectionHeader={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
|
@ -180,7 +178,6 @@ exports[`SectionList renders a footer when there is no data and no header 1`] =
|
|||
onScrollEndDrag={[Function]}
|
||||
onViewableItemsChanged={undefined}
|
||||
renderItem={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
renderSectionFooter={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
sections={
|
||||
|
@ -287,7 +284,6 @@ exports[`SectionList renders all the bells and whistles 1`] = `
|
|||
}
|
||||
refreshing={false}
|
||||
renderItem={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
renderSectionFooter={[Function]}
|
||||
renderSectionHeader={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
|
@ -505,7 +501,6 @@ exports[`SectionList renders empty list 1`] = `
|
|||
onScrollEndDrag={[Function]}
|
||||
onViewableItemsChanged={undefined}
|
||||
renderItem={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
sections={Array []}
|
||||
stickyHeaderIndices={Array []}
|
||||
|
|
|
@ -1,5 +1,144 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`VirtualizedList handles nested lists 1`] = `
|
||||
<RCTScrollView
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"key": "outer0",
|
||||
},
|
||||
Object {
|
||||
"key": "outer1",
|
||||
},
|
||||
]
|
||||
}
|
||||
disableVirtualization={false}
|
||||
getItem={[Function]}
|
||||
getItemCount={[Function]}
|
||||
horizontal={false}
|
||||
initialNumToRender={10}
|
||||
keyExtractor={[Function]}
|
||||
maxToRenderPerBatch={10}
|
||||
onContentSizeChange={[Function]}
|
||||
onEndReachedThreshold={2}
|
||||
onLayout={[Function]}
|
||||
onMomentumScrollEnd={[Function]}
|
||||
onScroll={[Function]}
|
||||
onScrollBeginDrag={[Function]}
|
||||
onScrollEndDrag={[Function]}
|
||||
renderItem={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
windowSize={21}
|
||||
>
|
||||
<View>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
>
|
||||
<View
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"key": "outer0:inner0",
|
||||
},
|
||||
Object {
|
||||
"key": "outer0:inner1",
|
||||
},
|
||||
]
|
||||
}
|
||||
disableVirtualization={false}
|
||||
getItem={[Function]}
|
||||
getItemCount={[Function]}
|
||||
horizontal={false}
|
||||
initialNumToRender={10}
|
||||
keyExtractor={[Function]}
|
||||
maxToRenderPerBatch={10}
|
||||
onContentSizeChange={[Function]}
|
||||
onEndReachedThreshold={2}
|
||||
onLayout={[Function]}
|
||||
onMomentumScrollEnd={[Function]}
|
||||
onScroll={[Function]}
|
||||
onScrollBeginDrag={[Function]}
|
||||
onScrollEndDrag={[Function]}
|
||||
renderItem={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
windowSize={21}
|
||||
>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
>
|
||||
<item
|
||||
title="outer0:inner0"
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
>
|
||||
<item
|
||||
title="outer0:inner1"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
>
|
||||
<RCTScrollView
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"key": "outer1:inner0",
|
||||
},
|
||||
Object {
|
||||
"key": "outer1:inner1",
|
||||
},
|
||||
]
|
||||
}
|
||||
disableVirtualization={false}
|
||||
getItem={[Function]}
|
||||
getItemCount={[Function]}
|
||||
horizontal={true}
|
||||
initialNumToRender={10}
|
||||
keyExtractor={[Function]}
|
||||
maxToRenderPerBatch={10}
|
||||
onContentSizeChange={[Function]}
|
||||
onEndReachedThreshold={2}
|
||||
onLayout={[Function]}
|
||||
onMomentumScrollEnd={[Function]}
|
||||
onScroll={[Function]}
|
||||
onScrollBeginDrag={[Function]}
|
||||
onScrollEndDrag={[Function]}
|
||||
renderItem={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
windowSize={21}
|
||||
>
|
||||
<View>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
>
|
||||
<item
|
||||
title="outer1:inner0"
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
>
|
||||
<item
|
||||
title="outer1:inner1"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</RCTScrollView>
|
||||
</View>
|
||||
</View>
|
||||
</RCTScrollView>
|
||||
`;
|
||||
|
||||
exports[`VirtualizedList handles separators correctly 1`] = `
|
||||
<RCTScrollView
|
||||
ItemSeparatorComponent={[Function]}
|
||||
|
@ -31,7 +170,6 @@ exports[`VirtualizedList handles separators correctly 1`] = `
|
|||
onScrollBeginDrag={[Function]}
|
||||
onScrollEndDrag={[Function]}
|
||||
renderItem={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
|
@ -110,7 +248,6 @@ exports[`VirtualizedList handles separators correctly 2`] = `
|
|||
onScrollBeginDrag={[Function]}
|
||||
onScrollEndDrag={[Function]}
|
||||
renderItem={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
|
@ -189,7 +326,6 @@ exports[`VirtualizedList handles separators correctly 3`] = `
|
|||
onScrollBeginDrag={[Function]}
|
||||
onScrollEndDrag={[Function]}
|
||||
renderItem={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
|
@ -288,7 +424,6 @@ exports[`VirtualizedList renders all the bells and whistles 1`] = `
|
|||
}
|
||||
refreshing={false}
|
||||
renderItem={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
|
@ -367,7 +502,6 @@ exports[`VirtualizedList renders empty list 1`] = `
|
|||
onScrollBeginDrag={[Function]}
|
||||
onScrollEndDrag={[Function]}
|
||||
renderItem={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
|
@ -398,7 +532,6 @@ exports[`VirtualizedList renders empty list with empty component 1`] = `
|
|||
onScrollBeginDrag={[Function]}
|
||||
onScrollEndDrag={[Function]}
|
||||
renderItem={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
|
@ -449,7 +582,6 @@ exports[`VirtualizedList renders list with empty component 1`] = `
|
|||
onScrollBeginDrag={[Function]}
|
||||
onScrollEndDrag={[Function]}
|
||||
renderItem={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
|
@ -485,7 +617,6 @@ exports[`VirtualizedList renders null list 1`] = `
|
|||
onScrollBeginDrag={[Function]}
|
||||
onScrollEndDrag={[Function]}
|
||||
renderItem={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
|
@ -525,7 +656,6 @@ exports[`VirtualizedList renders simple list 1`] = `
|
|||
onScrollBeginDrag={[Function]}
|
||||
onScrollEndDrag={[Function]}
|
||||
renderItem={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
|
@ -581,7 +711,6 @@ exports[`VirtualizedList test getItem functionality where data is not an Array 1
|
|||
onScrollBeginDrag={[Function]}
|
||||
onScrollEndDrag={[Function]}
|
||||
renderItem={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
|
|
Loading…
Reference in New Issue