Skip to content

Commit

Permalink
Add support for updating adjacent separators on row highlight to Flat…
Browse files Browse the repository at this point in the history
…List

Summary:
A nice bit of polish for apps is to update the separators between list items
when press actives the highlight (things get especially noticeable/ugly when
the separators are not full width and don't adjust). This can be difficult to
coordinate and update efficiently, so we bake the functionality into
`VirtualizedList`.

The approach taken here is a little different from `ListView`. We pass a new
`separators` object with `renderItem` that has easy-to-use callbacks for toggling
the 'highlighted' prop on both adjacent separators - they can be wired up
directly to the `onShow/HideUnderlay` props of `TouchableHighlight` (pit of
success and all that - we want those RN apps to be polished!), but we also
provide a more generic `separators.updateProps` method to set any custom
props. This also passes in `leadingItem` so more custom wiring can be done on
initial render (e.g. linking the highlight state with `Animated`).

This also moves the separator rendering into the `CellRenderer`. I think this might
also fix some subtle measurement bugs with the `onLayout` not capturing the
height of the separator, so that's nice too, but the main reason is to have
an easy place to store the state so we don't have to re-render the entire list
like `ListView` does. Instead we track references to the cells and call update
only on the two we care about so the feedback is instantaneous even with big,
heavy lists.

This diff also messes with a bunch of flow and updates the example to be more
like a standard list.

`SectionList` support is coming in a stacked diff.

**TestPlan**

Video demo:

https://youtu.be/uFE7qGA0mg4

Pretty sure this is backwards compatible....

Reviewed By: thechefchen

Differential Revision: D4833557

fbshipit-source-id: 685af46243ba13246bf280dae8a4223381ce8cea
  • Loading branch information
sahrens authored and facebook-github-bot committed Apr 13, 2017
1 parent 926bfdb commit f25df50
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 137 deletions.
112 changes: 65 additions & 47 deletions Examples/UIExplorer/js/FlatListExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const {
FooterComponent,
HeaderComponent,
ItemComponent,
ItemSeparatorComponent,
PlainInput,
SeparatorComponent,
Spindicator,
Expand Down Expand Up @@ -103,54 +104,59 @@ class FlatListExample extends React.PureComponent {
<UIExplorerPage
noSpacer={true}
noScroll={true}>
<View style={styles.searchRow}>
<View style={styles.options}>
<PlainInput
onChangeText={this._onChangeFilterText}
placeholder="Search..."
value={this.state.filterText}
/>
<PlainInput
onChangeText={this._onChangeScrollToIndex}
placeholder="scrollToIndex..."
/>
</View>
<View style={styles.options}>
{renderSmallSwitchOption(this, 'virtualized')}
{renderSmallSwitchOption(this, 'horizontal')}
{renderSmallSwitchOption(this, 'fixedHeight')}
{renderSmallSwitchOption(this, 'logViewable')}
{renderSmallSwitchOption(this, 'debug')}
<Spindicator value={this._scrollPos} />
<View style={styles.container}>
<View style={styles.searchRow}>
<View style={styles.options}>
<PlainInput
onChangeText={this._onChangeFilterText}
placeholder="Search..."
value={this.state.filterText}
/>
<PlainInput
onChangeText={this._onChangeScrollToIndex}
placeholder="scrollToIndex..."
/>
</View>
<View style={styles.options}>
{renderSmallSwitchOption(this, 'virtualized')}
{renderSmallSwitchOption(this, 'horizontal')}
{renderSmallSwitchOption(this, 'fixedHeight')}
{renderSmallSwitchOption(this, 'logViewable')}
{renderSmallSwitchOption(this, 'debug')}
<Spindicator value={this._scrollPos} />
</View>
</View>
<SeparatorComponent />
<AnimatedFlatList
ItemSeparatorComponent={ItemSeparatorComponent}
ListHeaderComponent={<HeaderComponent />}
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}
/>
</View>
<SeparatorComponent />
<AnimatedFlatList
ItemSeparatorComponent={SeparatorComponent}
ListHeaderComponent={<HeaderComponent />}
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}
/>
</UIExplorerPage>
);
}
Expand All @@ -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 (
<ItemComponent
item={item}
horizontal={this.state.horizontal}
fixedHeight={this.state.fixedHeight}
onPress={this._pressItem}
onShowUnderlay={separators.highlight}
onHideUnderlay={separators.unhighlight}
/>
);
};
Expand Down Expand Up @@ -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',
Expand Down
42 changes: 33 additions & 9 deletions Examples/UIExplorer/js/ListExampleShared.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,16 @@ function genItemData(count: number, start: number = 0): Array<Item> {
}

