From 5a399bd657977436e02c74699302961a308191fc Mon Sep 17 00:00:00 2001 From: Thomas BARRAS Date: Sat, 20 Oct 2018 20:59:25 -0400 Subject: [PATCH 1/2] UPDATE: rm createReactClass from SwipeableRow --- .../Experimental/SwipeableRow/SwipeableRow.js | 158 +++++++----------- 1 file changed, 65 insertions(+), 93 deletions(-) diff --git a/Libraries/Experimental/SwipeableRow/SwipeableRow.js b/Libraries/Experimental/SwipeableRow/SwipeableRow.js index 5e9e796cdcdb79..f8cdf4fa30c359 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableRow.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableRow.js @@ -14,11 +14,9 @@ const Animated = require('Animated'); const I18nManager = require('I18nManager'); const PanResponder = require('PanResponder'); const React = require('React'); -const PropTypes = require('prop-types'); const StyleSheet = require('StyleSheet'); const View = require('View'); -const createReactClass = require('create-react-class'); const emptyFunction = require('fbjs/lib/emptyFunction'); import type {LayoutEvent, PressEvent} from 'CoreEventTypes'; @@ -59,7 +57,7 @@ const RIGHT_SWIPE_THRESHOLD = 30 * SLOW_SPEED_SWIPE_FACTOR; type Props = $ReadOnly<{| children?: ?React.Node, isOpen?: ?boolean, - maxSwipeDistance?: ?number, + maxSwipeDistance: number, onClose?: ?Function, onOpen?: ?Function, onSwipeEnd?: ?Function, @@ -67,9 +65,15 @@ type Props = $ReadOnly<{| preventSwipeRight?: ?boolean, shouldBounceOnMount?: ?boolean, slideoutView?: ?React.Node, - swipeThreshold?: ?number, + swipeThreshold: number, |}>; +type State = { + currentLeft: Animated.Value, + isSwipeableViewRendered: boolean, + rowHeight: ?number, +}; + /** * 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 @@ -77,61 +81,33 @@ type Props = $ReadOnly<{| * 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, - _timeoutID: (null: ?TimeoutID), - - 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, +class SwipeableRow extends React.Component { + _panResponder = {}; + _previousLeft = CLOSED_LEFT_POSITION; + _timeoutID: ?TimeoutID = null; + + static defaultProps = { + isOpen: false, + preventSwipeRight: false, + maxSwipeDistance: 0, + onOpen: emptyFunction, + onClose: emptyFunction, + onSwipeEnd: emptyFunction, + onSwipeStart: emptyFunction, + swipeThreshold: 30, + }; + + state = { + currentLeft: new Animated.Value(this._previousLeft), /** - * 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. + * 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. */ - 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, - }; - }, + isSwipeableViewRendered: false, + rowHeight: null, + }; UNSAFE_componentWillMount(): void { this._panResponder = PanResponder.create({ @@ -144,7 +120,7 @@ const SwipeableRow = createReactClass({ onPanResponderTerminate: this._handlePanResponderEnd, onShouldBlockNativeResponder: (event, gestureState) => false, }); - }, + } componentDidMount(): void { if (this.props.shouldBounceOnMount) { @@ -156,7 +132,7 @@ const SwipeableRow = createReactClass({ this._animateBounceBack(ON_MOUNT_BOUNCE_DURATION); }, ON_MOUNT_BOUNCE_DELAY); } - }, + } UNSAFE_componentWillReceiveProps(nextProps: Object): void { /** @@ -166,13 +142,13 @@ const SwipeableRow = createReactClass({ if (this.props.isOpen && !nextProps.isOpen) { this._animateToClosedPosition(); } - }, + } componentWillUnmount() { if (this._timeoutID != null) { clearTimeout(this._timeoutID); } - }, + } render(): React.Element { // The view hidden behind the main view @@ -201,19 +177,19 @@ const SwipeableRow = createReactClass({ {swipeableView} ); - }, + } close(): void { - this.props.onClose(); + this.props.onClose && this.props.onClose(); this._animateToClosedPosition(); - }, + } _onSwipeableViewLayout(event: LayoutEvent): void { this.setState({ isSwipeableViewRendered: true, rowHeight: event.nativeEvent.layout.height, }); - }, + } _handleMoveShouldSetPanResponderCapture( event: PressEvent, @@ -221,41 +197,41 @@ const SwipeableRow = createReactClass({ ): boolean { // Decides whether a swipe is responded to by this component or its child return gestureState.dy < 10 && this._isValidSwipe(gestureState); - }, + } _handlePanResponderGrant( event: PressEvent, gestureState: GestureState, - ): void {}, + ): void {} _handlePanResponderMove(event: PressEvent, gestureState: GestureState): void { if (this._isSwipingExcessivelyRightFromClosedPosition(gestureState)) { return; } - this.props.onSwipeStart(); + this.props.onSwipeStart && this.props.onSwipeStart(); if (this._isSwipingRightFromClosed(gestureState)) { this._swipeSlowSpeed(gestureState); } else { this._swipeFullSpeed(gestureState); } - }, + } _isSwipingRightFromClosed(gestureState: GestureState): boolean { const gestureStateDx = IS_RTL ? -gestureState.dx : gestureState.dx; return this._previousLeft === CLOSED_LEFT_POSITION && gestureStateDx > 0; - }, + } _swipeFullSpeed(gestureState: GestureState): void { this.state.currentLeft.setValue(this._previousLeft + gestureState.dx); - }, + } _swipeSlowSpeed(gestureState: GestureState): void { this.state.currentLeft.setValue( this._previousLeft + gestureState.dx / SLOW_SPEED_SWIPE_FACTOR, ); - }, + } _isSwipingExcessivelyRightFromClosedPosition( gestureState: GestureState, @@ -270,14 +246,14 @@ const SwipeableRow = createReactClass({ this._isSwipingRightFromClosed(gestureState) && gestureStateDx > RIGHT_SWIPE_THRESHOLD ); - }, + } _onPanResponderTerminationRequest( event: PressEvent, gestureState: GestureState, ): boolean { return false; - }, + } _animateTo( toValue: number, @@ -292,14 +268,15 @@ const SwipeableRow = createReactClass({ this._previousLeft = toValue; callback(); }); - }, + } _animateToOpenPosition(): void { const maxSwipeDistance = IS_RTL ? -this.props.maxSwipeDistance : this.props.maxSwipeDistance; + this._animateTo(-maxSwipeDistance); - }, + } _animateToOpenPositionWith(speed: number, distMoved: number): void { /** @@ -321,15 +298,15 @@ const SwipeableRow = createReactClass({ ? -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 { /** @@ -344,7 +321,7 @@ const SwipeableRow = createReactClass({ duration, this._animateToClosedPositionDuringBounce, ); - }, + } // Ignore swipes due to user's finger moving slightly when tapping _isValidSwipe(gestureState: GestureState): boolean { @@ -357,7 +334,7 @@ const SwipeableRow = createReactClass({ } return Math.abs(gestureState.dx) > HORIZONTAL_SWIPE_DISTANCE_THRESHOLD; - }, + } _shouldAnimateRemainder(gestureState: GestureState): boolean { /** @@ -368,21 +345,21 @@ const SwipeableRow = createReactClass({ Math.abs(gestureState.dx) > this.props.swipeThreshold || gestureState.vx > HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD ); - }, + } _handlePanResponderEnd(event: PressEvent, gestureState: GestureState): void { const horizontalDistance = IS_RTL ? -gestureState.dx : gestureState.dx; if (this._isSwipingRightFromClosed(gestureState)) { - this.props.onOpen(); + this.props.onOpen && this.props.onOpen(); this._animateBounceBack(RIGHT_SWIPE_BOUNCE_BACK_DURATION); } else if (this._shouldAnimateRemainder(gestureState)) { if (horizontalDistance < 0) { // Swiped left - this.props.onOpen(); + this.props.onOpen && this.props.onOpen(); this._animateToOpenPositionWith(gestureState.vx, horizontalDistance); } else { // Swiped right - this.props.onClose(); + this.props.onClose && this.props.onClose(); this._animateToClosedPosition(); } } else { @@ -393,13 +370,8 @@ const SwipeableRow = createReactClass({ } } - this.props.onSwipeEnd(); - }, -}); - -// TODO: Delete this when `SwipeableRow` uses class syntax. -class TypedSwipeableRow extends React.Component { - close() {} + this.props.onSwipeEnd && this.props.onSwipeEnd(); + } } const styles = StyleSheet.create({ @@ -412,4 +384,4 @@ const styles = StyleSheet.create({ }, }); -module.exports = ((SwipeableRow: any): Class); +module.exports = SwipeableRow; From 9edf7c32c2510cb280c57cacfa9b8b003ae628dc Mon Sep 17 00:00:00 2001 From: Thomas BARRAS Date: Sat, 20 Oct 2018 21:19:37 -0400 Subject: [PATCH 2/2] UPDATE: Bind functions + clean UNSAFE method --- .../Experimental/SwipeableRow/SwipeableRow.js | 195 +++++++++--------- 1 file changed, 98 insertions(+), 97 deletions(-) diff --git a/Libraries/Experimental/SwipeableRow/SwipeableRow.js b/Libraries/Experimental/SwipeableRow/SwipeableRow.js index f8cdf4fa30c359..5a05c0c9915a6e 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableRow.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableRow.js @@ -57,15 +57,15 @@ const RIGHT_SWIPE_THRESHOLD = 30 * SLOW_SPEED_SWIPE_FACTOR; type Props = $ReadOnly<{| children?: ?React.Node, isOpen?: ?boolean, - maxSwipeDistance: number, - onClose?: ?Function, - onOpen?: ?Function, - onSwipeEnd?: ?Function, - onSwipeStart?: ?Function, + maxSwipeDistance?: ?number, + onClose?: ?() => void, + onOpen?: ?() => void, + onSwipeEnd?: ?() => void, + onSwipeStart?: ?() => void, preventSwipeRight?: ?boolean, shouldBounceOnMount?: ?boolean, slideoutView?: ?React.Node, - swipeThreshold: number, + swipeThreshold?: ?number, |}>; type State = { @@ -82,7 +82,81 @@ type State = { * to use this component separately. */ class SwipeableRow extends React.Component { - _panResponder = {}; + _handleMoveShouldSetPanResponderCapture = ( + event: PressEvent, + gestureState: GestureState, + ): boolean => { + // Decides whether a swipe is responded to by this component or its child + return gestureState.dy < 10 && this._isValidSwipe(gestureState); + }; + + _handlePanResponderGrant = ( + event: PressEvent, + gestureState: GestureState, + ): void => {}; + + _handlePanResponderMove = ( + event: PressEvent, + gestureState: GestureState, + ): void => { + if (this._isSwipingExcessivelyRightFromClosedPosition(gestureState)) { + return; + } + + this.props.onSwipeStart && this.props.onSwipeStart(); + + if (this._isSwipingRightFromClosed(gestureState)) { + this._swipeSlowSpeed(gestureState); + } else { + this._swipeFullSpeed(gestureState); + } + }; + + _onPanResponderTerminationRequest = ( + event: PressEvent, + gestureState: GestureState, + ): boolean => { + return false; + }; + + _handlePanResponderEnd = ( + event: PressEvent, + gestureState: GestureState, + ): void => { + const horizontalDistance = IS_RTL ? -gestureState.dx : gestureState.dx; + if (this._isSwipingRightFromClosed(gestureState)) { + this.props.onOpen && this.props.onOpen(); + this._animateBounceBack(RIGHT_SWIPE_BOUNCE_BACK_DURATION); + } else if (this._shouldAnimateRemainder(gestureState)) { + if (horizontalDistance < 0) { + // Swiped left + this.props.onOpen && this.props.onOpen(); + this._animateToOpenPositionWith(gestureState.vx, horizontalDistance); + } else { + // Swiped right + this.props.onClose && this.props.onClose(); + this._animateToClosedPosition(); + } + } else { + if (this._previousLeft === CLOSED_LEFT_POSITION) { + this._animateToClosedPosition(); + } else { + this._animateToOpenPosition(); + } + } + + this.props.onSwipeEnd && this.props.onSwipeEnd(); + }; + _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, + }); _previousLeft = CLOSED_LEFT_POSITION; _timeoutID: ?TimeoutID = null; @@ -109,19 +183,6 @@ class SwipeableRow extends React.Component { rowHeight: null, }; - 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) { /** @@ -184,39 +245,12 @@ class SwipeableRow extends React.Component { this._animateToClosedPosition(); } - _onSwipeableViewLayout(event: LayoutEvent): void { + _onSwipeableViewLayout = (event: LayoutEvent): void => { this.setState({ isSwipeableViewRendered: true, rowHeight: event.nativeEvent.layout.height, }); - } - - _handleMoveShouldSetPanResponderCapture( - event: PressEvent, - gestureState: GestureState, - ): boolean { - // Decides whether a swipe is responded to by this component or its child - return gestureState.dy < 10 && this._isValidSwipe(gestureState); - } - - _handlePanResponderGrant( - event: PressEvent, - gestureState: GestureState, - ): void {} - - _handlePanResponderMove(event: PressEvent, gestureState: GestureState): void { - if (this._isSwipingExcessivelyRightFromClosedPosition(gestureState)) { - return; - } - - this.props.onSwipeStart && this.props.onSwipeStart(); - - if (this._isSwipingRightFromClosed(gestureState)) { - this._swipeSlowSpeed(gestureState); - } else { - this._swipeFullSpeed(gestureState); - } - } + }; _isSwipingRightFromClosed(gestureState: GestureState): boolean { const gestureStateDx = IS_RTL ? -gestureState.dx : gestureState.dx; @@ -248,13 +282,6 @@ class SwipeableRow extends React.Component { ); } - _onPanResponderTerminationRequest( - event: PressEvent, - gestureState: GestureState, - ): boolean { - return false; - } - _animateTo( toValue: number, duration: number = SWIPE_DURATION, @@ -271,11 +298,11 @@ class SwipeableRow extends React.Component { } _animateToOpenPosition(): void { - const maxSwipeDistance = IS_RTL - ? -this.props.maxSwipeDistance - : this.props.maxSwipeDistance; - - this._animateTo(-maxSwipeDistance); + const maxSwipeDistance = this.props.maxSwipeDistance ?? 0; + const directionAwareMaxSwipeDistance = IS_RTL + ? -maxSwipeDistance + : maxSwipeDistance; + this._animateTo(-directionAwareMaxSwipeDistance); } _animateToOpenPositionWith(speed: number, distMoved: number): void { @@ -287,26 +314,25 @@ class SwipeableRow extends React.Component { speed > HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD ? speed : HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD; + const maxSwipeDistance = this.props.maxSwipeDistance ?? 0; /** * 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 = IS_RTL - ? -this.props.maxSwipeDistance - : this.props.maxSwipeDistance; - this._animateTo(-maxSwipeDistance, duration); + const duration = Math.abs((maxSwipeDistance - Math.abs(distMoved)) / speed); + const directionAwareMaxSwipeDistance = IS_RTL + ? -maxSwipeDistance + : maxSwipeDistance; + this._animateTo(-directionAwareMaxSwipeDistance, duration); } _animateToClosedPosition(duration: number = SWIPE_DURATION): void { this._animateTo(CLOSED_LEFT_POSITION, duration); } - _animateToClosedPositionDuringBounce(): void { + _animateToClosedPositionDuringBounce = (): void => { this._animateToClosedPosition(RIGHT_SWIPE_BOUNCE_BACK_DURATION); - } + }; _animateBounceBack(duration: number): void { /** @@ -341,37 +367,12 @@ class SwipeableRow extends React.Component { * If user has swiped past a certain distance, animate the rest of the way * if they let go */ + const swipeThreshold = this.props.swipeThreshold ?? 0; return ( - Math.abs(gestureState.dx) > this.props.swipeThreshold || + Math.abs(gestureState.dx) > swipeThreshold || gestureState.vx > HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD ); } - - _handlePanResponderEnd(event: PressEvent, gestureState: GestureState): void { - const horizontalDistance = IS_RTL ? -gestureState.dx : gestureState.dx; - if (this._isSwipingRightFromClosed(gestureState)) { - this.props.onOpen && this.props.onOpen(); - this._animateBounceBack(RIGHT_SWIPE_BOUNCE_BACK_DURATION); - } else if (this._shouldAnimateRemainder(gestureState)) { - if (horizontalDistance < 0) { - // Swiped left - this.props.onOpen && this.props.onOpen(); - this._animateToOpenPositionWith(gestureState.vx, horizontalDistance); - } else { - // Swiped right - this.props.onClose && this.props.onClose(); - this._animateToClosedPosition(); - } - } else { - if (this._previousLeft === CLOSED_LEFT_POSITION) { - this._animateToClosedPosition(); - } else { - this._animateToOpenPosition(); - } - } - - this.props.onSwipeEnd && this.props.onSwipeEnd(); - } } const styles = StyleSheet.create({