NUX-y bounce
Summary: On mount, bounce the 1st row so users know it's swipeable. Reviewed By: fkgozali Differential Revision: D3395214 fbshipit-source-id: 6d391209014a6a7957a2160734d8ef6548b7693b
This commit is contained in:
parent
79dcbc7b29
commit
4868948175
|
@ -47,8 +47,10 @@ const SwipeableListView = React.createClass({
|
|||
},
|
||||
|
||||
_listViewRef: (null: ?string),
|
||||
_shouldBounceFirstRowOnMount: false,
|
||||
|
||||
propTypes: {
|
||||
bounceFirstRowOnMount: PropTypes.bool.isRequired,
|
||||
dataSource: PropTypes.instanceOf(SwipeableListViewDataSource).isRequired,
|
||||
maxSwipeDistance: PropTypes.number,
|
||||
// Callback method to render the swipeable view
|
||||
|
@ -59,6 +61,7 @@ const SwipeableListView = React.createClass({
|
|||
|
||||
getDefaultProps(): Object {
|
||||
return {
|
||||
bounceFirstRowOnMount: false,
|
||||
renderQuickActions: () => null,
|
||||
};
|
||||
},
|
||||
|
@ -69,6 +72,10 @@ const SwipeableListView = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount(): void {
|
||||
this._shouldBounceFirstRowOnMount = this.props.bounceFirstRowOnMount;
|
||||
},
|
||||
|
||||
componentWillReceiveProps(nextProps: Object): void {
|
||||
if (
|
||||
this.state.dataSource.getDataSource() !== nextProps.dataSource.getDataSource()
|
||||
|
@ -114,7 +121,11 @@ const SwipeableListView = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_renderRow(rowData: Object, sectionID: string, rowID: string): ReactElement<any> {
|
||||
_renderRow(
|
||||
rowData: Object,
|
||||
sectionID: string,
|
||||
rowID: string,
|
||||
): ReactElement<any> {
|
||||
const slideoutView = this.props.renderQuickActions(rowData, sectionID, rowID);
|
||||
|
||||
// If renderRowSlideout is unspecified or returns falsey, don't allow swipe
|
||||
|
@ -122,6 +133,12 @@ const SwipeableListView = React.createClass({
|
|||
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}
|
||||
|
@ -130,7 +147,8 @@ const SwipeableListView = React.createClass({
|
|||
key={rowID}
|
||||
onOpen={() => this._onOpen(rowData.id)}
|
||||
onSwipeEnd={() => this._setListViewScrollable(true)}
|
||||
onSwipeStart={() => this._setListViewScrollable(false)}>
|
||||
onSwipeStart={() => this._setListViewScrollable(false)}
|
||||
shouldBounceOnMount={shouldBounceOnMount}>
|
||||
{this.props.renderRow(rowData, sectionID, rowID)}
|
||||
</SwipeableRow>
|
||||
);
|
||||
|
|
|
@ -88,6 +88,18 @@ class SwipeableListViewDataSource {
|
|||
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];
|
||||
}
|
||||
|
||||
setOpenRowID(rowID: string): SwipeableListViewDataSource {
|
||||
this._previousOpenRowID = this._openRowID;
|
||||
this._openRowID = rowID;
|
||||
|
|
|
@ -27,28 +27,41 @@ const Animated = require('Animated');
|
|||
const PanResponder = require('PanResponder');
|
||||
const React = require('React');
|
||||
const StyleSheet = require('StyleSheet');
|
||||
const TimerMixin = require('react-timer-mixin');
|
||||
const View = require('View');
|
||||
|
||||
const {PropTypes} = React;
|
||||
|
||||
const emptyFunction = require('fbjs/lib/emptyFunction');
|
||||
|
||||
// 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 = 15;
|
||||
// Distance left of closed position to bounce back when right-swiping from closed
|
||||
const RIGHT_SWIPE_BOUNCE_BACK_DISTANCE = 30;
|
||||
// Minimum swipe speed before we fully animate the user's action (open/close)
|
||||
const HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD = 0.5;
|
||||
// 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 = 200;
|
||||
|
||||
/**
|
||||
* 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 = 200;
|
||||
|
||||
// Distance left of closed position to bounce back when right-swiping from closed
|
||||
const RIGHT_SWIPE_BOUNCE_BACK_DISTANCE = 30;
|
||||
/**
|
||||
* 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;
|
||||
// Time, in milliseconds, of how long the animated swipe should be
|
||||
const SWIPE_DURATION = 200;
|
||||
|
||||
/**
|
||||
* Creates a swipable row that allows taps on the main item and a custom View
|
||||
|
@ -58,12 +71,16 @@ const SwipeableRow = React.createClass({
|
|||
_panResponder: {},
|
||||
_previousLeft: CLOSED_LEFT_POSITION,
|
||||
|
||||
mixins: [TimerMixin],
|
||||
|
||||
propTypes: {
|
||||
isOpen: PropTypes.bool,
|
||||
maxSwipeDistance: PropTypes.number.isRequired,
|
||||
onOpen: 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
|
||||
*/
|
||||
|
@ -116,6 +133,18 @@ const SwipeableRow = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
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);
|
||||
}
|
||||
},
|
||||
|
||||
componentWillReceiveProps(nextProps: Object): void {
|
||||
/**
|
||||
* We do not need an "animateOpen(noCallback)" because this animation is
|
||||
|
@ -126,6 +155,15 @@ const SwipeableRow = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
shouldComponentUpdate(nextProps: Object, nextState: Object): boolean {
|
||||
if (this.props.shouldBounceOnMount && !nextProps.shouldBounceOnMount) {
|
||||
// No need to rerender if SwipeableListView is disabling the bounce flag
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
render(): ReactElement<any> {
|
||||
// The view hidden behind the main view
|
||||
let slideOutView;
|
||||
|
@ -231,12 +269,16 @@ const SwipeableRow = React.createClass({
|
|||
return false;
|
||||
},
|
||||
|
||||
_animateTo(toValue: number, callback: Function = emptyFunction): void {
|
||||
_animateTo(
|
||||
toValue: number,
|
||||
duration: number = SWIPE_DURATION,
|
||||
callback: Function = emptyFunction,
|
||||
): void {
|
||||
Animated.timing(
|
||||
this.state.currentLeft,
|
||||
{
|
||||
duration: SWIPE_DURATION,
|
||||
toValue: toValue,
|
||||
duration,
|
||||
toValue,
|
||||
},
|
||||
).start(() => {
|
||||
this._previousLeft = toValue;
|
||||
|
@ -252,13 +294,14 @@ const SwipeableRow = React.createClass({
|
|||
this._animateTo(CLOSED_LEFT_POSITION);
|
||||
},
|
||||
|
||||
_animateRightSwipeBounceBack(): void {
|
||||
_animateBounceBack(duration: number = SWIPE_DURATION): void {
|
||||
/**
|
||||
* When swiping right, we want to bounce back past closed position on release
|
||||
* so users know they should swipe right to get content.
|
||||
*/
|
||||
this._animateTo(
|
||||
-RIGHT_SWIPE_BOUNCE_BACK_DISTANCE,
|
||||
duration,
|
||||
this._animateToClosedPosition,
|
||||
);
|
||||
},
|
||||
|
@ -268,15 +311,24 @@ const SwipeableRow = React.createClass({
|
|||
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 = gestureState.dx;
|
||||
|
||||
if (this._isSwipingRightFromClosed(gestureState)) {
|
||||
this.props.onOpen();
|
||||
this._animateRightSwipeBounceBack();
|
||||
} else if (Math.abs(horizontalDistance) > this.props.swipeThreshold) {
|
||||
// Overswiped
|
||||
|
||||
this._animateBounceBack();
|
||||
} else if (this._shouldAnimateRemainder(gestureState)) {
|
||||
if (horizontalDistance < 0) {
|
||||
// Swiped left
|
||||
this.props.onOpen();
|
||||
|
@ -286,8 +338,6 @@ const SwipeableRow = React.createClass({
|
|||
this._animateToClosedPosition();
|
||||
}
|
||||
} else {
|
||||
// Swiping from closed but let go before fully
|
||||
|
||||
if (this._previousLeft === CLOSED_LEFT_POSITION) {
|
||||
this._animateToClosedPosition();
|
||||
} else {
|
||||
|
|
Loading…
Reference in New Issue