[add] SwipeableFlatList and SwipeableListView

This commit is contained in:
Nicolas Gallagher
2018-06-04 11:39:55 -07:00
parent da38e87b50
commit b299eb6c59
9 changed files with 935 additions and 14 deletions
@@ -45,6 +45,7 @@ module.exports = {
Slider: true,
StatusBar: true,
StyleSheet: true,
SwipeableFlatList: true,
SwipeableListView: true,
Switch: true,
Text: true,
@@ -97,14 +97,14 @@ const ComponentExamples: Array<RNTesterExample> = [
key: 'StatusBarExample',
module: require('./StatusBarExample')
},
//{
// key: 'SwipeableFlatListExample',
// module: require('./SwipeableFlatListExample'),
//},
//{
// key: 'SwipeableListViewExample',
// module: require('./SwipeableListViewExample'),
//},
{
key: 'SwipeableFlatListExample',
module: require('./SwipeableFlatListExample'),
},
{
key: 'SwipeableListViewExample',
module: require('./SwipeableListViewExample'),
},
{
key: 'SwitchExample',
module: require('./SwitchExample')
@@ -0,0 +1,11 @@
/**
* Copyright (c) 2016-present, Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import SwipeableFlatList from '../../vendor/react-native/SwipeableFlatList';
export default SwipeableFlatList;
@@ -0,0 +1,11 @@
/**
* Copyright (c) 2016-present, Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import SwipeableListView from '../../vendor/react-native/SwipeableListView';
export default SwipeableListView;
+6 -6
View File
@@ -55,6 +55,8 @@ import ScrollView from './exports/ScrollView';
import SectionList from './exports/SectionList';
import Slider from './exports/Slider';
import StatusBar from './exports/StatusBar';
import SwipeableFlatList from './exports/SwipeableFlatList';
import SwipeableListView from './exports/SwipeableListView';
import Switch from './exports/Switch';
import Text from './exports/Text';
import TextInput from './exports/TextInput';
@@ -86,8 +88,6 @@ const ProgressBarAndroid = UnimplementedView;
const ProgressViewIOS = UnimplementedView;
const SegmentedControlIOS = UnimplementedView;
const SnapshotViewIOS = UnimplementedView;
const SwipeableFlatList = UnimplementedView;
const SwipeableListView = UnimplementedView;
const TabBarIOS = UnimplementedView;
const ToastAndroid = UnimplementedView;
const ToolbarAndroid = UnimplementedView;
@@ -164,6 +164,8 @@ export {
SectionList,
Slider,
StatusBar,
SwipeableFlatList,
SwipeableListView,
Switch,
Text,
TextInput,
@@ -192,8 +194,6 @@ export {
ProgressViewIOS,
SegmentedControlIOS,
SnapshotViewIOS,
SwipeableFlatList,
SwipeableListView,
TabBarIOS,
ToastAndroid,
ToolbarAndroid,
@@ -271,6 +271,8 @@ const ReactNative = {
SectionList,
Slider,
StatusBar,
SwipeableFlatList,
SwipeableListView,
Switch,
Text,
TextInput,
@@ -299,8 +301,6 @@ const ReactNative = {
ProgressViewIOS,
SegmentedControlIOS,
SnapshotViewIOS,
SwipeableFlatList,
SwipeableListView,
TabBarIOS,
ToastAndroid,
ToolbarAndroid,
@@ -0,0 +1,187 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @providesModule SwipeableFlatList
* @noflow
* @format
*/
'use strict';
import type {Props as FlatListProps} from '../FlatList';
import type {renderItemType} from '../VirtualizedList';
import PropTypes from 'prop-types';
import React from 'react';
import SwipeableRow from '../SwipeableRow';
import FlatList from '../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 occurring
* - 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 {
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,
});
}
}
export default SwipeableFlatList;
@@ -0,0 +1,113 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @providesModule SwipeableListViewDataSource
*/
'use strict';
import ListViewDataSource from '../ListView/ListViewDataSource';
/**
* Data source wrapper around ListViewDataSource to allow for tracking of
* which row is swiped open and close opened row(s) when another row is swiped
* open.
*
* See https://github.com/facebook/react-native/pull/5602 for why
* ListViewDataSource is not subclassed.
*/
class SwipeableListViewDataSource {
_previousOpenRowID: string;
_openRowID: string;
_dataBlob: any;
_dataSource: ListViewDataSource;
rowIdentities: Array<Array<string>>;
sectionIdentities: Array<string>;
constructor(params: Object) {
this._dataSource = new ListViewDataSource({
getRowData: params.getRowData,
getSectionHeaderData: params.getSectionHeaderData,
rowHasChanged: (row1, row2) => {
/**
* Row needs to be re-rendered if its swiped open/close status is
* changed, or its data blob changed.
*/
return (
(row1.id !== this._previousOpenRowID && row2.id === this._openRowID) ||
(row1.id === this._previousOpenRowID && row2.id !== this._openRowID) ||
params.rowHasChanged(row1, row2)
);
},
sectionHeaderHasChanged: params.sectionHeaderHasChanged,
});
}
cloneWithRowsAndSections(
dataBlob: any,
sectionIdentities: ?Array<string>,
rowIdentities: ?Array<Array<string>>
): SwipeableListViewDataSource {
this._dataSource = this._dataSource.cloneWithRowsAndSections(
dataBlob,
sectionIdentities,
rowIdentities
);
this._dataBlob = dataBlob;
this.rowIdentities = this._dataSource.rowIdentities;
this.sectionIdentities = this._dataSource.sectionIdentities;
return this;
}
// For the actual ListView to use
getDataSource(): ListViewDataSource {
return this._dataSource;
}
getOpenRowID(): ?string {
return this._openRowID;
}
getFirstRowID(): ?string {
/**
* If rowIdentities is specified, find the first data row from there since
* we don't want to attempt to bounce section headers. If unspecified, find
* the first data row from _dataBlob.
*/
if (this.rowIdentities) {
return this.rowIdentities[0] && this.rowIdentities[0][0];
}
return Object.keys(this._dataBlob)[0];
}
getLastRowID(): ?string {
if (this.rowIdentities && this.rowIdentities.length) {
const lastSection = this.rowIdentities[this.rowIdentities.length - 1];
if (lastSection && lastSection.length) {
return lastSection[lastSection.length - 1];
}
}
return Object.keys(this._dataBlob)[this._dataBlob.length - 1];
}
setOpenRowID(rowID: string): SwipeableListViewDataSource {
this._previousOpenRowID = this._openRowID;
this._openRowID = rowID;
this._dataSource = this._dataSource.cloneWithRowsAndSections(
this._dataBlob,
this.sectionIdentities,
this.rowIdentities
);
return this;
}
}
export default SwipeableListViewDataSource;
@@ -0,0 +1,211 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @providesModule SwipeableListView
* @noflow
*/
'use strict';
import ListView from '../ListView';
import PropTypes from 'prop-types';
import React from 'react';
import SwipeableListViewDataSource from './SwipeableListViewDataSource';
import SwipeableRow from '../SwipeableRow';
type DefaultProps = {
bounceFirstRowOnMount: boolean,
renderQuickActions: Function,
};
type Props = {
bounceFirstRowOnMount: boolean,
dataSource: SwipeableListViewDataSource,
maxSwipeDistance: number | (rowData: any, sectionID: string, rowID: string) => number,
onScroll?: ?Function,
renderRow: Function,
renderQuickActions: Function,
};
type State = {
dataSource: Object,
};
/**
* A container component that renders multiple SwipeableRow's in a ListView
* implementation. This is designed to be a drop-in replacement for the
* standard React Native `ListView`, so use it as if it were a ListView, but
* with extra props, i.e.
*
* let ds = SwipeableListView.getNewDataSource();
* ds.cloneWithRowsAndSections(dataBlob, ?sectionIDs, ?rowIDs);
* // ..
* <SwipeableListView renderRow={..} renderQuickActions={..} {..ListView 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
* - More to come
*/
class SwipeableListView extends React.Component<Props, State> {
props: Props;
state: State;
_listViewRef: ?React.Element<any> = null;
_shouldBounceFirstRowOnMount: boolean = false;
static getNewDataSource(): Object {
return new SwipeableListViewDataSource({
getRowData: (data, sectionID, rowID) => data[sectionID][rowID],
getSectionHeaderData: (data, sectionID) => data[sectionID],
rowHasChanged: (row1, row2) => row1 !== row2,
sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
});
}
static propTypes = {
/**
* To alert the user that swiping is possible, the first row can bounce
* on component mount.
*/
bounceFirstRowOnMount: PropTypes.bool.isRequired,
/**
* Use `SwipeableListView.getNewDataSource()` to get a data source to use,
* then use it just like you would a normal ListView data source
*/
dataSource: PropTypes.instanceOf(SwipeableListViewDataSource).isRequired,
// Maximum distance to open to after a swipe
maxSwipeDistance: PropTypes.oneOfType([
PropTypes.number,
PropTypes.func,
]).isRequired,
// Callback method to render the swipeable view
renderRow: PropTypes.func.isRequired,
// Callback method to render the view that will be unveiled on swipe
renderQuickActions: PropTypes.func.isRequired,
};
static defaultProps = {
bounceFirstRowOnMount: false,
renderQuickActions: () => null,
};
constructor(props: Props, context: any): void {
super(props, context);
this._shouldBounceFirstRowOnMount = this.props.bounceFirstRowOnMount;
this.state = {
dataSource: this.props.dataSource,
};
}
UNSAFE_componentWillReceiveProps(nextProps: Props): void {
if (this.state.dataSource.getDataSource() !== nextProps.dataSource.getDataSource()) {
this.setState({
dataSource: nextProps.dataSource,
});
}
}
render(): React.Node {
return (
<ListView
{...this.props}
ref={(ref) => {
this._listViewRef = ref;
}}
dataSource={this.state.dataSource.getDataSource()}
onScroll={this._onScroll}
renderRow={this._renderRow}
/>
);
}
_onScroll = (e): void => {
// Close any opens rows on ListView scroll
if (this.props.dataSource.getOpenRowID()) {
this.setState({
dataSource: this.state.dataSource.setOpenRowID(null),
});
}
this.props.onScroll && this.props.onScroll(e);
}
/**
* This is a work-around to lock vertical `ListView` scrolling on iOS and
* mimic Android behaviour. Locking vertical scrolling when horizontal
* scrolling is active allows us to significantly improve framerates
* (from high 20s to almost consistently 60 fps)
*/
_setListViewScrollable(value: boolean): void {
if (this._listViewRef && typeof this._listViewRef.setNativeProps === 'function') {
this._listViewRef.setNativeProps({
scrollEnabled: value,
});
}
}
// Passing through ListView's getScrollResponder() function
getScrollResponder(): ?Object {
if (this._listViewRef && typeof this._listViewRef.getScrollResponder === 'function') {
return this._listViewRef.getScrollResponder();
}
}
// This enables rows having variable width slideoutView.
_getMaxSwipeDistance(rowData: Object, sectionID: string, rowID: string): number {
if (typeof this.props.maxSwipeDistance === 'function') {
return this.props.maxSwipeDistance(rowData, sectionID, rowID);
}
return this.props.maxSwipeDistance;
}
_renderRow = (rowData: Object, sectionID: string, rowID: string): React.Element<any> => {
const slideoutView = this.props.renderQuickActions(rowData, sectionID, rowID);
// If renderQuickActions is unspecified or returns falsey, don't allow swipe
if (!slideoutView) {
return this.props.renderRow(rowData, sectionID, rowID);
}
let shouldBounceOnMount = false;
if (this._shouldBounceFirstRowOnMount) {
this._shouldBounceFirstRowOnMount = false;
shouldBounceOnMount = rowID === this.props.dataSource.getFirstRowID();
}
return (
<SwipeableRow
slideoutView={slideoutView}
isOpen={rowData.id === this.props.dataSource.getOpenRowID()}
maxSwipeDistance={this._getMaxSwipeDistance(rowData, sectionID, rowID)}
key={rowID}
onOpen={() => this._onOpen(rowData.id)}
onClose={() => this._onClose(rowData.id)}
onSwipeEnd={() => this._setListViewScrollable(true)}
onSwipeStart={() => this._setListViewScrollable(false)}
shouldBounceOnMount={shouldBounceOnMount}>
{this.props.renderRow(rowData, sectionID, rowID)}
</SwipeableRow>
);
};
_onOpen(rowID: string): void {
this.setState({
dataSource: this.state.dataSource.setOpenRowID(rowID),
});
}
_onClose(rowID: string): void {
this.setState({
dataSource: this.state.dataSource.setOpenRowID(null),
});
}
}
export default SwipeableListView;
@@ -0,0 +1,387 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @providesModule SwipeableRow
* @noflow
*/
'use strict';
import Animated from '../../../exports/Animated';
import I18nManager from '../../../exports/I18nManager';
import PanResponder from '../../../exports/PanResponder';
import React from 'react';
import PropTypes from 'prop-types';
import StyleSheet from '../../../exports/StyleSheet';
/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an error
* found when Flow v0.54 was deployed. To see the error delete this comment and
* run Flow. */
import TimerMixin from 'react-timer-mixin';
import View from '../../../exports/View';
import createReactClass from 'create-react-class';
import emptyFunction from 'fbjs/lib/emptyFunction';
const isRTL = () => I18nManager.isRTL;
// NOTE: Eventually convert these consts to an input object of configurations
// Position of the left of the swipable item when closed
const CLOSED_LEFT_POSITION = 0;
// Minimum swipe distance before we recognize it as such
const HORIZONTAL_SWIPE_DISTANCE_THRESHOLD = 10;
// Minimum swipe speed before we fully animate the user's action (open/close)
const HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD = 0.3;
// Factor to divide by to get slow speed; i.e. 4 means 1/4 of full speed
const SLOW_SPEED_SWIPE_FACTOR = 4;
// Time, in milliseconds, of how long the animated swipe should be
const SWIPE_DURATION = 300;
/**
* On SwipeableListView mount, the 1st item will bounce to show users it's
* possible to swipe
*/
const ON_MOUNT_BOUNCE_DELAY = 700;
const ON_MOUNT_BOUNCE_DURATION = 400;
// Distance left of closed position to bounce back when right-swiping from closed
const RIGHT_SWIPE_BOUNCE_BACK_DISTANCE = 30;
const RIGHT_SWIPE_BOUNCE_BACK_DURATION = 300;
/**
* Max distance of right swipe to allow (right swipes do functionally nothing).
* Must be multiplied by SLOW_SPEED_SWIPE_FACTOR because gestureState.dx tracks
* how far the finger swipes, and not the actual animation distance.
*/
const RIGHT_SWIPE_THRESHOLD = 30 * SLOW_SPEED_SWIPE_FACTOR;
/**
* Creates a swipable row that allows taps on the main item and a custom View
* on the item hidden behind the row. Typically this should be used in
* conjunction with SwipeableListView for additional functionality, but can be
* used in a normal ListView. See the renderRow for SwipeableListView to see how
* to use this component separately.
*/
const SwipeableRow = createReactClass({
displayName: 'SwipeableRow',
_panResponder: {},
_previousLeft: CLOSED_LEFT_POSITION,
mixins: [TimerMixin],
propTypes: {
children: PropTypes.any,
isOpen: PropTypes.bool,
preventSwipeRight: PropTypes.bool,
maxSwipeDistance: PropTypes.number.isRequired,
onOpen: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
onSwipeEnd: PropTypes.func.isRequired,
onSwipeStart: PropTypes.func.isRequired,
// Should bounce the row on mount
shouldBounceOnMount: PropTypes.bool,
/**
* A ReactElement that is unveiled when the user swipes
*/
slideoutView: PropTypes.node.isRequired,
/**
* The minimum swipe distance required before fully animating the swipe. If
* the user swipes less than this distance, the item will return to its
* previous (open/close) position.
*/
swipeThreshold: PropTypes.number.isRequired,
},
getInitialState(): Object {
return {
currentLeft: new Animated.Value(this._previousLeft),
/**
* In order to render component A beneath component B, A must be rendered
* before B. However, this will cause "flickering", aka we see A briefly
* then B. To counter this, _isSwipeableViewRendered flag is used to set
* component A to be transparent until component B is loaded.
*/
isSwipeableViewRendered: false,
rowHeight: (null: ?number),
};
},
getDefaultProps(): Object {
return {
isOpen: false,
preventSwipeRight: false,
maxSwipeDistance: 0,
onOpen: emptyFunction,
onClose: emptyFunction,
onSwipeEnd: emptyFunction,
onSwipeStart: emptyFunction,
swipeThreshold: 30,
};
},
UNSAFE_componentWillMount(): void {
this._panResponder = PanResponder.create({
onMoveShouldSetPanResponderCapture: this._handleMoveShouldSetPanResponderCapture,
onPanResponderGrant: this._handlePanResponderGrant,
onPanResponderMove: this._handlePanResponderMove,
onPanResponderRelease: this._handlePanResponderEnd,
onPanResponderTerminationRequest: this._onPanResponderTerminationRequest,
onPanResponderTerminate: this._handlePanResponderEnd,
onShouldBlockNativeResponder: (event, gestureState) => false,
});
},
componentDidMount(): void {
if (this.props.shouldBounceOnMount) {
/**
* Do the on mount bounce after a delay because if we animate when other
* components are loading, the animation will be laggy
*/
this.setTimeout(() => {
this._animateBounceBack(ON_MOUNT_BOUNCE_DURATION);
}, ON_MOUNT_BOUNCE_DELAY);
}
},
UNSAFE_componentWillReceiveProps(nextProps: Object): void {
/**
* We do not need an "animateOpen(noCallback)" because this animation is
* handled internally by this component.
*/
if (this.props.isOpen && !nextProps.isOpen) {
this._animateToClosedPosition();
}
},
render(): React.Element<any> {
// The view hidden behind the main view
let slideOutView;
if (this.state.isSwipeableViewRendered && this.state.rowHeight) {
slideOutView = (
<View style={[
styles.slideOutContainer,
{height: this.state.rowHeight},
]}>
{this.props.slideoutView}
</View>
);
}
// The swipeable item
const swipeableView = (
<Animated.View
onLayout={this._onSwipeableViewLayout}
style={{transform: [{translateX: this.state.currentLeft}]}}>
{this.props.children}
</Animated.View>
);
return (
<View
{...this._panResponder.panHandlers}>
{slideOutView}
{swipeableView}
</View>
);
},
close(): void {
this.props.onClose();
this._animateToClosedPosition();
},
_onSwipeableViewLayout(event: Object): void {
this.setState({
isSwipeableViewRendered: true,
rowHeight: event.nativeEvent.layout.height,
});
},
_handleMoveShouldSetPanResponderCapture(
event: Object,
gestureState: Object,
): boolean {
// Decides whether a swipe is responded to by this component or its child
return gestureState.dy < 10 && this._isValidSwipe(gestureState);
},
_handlePanResponderGrant(event: Object, gestureState: Object): void {
},
_handlePanResponderMove(event: Object, gestureState: Object): void {
if (this._isSwipingExcessivelyRightFromClosedPosition(gestureState)) {
return;
}
this.props.onSwipeStart();
if (this._isSwipingRightFromClosed(gestureState)) {
this._swipeSlowSpeed(gestureState);
} else {
this._swipeFullSpeed(gestureState);
}
},
_isSwipingRightFromClosed(gestureState: Object): boolean {
const gestureStateDx = isRTL() ? -gestureState.dx : gestureState.dx;
return this._previousLeft === CLOSED_LEFT_POSITION && gestureStateDx > 0;
},
_swipeFullSpeed(gestureState: Object): void {
this.state.currentLeft.setValue(this._previousLeft + gestureState.dx);
},
_swipeSlowSpeed(gestureState: Object): void {
this.state.currentLeft.setValue(
this._previousLeft + gestureState.dx / SLOW_SPEED_SWIPE_FACTOR,
);
},
_isSwipingExcessivelyRightFromClosedPosition(gestureState: Object): boolean {
/**
* We want to allow a BIT of right swipe, to allow users to know that
* swiping is available, but swiping right does not do anything
* functionally.
*/
const gestureStateDx = isRTL() ? -gestureState.dx : gestureState.dx;
return (
this._isSwipingRightFromClosed(gestureState) &&
gestureStateDx > RIGHT_SWIPE_THRESHOLD
);
},
_onPanResponderTerminationRequest(
event: Object,
gestureState: Object,
): boolean {
return false;
},
_animateTo(
toValue: number,
duration: number = SWIPE_DURATION,
callback: Function = emptyFunction,
): void {
Animated.timing(
this.state.currentLeft,
{
duration,
toValue,
useNativeDriver: true,
},
).start(() => {
this._previousLeft = toValue;
callback();
});
},
_animateToOpenPosition(): void {
const maxSwipeDistance = isRTL() ? -this.props.maxSwipeDistance : this.props.maxSwipeDistance;
this._animateTo(-maxSwipeDistance);
},
_animateToOpenPositionWith(
speed: number,
distMoved: number,
): void {
/**
* Ensure the speed is at least the set speed threshold to prevent a slow
* swiping animation
*/
speed = (
speed > HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD ?
speed :
HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD
);
/**
* Calculate the duration the row should take to swipe the remaining distance
* at the same speed the user swiped (or the speed threshold)
*/
const duration = Math.abs((this.props.maxSwipeDistance - Math.abs(distMoved)) / speed);
const maxSwipeDistance = isRTL() ? -this.props.maxSwipeDistance : this.props.maxSwipeDistance;
this._animateTo(-maxSwipeDistance, duration);
},
_animateToClosedPosition(duration: number = SWIPE_DURATION): void {
this._animateTo(CLOSED_LEFT_POSITION, duration);
},
_animateToClosedPositionDuringBounce(): void {
this._animateToClosedPosition(RIGHT_SWIPE_BOUNCE_BACK_DURATION);
},
_animateBounceBack(duration: number): void {
/**
* When swiping right, we want to bounce back past closed position on release
* so users know they should swipe right to get content.
*/
const swipeBounceBackDistance = isRTL() ?
-RIGHT_SWIPE_BOUNCE_BACK_DISTANCE :
RIGHT_SWIPE_BOUNCE_BACK_DISTANCE;
this._animateTo(
-swipeBounceBackDistance,
duration,
this._animateToClosedPositionDuringBounce,
);
},
// Ignore swipes due to user's finger moving slightly when tapping
_isValidSwipe(gestureState: Object): boolean {
if (this.props.preventSwipeRight && this._previousLeft === CLOSED_LEFT_POSITION && gestureState.dx > 0) {
return false;
}
return Math.abs(gestureState.dx) > HORIZONTAL_SWIPE_DISTANCE_THRESHOLD;
},
_shouldAnimateRemainder(gestureState: Object): boolean {
/**
* If user has swiped past a certain distance, animate the rest of the way
* if they let go
*/
return (
Math.abs(gestureState.dx) > this.props.swipeThreshold ||
gestureState.vx > HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD
);
},
_handlePanResponderEnd(event: Object, gestureState: Object): void {
const horizontalDistance = isRTL() ? -gestureState.dx : gestureState.dx;
if (this._isSwipingRightFromClosed(gestureState)) {
this.props.onOpen();
this._animateBounceBack(RIGHT_SWIPE_BOUNCE_BACK_DURATION);
} else if (this._shouldAnimateRemainder(gestureState)) {
if (horizontalDistance < 0) {
// Swiped left
this.props.onOpen();
this._animateToOpenPositionWith(gestureState.vx, horizontalDistance);
} else {
// Swiped right
this.props.onClose();
this._animateToClosedPosition();
}
} else {
if (this._previousLeft === CLOSED_LEFT_POSITION) {
this._animateToClosedPosition();
} else {
this._animateToOpenPosition();
}
}
this.props.onSwipeEnd();
},
});
const styles = StyleSheet.create({
slideOutContainer: {
bottom: 0,
left: 0,
position: 'absolute',
right: 0,
top: 0,
},
});
export default SwipeableRow;