From 052cd7eb8afa7a805ef13e940251be080499919c Mon Sep 17 00:00:00 2001 From: Fred Liu Date: Wed, 11 May 2016 00:24:08 -0700 Subject: [PATCH] SwipeableListView Summary: - Updated SwipeableListView to be much more performant by checking `rowHasChanged` more vigorously - New `SwipeableListViewDataSource` used to mask implementation details from caller Reviewed By: fkgozali Differential Revision: D3272172 fbshipit-source-id: 02f66ed7fce7d587118ad7d82b20f8e78db44b7b --- .../SwipeableRow/SwipeableListView.js | 115 +++++++++++++++++ .../SwipeableListViewDataSource.js | 103 +++++++++++++++ .../Experimental/SwipeableRow/SwipeableRow.js | 21 ++-- .../SwipeableRow/SwipeableRowListView.js | 117 ------------------ 4 files changed, 231 insertions(+), 125 deletions(-) create mode 100644 Libraries/Experimental/SwipeableRow/SwipeableListView.js create mode 100644 Libraries/Experimental/SwipeableRow/SwipeableListViewDataSource.js delete mode 100644 Libraries/Experimental/SwipeableRow/SwipeableRowListView.js diff --git a/Libraries/Experimental/SwipeableRow/SwipeableListView.js b/Libraries/Experimental/SwipeableRow/SwipeableListView.js new file mode 100644 index 000000000..f026473a7 --- /dev/null +++ b/Libraries/Experimental/SwipeableRow/SwipeableListView.js @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2013-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. + * + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + * + * @providesModule SwipeableListView + * @flow + */ +'use strict'; + +const ListView = require('ListView'); +const React = require('React'); +const SwipeableListViewDataSource = require('SwipeableListViewDataSource'); +const SwipeableRow = require('SwipeableRow'); + +const {PropTypes} = React; + +/** + * A container component that renders multiple SwipeableRow's in a ListView + * implementation. + */ +const SwipeableListView = React.createClass({ + statics: { + getNewDataSource(): Object { + return new SwipeableListViewDataSource({ + getRowData: (data, sectionID, rowID) => data[rowID], + getSectionHeaderData: (data, sectionID) => data[sectionID], + sectionHeaderHasChanged: (s1, s2) => s1 !== s2, + rowHasChanged: (row1, row2) => row1 !== row2, + }); + }, + }, + + propTypes: { + dataSource: PropTypes.object.isRequired, // SwipeableListViewDataSource + maxSwipeDistance: PropTypes.number, + // 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, + }, + + getDefaultProps(): Object { + return { + renderQuickActions: () => null, + }; + }, + + getInitialState(): Object { + return { + dataSource: this.props.dataSource.getDataSource(), + }; + }, + + componentWillReceiveProps(nextProps: Object): void { + if ('dataSource' in nextProps && this.state.dataSource !== nextProps.dataSource) { + this.setState({ + dataSource: nextProps.dataSource.getDataSource(), + }); + } + }, + + render(): ReactElement { + return ( + + ); + }, + + _renderRow(rowData: Object, sectionID: string, rowID: string): ReactElement { + const slideoutView = this.props.renderQuickActions(rowData, sectionID, rowID); + + // If renderRowSlideout is unspecified or returns falsey, don't allow swipe + if (!slideoutView) { + return this.props.renderRow(rowData, sectionID, rowID); + } + + return ( + this._onOpen(rowData.id)}> + {this.props.renderRow(rowData, sectionID, rowID)} + + ); + }, + + _onOpen(rowID: string): void { + this.setState({ + dataSource: this.props.dataSource.setOpenRowID(rowID), + }); + }, +}); + +module.exports = SwipeableListView; diff --git a/Libraries/Experimental/SwipeableRow/SwipeableListViewDataSource.js b/Libraries/Experimental/SwipeableRow/SwipeableListViewDataSource.js new file mode 100644 index 000000000..8d57f37ef --- /dev/null +++ b/Libraries/Experimental/SwipeableRow/SwipeableListViewDataSource.js @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2013-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. + * + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + * + * @providesModule SwipeableListViewDataSource + */ +'use strict'; + +const ListViewDataSource = require('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>; + sectionIdentities: Array; + + 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, + rowIdentities: ?Array> + ): 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; + } + + setOpenRowID(rowID: string): ListViewDataSource { + this._previousOpenRowID = this._openRowID; + this._openRowID = rowID; + + return this._dataSource.cloneWithRowsAndSections( + this._dataBlob, + this.sectionIdentities, + this.rowIdentities + ); + } +} + +module.exports = SwipeableListViewDataSource; diff --git a/Libraries/Experimental/SwipeableRow/SwipeableRow.js b/Libraries/Experimental/SwipeableRow/SwipeableRow.js index 0e409241d..d3c9a5e90 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableRow.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableRow.js @@ -25,6 +25,7 @@ const Animated = require('Animated'); const PanResponder = require('PanResponder'); +const Platform = require('Platform'); const React = require('React'); const StyleSheet = require('StyleSheet'); const View = require('View'); @@ -114,16 +115,20 @@ const SwipeableRow = React.createClass({ }, render(): ReactElement { + const slideoutStyle = [ + styles.slideOutContainer, + { + right: -this.state.scrollViewWidth, + width: this.state.scrollViewWidth, + }, + ]; + if (Platform.OS === 'ios') { + slideoutStyle.push({opacity: this._isSwipeableViewRendered ? 1 : 0}); + } + // The view hidden behind the main view const slideOutView = ( - + {this.props.slideoutView} ); diff --git a/Libraries/Experimental/SwipeableRow/SwipeableRowListView.js b/Libraries/Experimental/SwipeableRow/SwipeableRowListView.js deleted file mode 100644 index b9f2c3e03..000000000 --- a/Libraries/Experimental/SwipeableRow/SwipeableRowListView.js +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Copyright (c) 2013-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. - * - * The examples provided by Facebook are for non-commercial testing and - * evaluation purposes only. - * - * Facebook reserves all rights not expressly granted. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL - * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * - * - * @providesModule SwipeableRowListView - * @flow - */ -'use strict'; - -const ListViewDataSource = require('ListViewDataSource'); -const React = require('React'); -const SwipeableRow = require('SwipeableRow'); - -const {PropTypes} = React; - -/** - * A container component that renders multiple SwipeableRow's in a provided - * ListView implementation and allows a maximum of 1 SwipeableRow to be open at - * any given time. - */ -const SwipeableRowListView = React.createClass({ - propTypes: { - // Raw data blob for the ListView - dataBlob: PropTypes.object.isRequired, - /** - * Provided implementation of ListView that will be used to render - * SwipeableRow elements from dataBlob - */ - listView: PropTypes.func.isRequired, - maxSwipeDistance: PropTypes.number, - // Callback method to render the view that will be unveiled on swipe - renderRowSlideout: PropTypes.func.isRequired, - // Callback method to render the swipeable view - renderRowSwipeable: PropTypes.func.isRequired, - rowIDs: PropTypes.array.isRequired, - sectionIDs: PropTypes.array.isRequired, - }, - - getInitialState(): Object { - const ds = new ListViewDataSource({ - getRowData: (data, sectionID, rowID) => data[rowID], - getSectionHeaderData: (data, sectionID) => data[sectionID], - rowHasChanged: (row1, row2) => row1 !== row2, - sectionHeaderHasChanged: (s1, s2) => s1 !== s2, - }); - - return { - dataSource: ds.cloneWithRowsAndSections( - this.props.dataBlob, - this.props.sectionIDs, - this.props.rowIDs, - ), - }; - }, - - render(): ReactElement { - const CustomListView = this.props.listView; - - return ( - - ); - }, - - _renderRow(rowData: Object, sectionID: string, rowID: string): ReactElement { - return ( - this._onRowOpen(rowID)}> - {this.props.renderRowSwipeable( - rowData, - sectionID, - rowID, - this.state.dataSource, - )} - - ); - }, - - _onRowOpen(rowID: string): void { - // Need to recreate dataSource object and not just update existing - const blob = JSON.parse(JSON.stringify(this.props.dataBlob)); - blob[rowID].isOpen = true; - - this.setState({ - dataSource: this.state.dataSource.cloneWithRowsAndSections( - blob, - this.props.sectionIDs, - this.props.rowIDs, - ), - }); - }, -}); - -module.exports = SwipeableRowListView;