mirror of
https://github.com/zoriya/react-native-web.git
synced 2026-05-22 22:44:52 +00:00
[add] SwipeableFlatList and SwipeableListView
This commit is contained in:
@@ -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
@@ -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,
|
||||
|
||||
+187
@@ -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;
|
||||
Vendored
+113
@@ -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;
|
||||
+211
@@ -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;
|
||||
Reference in New Issue
Block a user