const HORIZ_WIDTH = 200;
const ITEM_HEIGHT = 72;

class ItemComponent extends React.PureComponent {
props: {
fixedHeight?: ?boolean,
horizontal?: ?boolean,
item: Item,
onPress: (key: number) => void,
onShowUnderlay?: () => void,
onHideUnderlay?: () => void,
};
_onPress = () => {
this.props.onPress(this.props.item.key);
Expand All @@ -72,9 +75,11 @@ class ItemComponent extends React.PureComponent {
return (
<TouchableHighlight
onPress={this._onPress}
onShowUnderlay={this.props.onShowUnderlay}
onHideUnderlay={this.props.onHideUnderlay}
style={horizontal ? styles.horizItem : styles.item}>
<View style={[
styles.row, horizontal && {width: HORIZ_WIDTH}]}>
styles.row, horizontal && {width: HORIZ_WIDTH}, fixedHeight && {height: ITEM_HEIGHT}]}>
{!item.noImage && <Image style={styles.thumb} source={imgSource} />}
<Text
style={styles.text}
Expand All @@ -101,7 +106,7 @@ const renderStackedItem = ({item}: {item: Item}) => {
class FooterComponent extends React.PureComponent {
render() {
return (
<View>
<View style={styles.headerFooterContainer}>
<SeparatorComponent />
<View style={styles.headerFooter}>
<Text>LIST FOOTER</Text>
Expand All @@ -114,7 +119,7 @@ class FooterComponent extends React.PureComponent {
class HeaderComponent extends React.PureComponent {
render() {
return (
<View>
<View style={styles.headerFooterContainer}>
<View style={styles.headerFooter}>
<Text>LIST HEADER</Text>
</View>
Expand All @@ -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 <View style={style} />;
}
}

class Spindicator extends React.PureComponent {
render() {
return (
Expand Down Expand Up @@ -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};
}

Expand Down Expand Up @@ -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,
Expand All @@ -245,7 +267,7 @@ const styles = StyleSheet.create({
row: {
flexDirection: 'row',
padding: 10,
backgroundColor: '#F6F6F6',
backgroundColor: 'white',
},
searchTextInput: {
backgroundColor: 'white',
Expand All @@ -260,7 +282,7 @@ const styles = StyleSheet.create({
},
separator: {
height: SEPARATOR_HEIGHT,
backgroundColor: 'gray',
backgroundColor: 'rgb(200, 199, 204)',
},
smallSwitch: Platform.select({
android: {
Expand All @@ -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',
Expand All @@ -303,6 +326,7 @@ module.exports = {
FooterComponent,
HeaderComponent,
ItemComponent,
ItemSeparatorComponent,
PlainInput,
SeparatorComponent,
Spindicator,
Expand Down
27 changes: 20 additions & 7 deletions Examples/UIExplorer/js/MultiColumnExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ class MultiColumnExample extends React.PureComponent {
</View>
<SeparatorComponent />
<FlatList
ItemSeparatorComponent={SeparatorComponent}
ListFooterComponent={FooterComponent}
ListHeaderComponent={HeaderComponent}
getItemLayout={this.state.fixedHeight ? this._getItemLayout : undefined}
Expand All @@ -115,15 +114,18 @@ class MultiColumnExample extends React.PureComponent {
);
}
_getItemLayout(data: any, index: number): {length: number, offset: number, index: number} {
return getItemLayout(data, index);
const length = getItemLayout(data, index).length + 2 * (CARD_MARGIN + BORDER_WIDTH);
return {length, offset: length * index, index};
}
_renderItemComponent = ({item}) => {
return (
<ItemComponent
item={item}
fixedHeight={this.state.fixedHeight}
onPress={this._pressItem}
/>
<View style={styles.card}>
<ItemComponent
item={item}
fixedHeight={this.state.fixedHeight}
onPress={this._pressItem}
/>
</View>
);
};
// This is called when items change viewability by scrolling into or out of the viewable area.
Expand All @@ -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',
Expand Down
Loading

0 comments on commit f25df50

Please sign in to comment.