Permalink
Browse files

Add SwipeableFlatList

Reviewed By: sahrens

Differential Revision: D5912488

fbshipit-source-id: 3d2872a7712c00badcbd8341a7d058df14a9091a
  • Loading branch information...
tomasreimers authored and facebook-github-bot committed Sep 29, 2017
1 parent b64e6c7 commit d8cc6e3c2b8c2dc25b6b32691779790c15de8d10
@@ -0,0 +1,189 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule SwipeableFlatList
* @flow
* @format
*/
'use strict';
import type {Props as FlatListProps} from 'FlatList';
import type {renderItemType} from 'VirtualizedList';
const PropTypes = require('prop-types');
const React = require('React');
const SwipeableRow = require('SwipeableRow');
const FlatList = require('FlatList');
type SwipableListProps = {
/**
* To alert the user that swiping is possible, the first row can bounce
* on component mount.
*/
bounceFirstRowOnMount: boolean,
// Maximum distance to open to after a swipe
maxSwipeDistance: number | (Object => number),
// Callback method to render the view that will be unveiled on swipe
renderQuickActions: renderItemType,
};
type Props<ItemT> = SwipableListProps & FlatListProps<ItemT>;
type State = {
openRowKey: ?string,
};
/**
* A container component that renders multiple SwipeableRow's in a FlatList
* implementation. This is designed to be a drop-in replacement for the
* standard React Native `FlatList`, so use it as if it were a FlatList, but
* with extra props, i.e.
*
* <SwipeableListView renderRow={..} renderQuickActions={..} {..FlatList props} />
*
* SwipeableRow can be used independently of this component, but the main
* benefit of using this component is
*
* - It ensures that at most 1 row is swiped open (auto closes others)
* - It can bounce the 1st row of the list so users know it's swipeable
* - Increase performance on iOS by locking list swiping when row swiping is occuring
* - More to come
*/
class SwipeableFlatList<ItemT> extends React.Component<Props<ItemT>, State> {
props: Props<ItemT>;
state: State;
_flatListRef: ?FlatList<ItemT> = null;
_shouldBounceFirstRowOnMount: boolean = false;
static propTypes = {
...FlatList.propTypes,
/**
* To alert the user that swiping is possible, the first row can bounce
* on component mount.
*/
bounceFirstRowOnMount: PropTypes.bool.isRequired,
// Maximum distance to open to after a swipe
maxSwipeDistance: PropTypes.oneOfType([PropTypes.number, PropTypes.func])
.isRequired,
// Callback method to render the view that will be unveiled on swipe
renderQuickActions: PropTypes.func.isRequired,
};
static defaultProps = {
...FlatList.defaultProps,
bounceFirstRowOnMount: true,
renderQuickActions: () => null,
};
constructor(props: Props<ItemT>, context: any): void {
super(props, context);
this.state = {
openRowKey: null,
};
this._shouldBounceFirstRowOnMount = this.props.bounceFirstRowOnMount;
}
render(): React.Node {
return (
<FlatList
{...this.props}
ref={ref => {
this._flatListRef = ref;
}}
onScroll={this._onScroll}
renderItem={this._renderItem}
/>
);
}
_onScroll = (e): void => {
// Close any opens rows on ListView scroll
if (this.state.openRowKey) {
this.setState({
openRowKey: null,
});
}
this.props.onScroll && this.props.onScroll(e);
};
_renderItem = (info: Object): ?React.Element<any> => {
const slideoutView = this.props.renderQuickActions(info);
const key = this.props.keyExtractor(info.item, info.index);
// If renderQuickActions is unspecified or returns falsey, don't allow swipe
if (!slideoutView) {
return this.props.renderItem(info);
}
let shouldBounceOnMount = false;
if (this._shouldBounceFirstRowOnMount) {
this._shouldBounceFirstRowOnMount = false;
shouldBounceOnMount = true;
}
return (
<SwipeableRow
slideoutView={slideoutView}
isOpen={key === this.state.openRowKey}
maxSwipeDistance={this._getMaxSwipeDistance(info)}
onOpen={() => this._onOpen(key)}
onClose={() => this._onClose(key)}
shouldBounceOnMount={shouldBounceOnMount}
onSwipeEnd={this._setListViewScrollable}
onSwipeStart={this._setListViewNotScrollable}>
{this.props.renderItem(info)}
</SwipeableRow>
);
};
// This enables rows having variable width slideoutView.
_getMaxSwipeDistance(info: Object): number {

This comment has been minimized.

Show comment
Hide comment
@chirag04

chirag04 Sep 29, 2017

Collaborator

this function needs to return a number which is really a problem because each quick action needs to be of a fixed width. curios to know how you guys pass that number @sahrens. is it static number for you guys or you guys do some layout first?

@chirag04

chirag04 Sep 29, 2017

Collaborator

this function needs to return a number which is really a problem because each quick action needs to be of a fixed width. curios to know how you guys pass that number @sahrens. is it static number for you guys or you guys do some layout first?

This comment has been minimized.

Show comment
Hide comment
@tomasreimers

tomasreimers Oct 2, 2017

Member

@chirag04 , Hi! Original author here -- Right now I'm just mirroring the implementation of SwipeableListView, so that developers can move from one to the other easily. What do you suggest as an alternative?

@tomasreimers

tomasreimers Oct 2, 2017

Member

@chirag04 , Hi! Original author here -- Right now I'm just mirroring the implementation of SwipeableListView, so that developers can move from one to the other easily. What do you suggest as an alternative?

This comment has been minimized.

Show comment
Hide comment
@chirag04

chirag04 Oct 2, 2017

Collaborator

@tomasreimers sure, my question was more general and applicable to SwipleableListView too. Ideally SwipeableRow should figure out this number on it's own.

I don't have any concrete suggestion yet but naively swipe-able row doing a layout measure for quick actions would make it dynamic. measuring each and every row is def worse.

Maybe some flexbox approach would work? idk.

@chirag04

chirag04 Oct 2, 2017

Collaborator

@tomasreimers sure, my question was more general and applicable to SwipleableListView too. Ideally SwipeableRow should figure out this number on it's own.

I don't have any concrete suggestion yet but naively swipe-able row doing a layout measure for quick actions would make it dynamic. measuring each and every row is def worse.

Maybe some flexbox approach would work? idk.

This comment has been minimized.

Show comment
Hide comment
@tomasreimers

tomasreimers Oct 3, 2017

Member

Totally agree with you. I think that it is sub-optimal that the buttons need to be fixed width, and that the developer needs to keep track of this. This is definitely something I also want to look more into, but I don't have a concrete idea yet (I would be interested to experiment with doing a dynamic measurement when the user starts swiping). If you have any ideas feel free to PR!

@tomasreimers

tomasreimers Oct 3, 2017

Member

Totally agree with you. I think that it is sub-optimal that the buttons need to be fixed width, and that the developer needs to keep track of this. This is definitely something I also want to look more into, but I don't have a concrete idea yet (I would be interested to experiment with doing a dynamic measurement when the user starts swiping). If you have any ideas feel free to PR!

if (typeof this.props.maxSwipeDistance === 'function') {
return this.props.maxSwipeDistance(info);
}
return this.props.maxSwipeDistance;
}
_setListViewScrollableTo(value: boolean) {
if (this._flatListRef) {
this._flatListRef.setNativeProps({
scrollEnabled: value,
});
}
}
_setListViewScrollable = () => {
this._setListViewScrollableTo(true);
};
_setListViewNotScrollable = () => {
this._setListViewScrollableTo(false);
};
_onOpen(key: any): void {
this.setState({
openRowKey: key,
});
}
_onClose(key: any): void {
this.setState({
openRowKey: null,
});
}
}
module.exports = SwipeableFlatList;
@@ -201,15 +201,15 @@ type OptionalProps<ItemT> = {
*/
viewabilityConfigCallbackPairs?: Array<ViewabilityConfigCallbackPair>,
};
type Props<ItemT> = RequiredProps<ItemT> &
export type Props<ItemT> = RequiredProps<ItemT> &
OptionalProps<ItemT> &
VirtualizedListProps;
const defaultProps = {
...VirtualizedList.defaultProps,
numColumns: 1,
};
type DefaultProps = typeof defaultProps;
export type DefaultProps = typeof defaultProps;
/**
* A performant interface for rendering simple, flat lists, supporting the most handy features:
@@ -42,7 +42,7 @@ import type {
type Item = any;
type renderItemType = (info: any) => ?React.Element<any>;
export type renderItemType = (info: any) => ?React.Element<any>;
type ViewabilityHelperCallbackTuple = {
viewabilityHelper: ViewabilityHelper,
@@ -46,6 +46,7 @@ const ReactNative = {
get Switch() { return require('Switch'); },
get RefreshControl() { return require('RefreshControl'); },
get StatusBar() { return require('StatusBar'); },
get SwipeableFlatList() { return require('SwipeableFlatList'); },
get SwipeableListView() { return require('SwipeableListView'); },
get TabBarIOS() { return require('TabBarIOS'); },
get Text() { return require('Text'); },
@@ -85,6 +85,10 @@ const ComponentExamples: Array<RNTesterExample> = [
key: 'StatusBarExample',
module: require('./StatusBarExample'),
},
{
key: 'SwipeableFlatListExample',
module: require('./SwipeableFlatListExample')
},
{
key: 'SwipeableListViewExample',
module: require('./SwipeableListViewExample')
@@ -153,6 +153,11 @@ const ComponentExamples: Array<RNTesterExample> = [
module: require('./StatusBarExample'),
supportsTVOS: false,
},
{
key: 'SwipeableFlatListExample',
module: require('./SwipeableFlatListExample'),
supportsTVOS: false,
},
{
key: 'SwipeableListViewExample',
module: require('./SwipeableListViewExample'),
@@ -0,0 +1,151 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule SwipeableFlatListExample
* @flow
* @format
*/
'use strict';
const React = require('react');
const createReactClass = require('create-react-class');
const ReactNative = require('react-native');
const {
Image,
SwipeableFlatList,
TouchableHighlight,
StyleSheet,
Text,
View,
Alert,
} = ReactNative;
const RNTesterPage = require('./RNTesterPage');
const data = [
{
key: 'like',
icon: require('./Thumbnails/like.png'),
data: 'Like!',
},
{
key: 'heart',
icon: require('./Thumbnails/heart.png'),
data: 'Heart!',
},
{
key: 'party',
icon: require('./Thumbnails/party.png'),
data: 'Party!',
},
];
const SwipeableFlatListExample = createReactClass({
displayName: 'SwipeableFlatListExample',
statics: {
title: '<SwipeableFlatList>',
description: 'Performant, scrollable, swipeable list of data.',
},
render: function() {
return (
<RNTesterPage
title={this.props.navigator ? null : '<SwipeableListView>'}
noSpacer={true}
noScroll={true}>
<SwipeableFlatList
data={data}
bounceFirstRowOnMount={true}
maxSwipeDistance={160}
renderItem={this._renderItem.bind(this)}
renderQuickActions={this._renderQuickActions.bind(this)}
/>
</RNTesterPage>
);
},
_renderItem: function({item}): ?React.Element<any> {
return (
<View style={styles.row}>
<Image style={styles.rowIcon} source={item.icon} />
<View style={styles.rowData}>
<Text style={styles.rowDataText}>
{item.data}
</Text>
</View>
</View>
);
},
_renderQuickActions: function({item}: Object): ?React.Element<any> {
return (
<View style={styles.actionsContainer}>
<TouchableHighlight
style={styles.actionButton}
onPress={() => {
Alert.alert(
'Tips',
'You could do something with this edit action!',
);
}}>
<Text style={styles.actionButtonText}>Edit</Text>
</TouchableHighlight>
<TouchableHighlight
style={[styles.actionButton, styles.actionButtonDestructive]}
onPress={() => {
Alert.alert(
'Tips',
'You could do something with this remove action!',
);
}}>
<Text style={styles.actionButtonText}>Remove</Text>
</TouchableHighlight>
</View>
);
},
});
var styles = StyleSheet.create({
row: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
padding: 10,
backgroundColor: '#F6F6F6',
},
rowIcon: {
width: 64,
height: 64,
marginRight: 20,
},
rowData: {
flex: 1,
},
rowDataText: {
fontSize: 24,
},
actionsContainer: {
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
},
actionButton: {
padding: 10,
width: 80,
backgroundColor: '#999999',
},
actionButtonDestructive: {
backgroundColor: '#FF0000',
},
actionButtonText: {
textAlign: 'center',
},
});
module.exports = SwipeableFlatListExample;

