Add multi column support

Reviewed By: angelahess

Differential Revision: D4540706

fbshipit-source-id: d8f84d13484d50692405c4a461c8d6c0e49f2cc9
This commit is contained in:
Spencer Ahrens 2017-02-10 09:50:11 -08:00 committed by Facebook Github Bot
parent a97f665629
commit 4388783a21
4 changed files with 183 additions and 52 deletions

View File

@ -73,16 +73,18 @@ class FlatListExample extends React.PureComponent {
noSpacer={true}
noScroll={true}>
<View style={styles.searchRow}>
<PlainInput
onChangeText={this._onChangeFilterText}
placeholder="Search..."
value={this.state.filterText}
/>
<PlainInput
onChangeText={this._onChangeScrollToIndex}
placeholder="scrollToIndex..."
style={styles.searchTextInput}
/>
<View style={styles.options}>
<PlainInput
onChangeText={this._onChangeFilterText}
placeholder="Search..."
value={this.state.filterText}
/>
<PlainInput
onChangeText={this._onChangeScrollToIndex}
placeholder="scrollToIndex..."
style={styles.searchTextInput}
/>
</View>
<View style={styles.options}>
{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}

View File

@ -182,17 +182,15 @@ function renderSmallSwitchOption(context: Object, key: string) {
);
}
function PlainInput({placeholder, value, onChangeText}: Object) {
function PlainInput(props: Object) {
return (
<TextInput
autoCapitalize="none"
autoCorrect={false}
clearButtonMode="always"
onChangeText={onChangeText}
placeholder={placeholder}
underlineColorAndroid="transparent"
style={styles.searchTextInput}
value={value}
{...props}
/>
);
}
@ -229,6 +227,7 @@ const styles = StyleSheet.create({
paddingVertical: 0,
height: 26,
fontSize: 14,
flexGrow: 1,
},
separator: {
height: SEPARATOR_HEIGHT,

View File

@ -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 = '<FlatList> - 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 (
<UIExplorerPage
title={this.props.navigator ? null : '<FlatList> - 2 Columns'}
title={this.props.navigator ? null : '<FlatList> - MultiColumn'}
noSpacer={true}
noScroll={true}>
<View style={styles.searchRow}>
<PlainInput
onChangeText={this._onChangeFilterText}
placeholder="Search..."
value={this.state.filterText}
/>
<View style={styles.row}>
<PlainInput
onChangeText={this._onChangeFilterText}
placeholder="Search..."
value={this.state.filterText}
/>
<Text> numColumns: </Text>
<PlainInput
clearButtonMode="never"
onChangeText={this._onChangeNumColumns}
value={this.state.numColumns ? String(this.state.numColumns) : ''}
/>
</View>
<View style={styles.row}>
{renderSmallSwitchOption(this, 'virtualized')}
{renderSmallSwitchOption(this, 'fixedHeight')}
{renderSmallSwitchOption(this, 'logViewable')}
</View>
</View>
<SeparatorComponent />
<FlatList
FooterComponent={FooterComponent}
HeaderComponent={HeaderComponent}
ItemComponent={this._renderItemComponent}
SeparatorComponent={SeparatorComponent}
getItemLayout={this.state.fixedHeight ? this._getItemLayout : undefined}
data={grid}
key={this.state.fixedHeight ? 'f' : 'v'}
data={filteredData}
key={this.state.numColumns + (this.state.fixedHeight ? 'f' : 'v')}
numColumns={this.state.numColumns || 1}
onRefresh={() => 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 (
<View style={styles.row}>
{item.columns.map((it, ii) => (
<ItemComponent
key={ii}
item={it}
fixedHeight={this.state.fixedHeight}
onPress={this._pressItem}
/>
))}
</View>
<ItemComponent
item={item}
fixedHeight={this.state.fixedHeight}
onPress={this._pressItem}
/>
);
};
_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;

View File

@ -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<Item>, index: number): Item | Array<Item> => {
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<Item>): number => {
return Math.floor(data.length / (this.props.numColumns || 1));
};
_keyExtractor = (items: Item | Array<Item>, 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<Viewable>, 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 (
<View style={{flexDirection: 'row'}}>
{item.map((it, kk) =>
<ItemComponent key={kk} item={it} index={index * numColumns + kk} />)
}
</View>
);
} else {
return <ItemComponent item={item} index={index} />;
}
};
_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 <MetroListView {...this.props} items={this.props.data} ref={this._captureRef} />;
} else {
return <VirtualizedList {...this.props} ref={this._captureRef} />;
return (
<VirtualizedList
{...this.props}
ItemComponent={this._renderItem}
getItem={this._getItem}
getItemCount={this._getItemCount}
keyExtractor={this._keyExtractor}
ref={this._captureRef}
shouldItemUpdate={this._shouldItemUpdate}
onViewableItemsChanged={this.props.onViewableItemsChanged && this._onViewableItemsChanged}
/>
);
}
}
}