diff --git a/Examples/UIExplorer/js/FlatListExample.js b/Examples/UIExplorer/js/FlatListExample.js index 83e754a56..527fb7ed9 100644 --- a/Examples/UIExplorer/js/FlatListExample.js +++ b/Examples/UIExplorer/js/FlatListExample.js @@ -40,6 +40,7 @@ const { FooterComponent, HeaderComponent, ItemComponent, + ItemSeparatorComponent, PlainInput, SeparatorComponent, Spindicator, @@ -103,54 +104,59 @@ class FlatListExample extends React.PureComponent { - - - - - - - {renderSmallSwitchOption(this, 'virtualized')} - {renderSmallSwitchOption(this, 'horizontal')} - {renderSmallSwitchOption(this, 'fixedHeight')} - {renderSmallSwitchOption(this, 'logViewable')} - {renderSmallSwitchOption(this, 'debug')} - + + + + + + + + {renderSmallSwitchOption(this, 'virtualized')} + {renderSmallSwitchOption(this, 'horizontal')} + {renderSmallSwitchOption(this, 'fixedHeight')} + {renderSmallSwitchOption(this, 'logViewable')} + {renderSmallSwitchOption(this, 'debug')} + + + + } + ListFooterComponent={FooterComponent} + data={filteredData} + debug={this.state.debug} + disableVirtualization={!this.state.virtualized} + getItemLayout={this.state.fixedHeight ? + this._getItemLayout : + undefined + } + horizontal={this.state.horizontal} + key={(this.state.horizontal ? 'h' : 'v') + + (this.state.fixedHeight ? 'f' : 'd') + } + keyboardShouldPersistTaps="always" + keyboardDismissMode="on-drag" + legacyImplementation={false} + numColumns={1} + onEndReached={this._onEndReached} + onRefresh={this._onRefresh} + onScroll={this.state.horizontal ? this._scrollSinkX : this._scrollSinkY} + onViewableItemsChanged={this._onViewableItemsChanged} + ref={this._captureRef} + refreshing={false} + renderItem={this._renderItemComponent} + contentContainerStyle={styles.list} + viewabilityConfig={VIEWABILITY_CONFIG} + /> - - } - ListFooterComponent={FooterComponent} - data={filteredData} - debug={this.state.debug} - disableVirtualization={!this.state.virtualized} - getItemLayout={this.state.fixedHeight ? - this._getItemLayout : - undefined - } - horizontal={this.state.horizontal} - key={(this.state.horizontal ? 'h' : 'v') + - (this.state.fixedHeight ? 'f' : 'd') - } - legacyImplementation={false} - numColumns={1} - onEndReached={this._onEndReached} - onRefresh={this._onRefresh} - onScroll={this.state.horizontal ? this._scrollSinkX : this._scrollSinkY} - onViewableItemsChanged={this._onViewableItemsChanged} - ref={this._captureRef} - refreshing={false} - renderItem={this._renderItemComponent} - viewabilityConfig={VIEWABILITY_CONFIG} - /> ); } @@ -159,18 +165,23 @@ class FlatListExample extends React.PureComponent { return getItemLayout(data, index, this.state.horizontal); }; _onEndReached = () => { + if (this.state.data.length >= 1000) { + return; + } this.setState((state) => ({ data: state.data.concat(genItemData(100, state.data.length)), })); }; _onRefresh = () => alert('onRefresh: nothing to refresh :P'); - _renderItemComponent = ({item}) => { + _renderItemComponent = ({item, separators}) => { return ( ); }; @@ -203,6 +214,13 @@ class FlatListExample extends React.PureComponent { const styles = StyleSheet.create({ + container: { + backgroundColor: 'rgb(239, 239, 244)', + flex: 1, + }, + list: { + backgroundColor: 'white', + }, options: { flexDirection: 'row', flexWrap: 'wrap', diff --git a/Examples/UIExplorer/js/ListExampleShared.js b/Examples/UIExplorer/js/ListExampleShared.js index 6eae3fcb8..238386e41 100644 --- a/Examples/UIExplorer/js/ListExampleShared.js +++ b/Examples/UIExplorer/js/ListExampleShared.js @@ -54,6 +54,7 @@ function genItemData(count: number, start: number = 0): Array { } const HORIZ_WIDTH = 200; +const ITEM_HEIGHT = 72; class ItemComponent extends React.PureComponent { props: { @@ -61,6 +62,8 @@ class ItemComponent extends React.PureComponent { horizontal?: ?boolean, item: Item, onPress: (key: number) => void, + onShowUnderlay?: () => void, + onHideUnderlay?: () => void, }; _onPress = () => { this.props.onPress(this.props.item.key); @@ -72,9 +75,11 @@ class ItemComponent extends React.PureComponent { return ( + styles.row, horizontal && {width: HORIZ_WIDTH}, fixedHeight && {height: ITEM_HEIGHT}]}> {!item.noImage && } { class FooterComponent extends React.PureComponent { render() { return ( - + LIST FOOTER @@ -114,7 +119,7 @@ class FooterComponent extends React.PureComponent { class HeaderComponent extends React.PureComponent { render() { return ( - + LIST HEADER @@ -130,6 +135,15 @@ class SeparatorComponent extends React.PureComponent { } } +class ItemSeparatorComponent extends React.PureComponent { + render() { + const style = this.props.highlighted + ? [styles.itemSeparator, {marginLeft: 0, backgroundColor: 'rgb(217, 217, 217)'}] + : styles.itemSeparator; + return ; + } +} + class Spindicator extends React.PureComponent { render() { return ( @@ -181,7 +195,7 @@ const SEPARATOR_HEIGHT = StyleSheet.hairlineWidth; function getItemLayout(data: any, index: number, horizontal?: boolean) { const [length, separator, header] = horizontal ? - [HORIZ_WIDTH, 0, HEADER.width] : [84, SEPARATOR_HEIGHT, HEADER.height]; + [HORIZ_WIDTH, 0, HEADER.width] : [ITEM_HEIGHT, SEPARATOR_HEIGHT, HEADER.height]; return {length, offset: (length + separator) * index + header, index}; } @@ -231,12 +245,20 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, + headerFooterContainer: { + backgroundColor: 'rgb(239, 239, 244)', + }, horizItem: { alignSelf: 'flex-start', // Necessary for touch highlight }, item: { flex: 1, }, + itemSeparator: { + height: SEPARATOR_HEIGHT, + backgroundColor: 'rgb(200, 199, 204)', + marginLeft: 60, + }, option: { flexDirection: 'row', padding: 8, @@ -245,7 +267,7 @@ const styles = StyleSheet.create({ row: { flexDirection: 'row', padding: 10, - backgroundColor: '#F6F6F6', + backgroundColor: 'white', }, searchTextInput: { backgroundColor: 'white', @@ -260,7 +282,7 @@ const styles = StyleSheet.create({ }, separator: { height: SEPARATOR_HEIGHT, - backgroundColor: 'gray', + backgroundColor: 'rgb(200, 199, 204)', }, smallSwitch: Platform.select({ android: { @@ -276,12 +298,13 @@ const styles = StyleSheet.create({ }), stacked: { alignItems: 'center', - backgroundColor: '#F6F6F6', + backgroundColor: 'white', padding: 10, }, thumb: { - width: 64, - height: 64, + width: 50, + height: 50, + left: -5, }, spindicator: { marginLeft: 'auto', @@ -303,6 +326,7 @@ module.exports = { FooterComponent, HeaderComponent, ItemComponent, + ItemSeparatorComponent, PlainInput, SeparatorComponent, Spindicator, diff --git a/Examples/UIExplorer/js/MultiColumnExample.js b/Examples/UIExplorer/js/MultiColumnExample.js index 3427c8aeb..5dfd192de 100644 --- a/Examples/UIExplorer/js/MultiColumnExample.js +++ b/Examples/UIExplorer/js/MultiColumnExample.js @@ -97,7 +97,6 @@ class MultiColumnExample extends React.PureComponent { { return ( - + + + ); }; // This is called when items change viewability by scrolling into or out of the viewable area. @@ -142,7 +144,18 @@ class MultiColumnExample extends React.PureComponent { }; } +const CARD_MARGIN = 4; +const BORDER_WIDTH = 1; + const styles = StyleSheet.create({ + card: { + margin: CARD_MARGIN, + borderRadius: 10, + flex: 1, + overflow: 'hidden', + borderColor: 'lightgray', + borderWidth: BORDER_WIDTH, + }, row: { flexDirection: 'row', alignItems: 'center', diff --git a/Libraries/Lists/FlatList.js b/Libraries/Lists/FlatList.js index 17ccf2ce8..af8511e66 100644 --- a/Libraries/Lists/FlatList.js +++ b/Libraries/Lists/FlatList.js @@ -24,19 +24,40 @@ import type {Props as VirtualizedListProps} from 'VirtualizedList'; type RequiredProps = { /** - * Takes an item from `data` and renders it into the list. Typical usage: + * Takes an item from `data` and renders it into the list. Example usage: * - * _renderItem = ({item}) => ( - * this._onPress(item)}> - * {item.title}} - * - * ); - * ... - * + * ( + * + * )} + * data={[{title: 'Title Text', key: 'item1'}]} + * renderItem={({item, separators}) => ( + * this._onPress(item)} + * onShowUnderlay={separators.highlight} + * onHideUnderlay={separators.unhighlight}> + * + * {item.title}} + * + * + * )} + * /> * - * Provides additional metadata like `index` if you need it. + * 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: (info: {item: ItemT, index: number}) => ?React.Element, + renderItem: (info: { + item: ItemT, + index: number, + separators: { + highlight: () => void, + unhighlight: () => void, + updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, + }, + }) => ?React.Element, /** * For simplicity, data is just a plain array. If you want to use something else, like an * immutable list, use the underlying `VirtualizedList` directly. @@ -45,7 +66,10 @@ type RequiredProps = { }; type OptionalProps = { /** - * Rendered in between each item, but not at the top or bottom. + * Rendered in between each item, but not at the top or bottom. By default, `highlighted` and + * `leadingItem` props are provided. `renderItem` provides `separators.highlight`/`unhighlight` + * which will update the `highlighted` prop, but you can also add custom props with + * `separators.updateProps`. */ ItemSeparatorComponent?: ?ReactClass, /** @@ -424,7 +448,7 @@ class FlatList extends React.PureComponent, vo } }; - _renderItem = (info: {item: ItemT | Array, index: number}) => { + _renderItem = (info: Object) => { const {renderItem, numColumns, columnWrapperStyle} = this.props; if (numColumns > 1) { const {item, index} = info; @@ -432,7 +456,11 @@ class FlatList extends React.PureComponent, vo return ( {item.map((it, kk) => { - const element = renderItem({item: it, index: index * numColumns + kk}); + const element = renderItem({ + item: it, + index: index * numColumns + kk, + separators: info.separators, + }); return element && React.cloneElement(element, {key: kk}); })} diff --git a/Libraries/Lists/MetroListView.js b/Libraries/Lists/MetroListView.js index eae4a6f32..125c60584 100644 --- a/Libraries/Lists/MetroListView.js +++ b/Libraries/Lists/MetroListView.js @@ -22,7 +22,7 @@ type Item = any; type NormalProps = { FooterComponent?: ReactClass<*>, - renderItem: ({item: Item, index: number}) => ?React.Element<*>, + renderItem: (info: Object) => ?React.Element<*>, renderSectionHeader?: ({section: Object}) => ?React.Element<*>, SeparatorComponent?: ?ReactClass<*>, // not supported yet diff --git a/Libraries/Lists/VirtualizedList.js b/Libraries/Lists/VirtualizedList.js index 2fa3fd3c0..b03d8198a 100644 --- a/Libraries/Lists/VirtualizedList.js +++ b/Libraries/Lists/VirtualizedList.js @@ -29,7 +29,15 @@ import type {ViewabilityConfig, ViewToken} from 'ViewabilityHelper'; type Item = any; -type renderItemType = (info: {item: Item, index: number}) => ?React.Element; +type renderItemType = (info: { + item: Item, + index: number, + separators: { + highlight: () => void, + unhighlight: () => void, + updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, + }, +}) => ?React.Element; type RequiredProps = { renderItem: renderItemType, @@ -290,13 +298,10 @@ class VirtualizedList extends React.PureComponent { windowSize: 21, // multiples of length }; - state: State = { - first: 0, - last: this.props.initialNumToRender, - }; + state: State; - constructor(props: Props) { - super(props); + constructor(props: Props, context: Object) { + super(props, context); invariant( !props.onScroll || !props.onScroll.__isNative, 'Components based on VirtualizedList must be wrapped with Animated.createAnimatedComponent ' + @@ -345,6 +350,7 @@ class VirtualizedList extends React.PureComponent { const {ItemSeparatorComponent, data, getItem, getItemCount, keyExtractor} = this.props; const stickyOffset = this.props.ListHeaderComponent ? 1 : 0; const end = getItemCount(data) - 1; + let prevCellKey; last = Math.min(end, last); for (let ii = first; ii <= last; ii++) { const item = getItem(data, ii); @@ -356,20 +362,29 @@ class VirtualizedList extends React.PureComponent { cells.push( this._onCellLayout(e, key, ii)} onUnmount={this._onCellUnmount} parentProps={this.props} + ref={(ref) => {this._cellRefs[key] = ref;}} /> ); - if (ItemSeparatorComponent && ii < end) { - cells.push(); - } + prevCellKey = key; } } + _onUpdateSeparators = (keys: Array, newProps: Object) => { + keys.forEach((key) => { + const ref = key != null && this._cellRefs[key]; + ref && ref.updateSeparatorProps(newProps); + }); + }; + render() { const {ListFooterComponent, ListHeaderComponent} = this.props; const {data, disableVirtualization, horizontal} = this.props; @@ -487,6 +502,7 @@ class VirtualizedList extends React.PureComponent { } _averageCellLength = 0; + _cellRefs = {}; _hasDataChangedSinceEndReached = true; _hasWarned = {}; _highestMeasuredFrameIndex = 0; @@ -776,34 +792,70 @@ class VirtualizedList extends React.PureComponent { class CellRenderer extends React.Component { props: { + ItemSeparatorComponent: ?ReactClass<*>, cellKey: string, index: number, item: Item, onLayout: (event: Object) => void, // This is extracted by ScrollViewStickyHeader onUnmount: (cellKey: string) => void, + onUpdateSeparators: (cellKeys: Array, props: Object) => void, parentProps: { - renderItem: renderItemType, getItemLayout?: ?Function, + renderItem: renderItemType, + }, + prevCellKey: ?string, + }; + + state = { + separatorProps: { + highlighted: false, + leadingItem: this.props.item, }, }; + + // TODO: consider factoring separator stuff out of VirtualizedList into FlatList since it's not + // reused by SectionList and we can keep VirtualizedList simpler. + _separators = { + highlight: () => { + const {cellKey, prevCellKey} = this.props; + this.props.onUpdateSeparators([cellKey, prevCellKey], {highlighted: true}); + }, + unhighlight: () => { + const {cellKey, prevCellKey} = this.props; + this.props.onUpdateSeparators([cellKey, prevCellKey], {highlighted: false}); + }, + updateProps: (select: 'leading' | 'trailing', newProps: Object) => { + const {cellKey, prevCellKey} = this.props; + this.props.onUpdateSeparators([select === 'leading' ? cellKey : prevCellKey], newProps); + }, + }; + + updateSeparatorProps(newProps: Object) { + this.setState(state => ({separatorProps: {...state.separatorProps, ...newProps}})); + } + componentWillUnmount() { this.props.onUnmount(this.props.cellKey); } + render() { - const {item, index, parentProps} = this.props; + const {ItemSeparatorComponent, item, index, parentProps} = this.props; const {renderItem, getItemLayout} = parentProps; invariant(renderItem, 'no renderItem!'); - const element = renderItem({item, index}); - if (getItemLayout && - !parentProps.debug && - !FillRateHelper.enabled()) { - return element; - } + const element = renderItem({ + item, + index, + separators: this._separators, + }); + const onLayout = (getItemLayout && !parentProps.debug && !FillRateHelper.enabled()) + ? undefined + : this.props.onLayout; // NOTE: that when this is a sticky header, `onLayout` will get automatically extracted and // called explicitly by `ScrollViewStickyHeader`. return ( - + {element} + {ItemSeparatorComponent && } ); } diff --git a/Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap b/Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap index d96352abd..db1a896ae 100644 --- a/Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap +++ b/Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap @@ -63,54 +63,66 @@ exports[`FlatList renders all the bells and whistles 1`] = `
- - + + + + + - - - + + + + + - - + + +