6 comments on commit d8cc6e3

@kesha-antonov

This comment has been minimized.

Show comment
Hide comment
@kesha-antonov

kesha-antonov Nov 5, 2017

Hello! @tomasreimers

Awesome PR. Thanks for this!

Can you also do support for SectionList?
Because I have SwipeableListView with sections.

kesha-antonov replied Nov 5, 2017

Hello! @tomasreimers

Awesome PR. Thanks for this!

Can you also do support for SectionList?
Because I have SwipeableListView with sections.

@kesha-antonov

This comment has been minimized.

Show comment
Hide comment
@kesha-antonov

kesha-antonov Nov 5, 2017

Also when you swipe item - others items not closed. Which they should.

It ensures that at most 1 row is swiped open (auto closes others)

kesha-antonov replied Nov 5, 2017

Also when you swipe item - others items not closed. Which they should.

It ensures that at most 1 row is swiped open (auto closes others)

@AntonPuko

This comment has been minimized.

Show comment
Hide comment
@AntonPuko

AntonPuko Nov 9, 2017

@kesha-antonov, The current version of the component is pretty broken due rerenders flow and optimizations, so it's not updated when it should(and/or) updated when it shouldn't :). The workaround I found is to fork this and add extraData={this.state} to FlatList props and make a wrapper around SwipeableRow with correct shouldComponentUpate.

