Skip to content

Commit

Permalink
Add SwipeableFlatList
Browse files Browse the repository at this point in the history
Reviewed By: sahrens

Differential Revision: D5912488

fbshipit-source-id: 3d2872a7712c00badcbd8341a7d058df14a9091a
  • Loading branch information
Tomas Reimers authored and facebook-github-bot committed Sep 29, 2017
1 parent b64e6c7 commit d8cc6e3
Show file tree
Hide file tree
Showing 7 changed files with 353 additions and 3 deletions.
189 changes: 189 additions & 0 deletions Libraries/Experimental/SwipeableRow/SwipeableFlatList.js
Original file line number Diff line number Diff line change
@@ -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.

Copy link
@chirag04

chirag04 Sep 29, 2017

Contributor

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.

Copy link
@tomasreimers

tomasreimers Oct 2, 2017

Contributor

@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.

Copy link
@chirag04

chirag04 Oct 2, 2017

Contributor

@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.

Copy link
@tomasreimers

tomasreimers Oct 3, 2017

Contributor

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;
4 changes: 2 additions & 2 deletions Libraries/Lists/FlatList.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion Libraries/Lists/VirtualizedList.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions Libraries/react-native/react-native-implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'); },
Expand Down
4 changes: 4 additions & 0 deletions RNTester/js/RNTesterList.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ const ComponentExamples: Array<RNTesterExample> = [
key: 'StatusBarExample',
module: require('./StatusBarExample'),
},
{
key: 'SwipeableFlatListExample',
module: require('./SwipeableFlatListExample')
},
{
key: 'SwipeableListViewExample',
module: require('./SwipeableListViewExample')
Expand Down
5 changes: 5 additions & 0 deletions RNTester/js/RNTesterList.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
151 changes: 151 additions & 0 deletions RNTester/js/SwipeableFlatListExample.js
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello! @tomasreimers

Awesome PR. Thanks for this!

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

@kesha-antonov
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link

@AntonPuko AntonPuko commented on d8cc6e3 Nov 9, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AntonPuko #16682
Already did it 👍

@tomasreimers
Copy link
Contributor

@tomasreimers tomasreimers commented on d8cc6e3 Dec 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link

@AntonPuko AntonPuko commented on d8cc6e3 Dec 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.