From 63f7efcd32dcfc5ff0bb742aae8d4fc35665cbbd Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Mon, 12 Jun 2017 22:32:56 -0700 Subject: [PATCH] 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 --- Libraries/Lists/VirtualizedList.js | 121 +++++++++----- .../Lists/__tests__/VirtualizedList-test.js | 22 +++ .../__snapshots__/FlatList-test.js.snap | 4 - .../__snapshots__/SectionList-test.js.snap | 5 - .../VirtualizedList-test.js.snap | 149 ++++++++++++++++-- 5 files changed, 243 insertions(+), 58 deletions(-) diff --git a/Libraries/Lists/VirtualizedList.js b/Libraries/Lists/VirtualizedList.js index b529e6f3b..312419385 100644 --- a/Libraries/Lists/VirtualizedList.js +++ b/Libraries/Lists/VirtualizedList.js @@ -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, + renderScrollComponent?: (props: Object) => React.Element, /** * 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 { }, 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 ( - - } - /> - ); - } else { - return ; - } - }, 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 { '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 { }); }; + _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 { } 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 { 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 { ); 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 { ); } + const scrollProps = { + ...this.props, + onContentSizeChange: this._onContentSizeChange, + onLayout: this._onLayout, + onScroll: this._onScroll, + onScrollBeginDrag: this._onScrollBeginDrag, + onScrollEndDrag: this._onScrollEndDrag, + onMomentumScrollEnd: this._onMomentumScrollEnd, + scrollEventThrottle: this.props.scrollEventThrottle, // TODO: Android support + stickyHeaderIndices, + }; const ret = React.cloneElement( - this.props.renderScrollComponent(this.props), + (this.props.renderScrollComponent || this._defaultRenderScrollComponent)(scrollProps), { - 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, }, cells, ); @@ -601,6 +617,32 @@ class VirtualizedList extends React.PureComponent { ); } + _defaultRenderScrollComponent = (props) => { + if (this._isNestedWithSameOrientation()) { + return ; + } 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 ( + + } + /> + ); + } else { + return ; + } + }; + _onCellLayout(e, cellKey, index) { const layout = e.nativeEvent.layout; const next = { @@ -816,14 +858,15 @@ class VirtualizedList extends React.PureComponent { }; _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, ); diff --git a/Libraries/Lists/__tests__/VirtualizedList-test.js b/Libraries/Lists/__tests__/VirtualizedList-test.js index c2f4b5a6c..2a6cf69b4 100644 --- a/Libraries/Lists/__tests__/VirtualizedList-test.js +++ b/Libraries/Lists/__tests__/VirtualizedList-test.js @@ -135,4 +135,26 @@ describe('VirtualizedList', () => { expect(component).toMatchSnapshot(); infos[1].separators.unhighlight(); }); + + it('handles nested lists', () => { + const component = ReactTestRenderer.create( + ( + { + return ; + }} + getItem={(data, index) => data[index]} + getItemCount={(data) => data.length} + /> + )} + getItem={(data, index) => data[index]} + getItemCount={(data) => data.length} + /> + ); + expect(component).toMatchSnapshot(); + }); }); diff --git a/Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap b/Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap index 7c41e3258..a8ed472ce 100644 --- a/Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap +++ b/Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap @@ -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} diff --git a/Libraries/Lists/__tests__/__snapshots__/SectionList-test.js.snap b/Libraries/Lists/__tests__/__snapshots__/SectionList-test.js.snap index 90077e007..fafd42c26 100644 --- a/Libraries/Lists/__tests__/__snapshots__/SectionList-test.js.snap +++ b/Libraries/Lists/__tests__/__snapshots__/SectionList-test.js.snap @@ -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 []} diff --git a/Libraries/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap b/Libraries/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap index be39cd2e3..e62fe97c9 100644 --- a/Libraries/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap +++ b/Libraries/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap @@ -1,5 +1,144 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`VirtualizedList handles nested lists 1`] = ` + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + exports[`VirtualizedList handles separators correctly 1`] = `