AntonPuko replied Nov 9, 2017

@kesha-antonov, The current version of the component is pretty broken due rerenders flow and optimizations, so it's not updated when it should(and/or) updated when it shouldn't :). The workaround I found is to fork this and add extraData={this.state} to FlatList props and make a wrapper around SwipeableRow with correct shouldComponentUpate.

@kesha-antonov

This comment has been minimized.

Show comment
Hide comment
@kesha-antonov

kesha-antonov replied Nov 9, 2017

@AntonPuko #16682
Already did it 👍

@tomasreimers

This comment has been minimized.

Show comment
Hide comment
@tomasreimers

tomasreimers Dec 16, 2017

Member

Apologies for the incredibly late response!

@AntonPuko , would love to see a PR if you have one!

@kesha-antonov added the two as tasks for myself, although I've been handling other tasks. If you end up writing it, would love to incorporate the changes, otherwise I'll probably get around to it when the latest project is done! :)

Member

tomasreimers replied Dec 16, 2017

Apologies for the incredibly late response!

@AntonPuko , would love to see a PR if you have one!

@kesha-antonov added the two as tasks for myself, although I've been handling other tasks. If you end up writing it, would love to incorporate the changes, otherwise I'll probably get around to it when the latest project is done! :)

@AntonPuko

This comment has been minimized.

