From 4388783a21063448f5de6e635fc582f048404a34 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Fri, 10 Feb 2017 09:50:11 -0800 Subject: [PATCH] Add multi column support Reviewed By: angelahess Differential Revision: D4540706 fbshipit-source-id: d8f84d13484d50692405c4a461c8d6c0e49f2cc9 --- Examples/UIExplorer/js/FlatListExample.js | 25 ++-- Examples/UIExplorer/js/ListExampleShared.js | 7 +- ...ColumnExample.js => MultiColumnExample.js} | 70 ++++----- Libraries/Experimental/FlatList.js | 133 +++++++++++++++++- 4 files changed, 183 insertions(+), 52 deletions(-) rename Examples/UIExplorer/js/{TwoColumnExample.js => MultiColumnExample.js} (72%) diff --git a/Examples/UIExplorer/js/FlatListExample.js b/Examples/UIExplorer/js/FlatListExample.js index 7917445b7..825da7f7c 100644 --- a/Examples/UIExplorer/js/FlatListExample.js +++ b/Examples/UIExplorer/js/FlatListExample.js @@ -73,16 +73,18 @@ class FlatListExample extends React.PureComponent { noSpacer={true} noScroll={true}> - - + + + + {renderSmallSwitchOption(this, 'virtualized')} {renderSmallSwitchOption(this, 'horizontal')} @@ -95,12 +97,13 @@ class FlatListExample extends React.PureComponent { FooterComponent={FooterComponent} ItemComponent={this._renderItemComponent} SeparatorComponent={SeparatorComponent} + data={filteredData} disableVirtualization={!this.state.virtualized} getItemLayout={this.state.fixedHeight ? this._getItemLayout : undefined} horizontal={this.state.horizontal} - data={filteredData} key={(this.state.horizontal ? 'h' : 'v') + (this.state.fixedHeight ? 'f' : 'd')} legacyImplementation={false} + numColumns={1} onRefresh={() => alert('onRefresh: nothing to refresh :P')} refreshing={false} onViewableItemsChanged={this._onViewableItemsChanged} diff --git a/Examples/UIExplorer/js/ListExampleShared.js b/Examples/UIExplorer/js/ListExampleShared.js index 2b8e9c2da..7a0f04c6c 100644 --- a/Examples/UIExplorer/js/ListExampleShared.js +++ b/Examples/UIExplorer/js/ListExampleShared.js @@ -182,17 +182,15 @@ function renderSmallSwitchOption(context: Object, key: string) { ); } -function PlainInput({placeholder, value, onChangeText}: Object) { +function PlainInput(props: Object) { return ( ); } @@ -229,6 +227,7 @@ const styles = StyleSheet.create({ paddingVertical: 0, height: 26, fontSize: 14, + flexGrow: 1, }, separator: { height: SEPARATOR_HEIGHT, diff --git a/Examples/UIExplorer/js/TwoColumnExample.js b/Examples/UIExplorer/js/MultiColumnExample.js similarity index 72% rename from Examples/UIExplorer/js/TwoColumnExample.js rename to Examples/UIExplorer/js/MultiColumnExample.js index 624e32e78..256271eca 100644 --- a/Examples/UIExplorer/js/TwoColumnExample.js +++ b/Examples/UIExplorer/js/MultiColumnExample.js @@ -26,6 +26,7 @@ const React = require('react'); const ReactNative = require('react-native'); const { StyleSheet, + Text, View, } = ReactNative; @@ -46,55 +47,65 @@ const { renderSmallSwitchOption, } = require('./ListExampleShared'); -class TwoColumnExample extends React.PureComponent { - static title = 'Two Columns with FlatList'; - static description = 'Performant, scrollable list of data in two columns.'; +class MultiColumnExample extends React.PureComponent { + static title = ' - MultiColumn'; + static description = 'Performant, scrollable grid of data.'; state = { data: genItemData(1000), filterText: '', fixedHeight: true, logViewable: false, + numColumns: 2, virtualized: true, }; _onChangeFilterText = (filterText) => { this.setState(() => ({filterText})); }; + _onChangeNumColumns = (numColumns) => { + this.setState(() => ({numColumns: Number(numColumns)})); + }; render() { const filterRegex = new RegExp(String(this.state.filterText), 'i'); const filter = (item) => (filterRegex.test(item.text) || filterRegex.test(item.title)); const filteredData = this.state.data.filter(filter); - const grid = []; - for (let ii = 0; ii < filteredData.length; ii += 2) { - const i1 = filteredData[ii]; - const i2 = filteredData[ii + 1]; - grid.push({columns: i2 ? [i1, i2] : [i1], key: i1.key + (i2 && i2.key)}); - } return ( - 2 Columns'} + title={this.props.navigator ? null : ' - MultiColumn'} noSpacer={true} noScroll={true}> - + + + numColumns: + + {renderSmallSwitchOption(this, 'virtualized')} {renderSmallSwitchOption(this, 'fixedHeight')} {renderSmallSwitchOption(this, 'logViewable')} + alert('onRefresh: nothing to refresh :P')} + refreshing={false} shouldItemUpdate={this._shouldItemUpdate} disableVirtualization={!this.state.virtualized} onViewableItemsChanged={this._onViewableItemsChanged} @@ -108,24 +119,19 @@ class TwoColumnExample extends React.PureComponent { } _renderItemComponent = ({item}) => { return ( - - {item.columns.map((it, ii) => ( - - ))} - + ); }; - _shouldItemUpdate(curr, next) { + _shouldItemUpdate(prev, next) { // Note that this does not check state.fixedHeight because we blow away the whole list by // changing the key anyway. - return curr.item.columns.some((cIt, idx) => cIt !== next.item.columns[idx]); + return prev.item !== next.item; } - // This is called when items change viewability by scrolling into our out of the viewable area. + // This is called when items change viewability by scrolling into or out of the viewable area. _onViewableItemsChanged = (info: { changed: Array<{ key: string, isViewable: boolean, item: {columns: Array<*>}, index: ?number, section?: any @@ -144,11 +150,11 @@ class TwoColumnExample extends React.PureComponent { const styles = StyleSheet.create({ row: { flexDirection: 'row', + alignItems: 'center', }, searchRow: { - backgroundColor: '#eeeeee', padding: 10, }, }); -module.exports = TwoColumnExample; +module.exports = MultiColumnExample; diff --git a/Libraries/Experimental/FlatList.js b/Libraries/Experimental/FlatList.js index 362e26ca4..c605d6b8b 100644 --- a/Libraries/Experimental/FlatList.js +++ b/Libraries/Experimental/FlatList.js @@ -34,8 +34,11 @@ const MetroListView = require('MetroListView'); // Used as a fallback legacy option const React = require('React'); +const View = require('View'); const VirtualizedList = require('VirtualizedList'); +const invariant = require('invariant'); + import type {Viewable} from 'ViewabilityHelper'; type Item = any; @@ -85,7 +88,12 @@ type OptionalProps = { * 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: Item, index: number) => string, + keyExtractor: (item: Item, 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, /** * Called once when the scroll position gets within onEndReachedThreshold of the rendered content. */ @@ -108,7 +116,7 @@ type OptionalProps = { /** * Optional optimization to minimize re-rendering items. */ - shouldItemUpdate?: ?( + shouldItemUpdate: ( prevProps: {item: Item, index: number}, nextProps: {item: Item, index: number} ) => boolean, @@ -135,6 +143,10 @@ type Props = RequiredProps & OptionalProps; // plus props from the underlying im * /> */ class FlatList extends React.PureComponent { + static defaultProps = { + keyExtractor: VirtualizedList.defaultProps.keyExtractor, + shouldItemUpdate: VirtualizedList.defaultProps.shouldItemUpdate, + }; props: Props; /** * Scrolls to the end of the content. May be janky without getItemLayout prop. @@ -168,11 +180,27 @@ class FlatList extends React.PureComponent { this._listRef.scrollToOffset(params); } + componentWillMount() { + this._checkProps(this.props); + } + + componentWillReceiveProps(nextProps: Props) { + this._checkProps(nextProps); + } + _hasWarnedLegacy = false; _listRef: VirtualizedList; + _captureRef = (ref) => { this._listRef = ref; }; - render() { - if (this.props.legacyImplementation) { + + _checkProps(props: Props) { + const {getItem, getItemCount, horizontal, legacyImplementation, numColumns, } = props; + invariant(!getItem && !getItemCount, 'FlatList does not support custom data formats.'); + if (numColumns > 1) { + invariant(!horizontal, 'numColumns does not support horizontal.'); + } + if (legacyImplementation) { + invariant(!(numColumns > 1), 'Legacy list does not support multiple columns.'); // Warning: may not have full feature parity and is meant more for debugging and performance // comparison. if (!this._hasWarnedLegacy) { @@ -182,9 +210,104 @@ class FlatList extends React.PureComponent { ); this._hasWarnedLegacy = true; } + } + } + + _getItem = (data: Array, index: number): Item | Array => { + const {numColumns} = this.props; + if (numColumns > 1) { + const ret = []; + for (let kk = 0; kk < numColumns; kk++) { + const item = data[index * numColumns + kk]; + item && ret.push(item); + } + return ret; + } else { + return data[index]; + } + }; + + _getItemCount = (data: Array): number => { + return Math.floor(data.length / (this.props.numColumns || 1)); + }; + + _keyExtractor = (items: Item | Array, index: number): string => { + const {keyExtractor, numColumns} = this.props; + if (numColumns > 1) { + return items.map((it, kk) => keyExtractor(it, index * numColumns + kk)).join(':'); + } else { + return keyExtractor(items, index); + } + }; + + _pushMultiColumnViewable(arr: Array, v: Viewable): 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}); + }); + } + _onViewableItemsChanged = (info) => { + const {numColumns, onViewableItemsChanged} = this.props; + if (!onViewableItemsChanged) { + return; + } + 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); + } + }; + + _renderItem = ({item, index}) => { + const {ItemComponent, numColumns} = this.props; + if (numColumns > 1) { + return ( + + {item.map((it, kk) => + ) + } + + ); + } else { + return ; + } + }; + + _shouldItemUpdate = (prev, next) => { + const {numColumns, shouldItemUpdate} = this.props; + if (numColumns > 1) { + return prev.item.length !== next.item.length || + prev.item.some((prevItem, ii) => shouldItemUpdate( + {item: prevItem, index: prev.index + ii}, + {item: next.item[ii], index: next.index + ii}, + )); + } else { + return shouldItemUpdate(prev, next); + } + }; + + render() { + if (this.props.legacyImplementation) { return ; } else { - return ; + return ( + + ); } } }