Show comment
Hide comment
@AntonPuko

AntonPuko Dec 16, 2017

@tomasreimers Hi, not sure if I'll have time to create PR nearly soon, but here is my version, feel free to grab if you want:
https://github.com/Brewskey/Brewskey.App/blob/v2/src/common/SwipeableFlatList/index.js
It has a few extra stuff like _onRefresh and hardcoded onEndReachedThreshold, that is project dependent and doesn't exist in the official version and can be removed, but in other places looks cleaner.

I think with current official swipeableRow all rows still will be re-rendered every time but its lack of SwipeableRow component, It works well if we use PureComponents for SwipeableRows, something like that: https://github.com/Brewskey/Brewskey.App/blob/v2/src/common/SwipeableLoaderRow.js

AntonPuko replied Dec 16, 2017

@tomasreimers Hi, not sure if I'll have time to create PR nearly soon, but here is my version, feel free to grab if you want:
https://github.com/Brewskey/Brewskey.App/blob/v2/src/common/SwipeableFlatList/index.js
It has a few extra stuff like _onRefresh and hardcoded onEndReachedThreshold, that is project dependent and doesn't exist in the official version and can be removed, but in other places looks cleaner.

I think with current official swipeableRow all rows still will be re-rendered every time but its lack of SwipeableRow component, It works well if we use PureComponents for SwipeableRows, something like that: https://github.com/Brewskey/Brewskey.App/blob/v2/src/common/SwipeableLoaderRow.js

Please sign in to comment.