Support animation and gesture for Pager.

Summary: We need to support animation and gesture for Pager.

Reviewed By: ericvicenti

Differential Revision: D3066596

fb-gh-sync-id: 1c1a3d34b4298b4b0dd158f817057ae22dea72f4
shipit-source-id: 1c1a3d34b4298b4b0dd158f817057ae22dea72f4
This commit is contained in:
Hedger Wang 2016-03-23 12:49:28 -07:00 committed by Facebook Github Bot 9
parent 3c488afb0f
commit 4f8668b110
8 changed files with 424 additions and 60 deletions

View File

@ -33,9 +33,11 @@
'use strict';
const Animated = require('Animated');
const NavigationCardStackPanResponder = require('NavigationCardStackPanResponder');
const NavigationCardStackStyleInterpolator = require('NavigationCardStackStyleInterpolator');
const NavigationContainer = require('NavigationContainer');
const NavigationLinearPanResponder = require('NavigationLinearPanResponder');
const NavigationPagerPanResponder = require('NavigationPagerPanResponder');
const NavigationPagerStyleInterpolator = require('NavigationPagerStyleInterpolator');
const NavigationPropTypes = require('NavigationPropTypes');
const React = require('react-native');
const ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin');
@ -91,7 +93,7 @@ class NavigationCard extends React.Component<any, Props, any> {
}
if (panHandlers === undefined) {
// fall back to default pan handlers.
panHandlers = NavigationLinearPanResponder.forHorizontal(props);
panHandlers = NavigationCardStackPanResponder.forHorizontal(props);
}
return (
@ -119,4 +121,13 @@ const styles = StyleSheet.create({
},
});
module.exports = NavigationContainer.create(NavigationCard);
const NavigationCardContainer = NavigationContainer.create(NavigationCard);
// Export these buil-in interaction modules.
NavigationCardContainer.CardStackPanResponder = NavigationCardStackPanResponder;
NavigationCardContainer.CardStackStyleInterpolator = NavigationCardStackStyleInterpolator;
NavigationCardContainer.PagerPanResponder = NavigationPagerPanResponder;
NavigationCardContainer.PagerStyleInterpolator = NavigationPagerStyleInterpolator;
module.exports = NavigationCardContainer;

View File

@ -37,7 +37,7 @@ const NavigationAnimatedView = require('NavigationAnimatedView');
const NavigationCard = require('NavigationCard');
const NavigationCardStackStyleInterpolator = require('NavigationCardStackStyleInterpolator');
const NavigationContainer = require('NavigationContainer');
const NavigationLinearPanResponder = require('NavigationLinearPanResponder');
const NavigationCardStackPanResponder = require('NavigationCardStackPanResponder');
const NavigationPropTypes = require('NavigationPropTypes');
const React = require('React');
const ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin');
@ -46,7 +46,7 @@ const StyleSheet = require('StyleSheet');
const emptyFunction = require('fbjs/lib/emptyFunction');
const {PropTypes} = React;
const {Directions} = NavigationLinearPanResponder;
const {Directions} = NavigationCardStackPanResponder;
import type {
NavigationAnimatedValue,
@ -58,7 +58,7 @@ import type {
import type {
NavigationGestureDirection,
} from 'NavigationLinearPanResponder';
} from 'NavigationCardStackPanResponder';
type Props = {
direction: NavigationGestureDirection,
@ -80,7 +80,18 @@ const defaultProps = {
};
/**
* A controlled navigation view that renders a list of cards.
* A controlled navigation view that renders a stack of cards.
*
* +------------+
* +-+ |
* +-+ | |
* | | | |
* | | | Focused |
* | | | Card |
* | | | |
* +-+ | |
* +-+ |
* +------------+
*/
class NavigationCardStack extends React.Component {
_applyAnimation: NavigationAnimationSetter;
@ -123,8 +134,8 @@ class NavigationCardStack extends React.Component {
NavigationCardStackStyleInterpolator.forHorizontal(props);
const panHandlers = isVertical ?
NavigationLinearPanResponder.forVertical(props) :
NavigationLinearPanResponder.forHorizontal(props);
NavigationCardStackPanResponder.forVertical(props) :
NavigationCardStackPanResponder.forHorizontal(props);
return (
<NavigationCard

View File

@ -6,7 +6,7 @@
* 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.
*
* @providesModule NavigationLinearPanResponder
* @providesModule NavigationCardStackPanResponder
* @flow
* @typechecks
*/
@ -65,10 +65,20 @@ const Actions = {
};
/**
* Pan responder that handles the One-dimensional gesture (horizontal or
* vertical).
* Pan responder that handles gesture for a card in the cards stack.
*
* +------------+
* +-+ |
* +-+ | |
* | | | |
* | | | Focused |
* | | | Card |
* | | | |
* +-+ | |
* +-+ |
* +------------+
*/
class NavigationLinearPanResponder extends NavigationAbstractPanResponder {
class NavigationCardStackPanResponder extends NavigationAbstractPanResponder {
_isResponding: boolean;
_isVertical: boolean;
@ -181,7 +191,7 @@ function createPanHandlers(
direction: NavigationGestureDirection,
props: NavigationSceneRendererProps,
): NavigationPanPanHandlers {
const responder = new NavigationLinearPanResponder(direction, props);
const responder = new NavigationCardStackPanResponder(direction, props);
return responder.panHandlers;
}
@ -198,8 +208,17 @@ function forVertical(
}
module.exports = {
// constants
ANIMATION_DURATION,
DISTANCE_THRESHOLD,
POSITION_THRESHOLD,
RESPOND_THRESHOLD,
// enums
Actions,
Directions,
// methods.
forHorizontal,
forVertical,
};

View File

@ -32,15 +32,25 @@
*/
'use strict';
/**
* Predefined interpolator that renders the animated style for NavigationCard.
*
*/
import type {
NavigationSceneRendererProps,
} from 'NavigationTypeDefinition';
/**
* Utility that builds the style for the card in the cards stack.
*
* +------------+
* +-+ |
* +-+ | |
* | | | |
* | | | Focused |
* | | | Card |
* | | | |
* +-+ | |
* +-+ |
* +------------+
*/
function forHorizontal(props: NavigationSceneRendererProps): Object {
const {
layout,

View File

@ -39,7 +39,9 @@ const NavigationCard = require('NavigationCard');
const NavigationCardStackStyleInterpolator = require('NavigationCardStackStyleInterpolator');
const NavigationContext = require('NavigationContext');
const NavigationLegacyNavigatorRouteStack = require('NavigationLegacyNavigatorRouteStack');
const NavigationLinearPanResponder = require('NavigationLinearPanResponder');
const NavigationCardStackPanResponder = require('NavigationCardStackPanResponder');
const NavigationPagerPanResponder = require('NavigationPagerPanResponder');
const NavigationPagerStyleInterpolator = require('NavigationPagerStyleInterpolator');
const NavigatorBreadcrumbNavigationBar = require('NavigatorBreadcrumbNavigationBar');
const NavigatorNavigationBar = require('NavigatorNavigationBar');
const NavigatorSceneConfigs = require('NavigatorSceneConfigs');
@ -47,6 +49,7 @@ const React = require('react-native');
const ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin');
import type {
NavigationActionCaller,
NavigationAnimatedValue,
NavigationAnimationSetter,
NavigationParentState,
@ -73,15 +76,6 @@ type State = {
routeStack: Array<any>,
};
function getConfigPopDirection(config: any): ?string {
if (config && config.gestures && config.gestures.pop) {
const direction = config.gestures.pop.direction;
return direction ? String(direction) : null;
}
return null;
}
const RouteStack = NavigationLegacyNavigatorRouteStack;
/**
@ -108,6 +102,7 @@ class NavigationLegacyNavigator extends React.Component<any, Props, State> {
_renderScene: NavigationSceneRenderer;
_routeFocused: any;
_routeToFocus: any;
_onNavigate: NavigationActionCaller;
_stack: NavigationLegacyNavigatorRouteStack;
_useAnimation: boolean;
@ -206,6 +201,7 @@ class NavigationLegacyNavigator extends React.Component<any, Props, State> {
componentWillMount(): void {
this._applyAnimation = this._applyAnimation.bind(this);
this._onNavigate = this._onNavigate.bind(this);
this._onNavigationBarRef = this._onNavigationBarRef.bind(this);
this._onPositionChange = this._onPositionChange.bind(this);
this._renderCard = this._renderCard.bind(this);
@ -240,6 +236,7 @@ class NavigationLegacyNavigator extends React.Component<any, Props, State> {
<NavigationAnimatedView
applyAnimation={this._applyAnimation}
navigationState={this._stack.toNavigationState()}
onNavigate={this._onNavigate}
renderOverlay={this._renderHeader}
renderScene={this._renderCard}
style={this.props.style}
@ -285,37 +282,37 @@ class NavigationLegacyNavigator extends React.Component<any, Props, State> {
const {scene} = props;
const {configureScene} = this.props;
let isVertical = false;
// Default getters for style and pan responders.
let styleGetter = NavigationCardStackStyleInterpolator.forHorizontal;
let panResponderGetter = NavigationCardStackPanResponder.forHorizontal;
if (configureScene) {
const route = RouteStack.getRouteByNavigationState(scene.navigationState);
const config = configureScene(route, this.state.routeStack);
const direction = getConfigPopDirection(config);
switch (direction) {
case 'left-to-right':
// default.
break;
case 'top-to-bottom':
isVertical = true;
break;
default:
// unsupported config.
if (__DEV__) {
console.warn('unsupported scene configuration %s', direction);
}
if (config) {
const gestures = config.gestures || {};
if (gestures.pop && gestures.pop.direction === 'left-to-right') {
// pass, will use default getters.
} else if (gestures.pop && gestures.pop.direction === 'top-to-bottom') {
styleGetter = NavigationCardStackStyleInterpolator.forVertical;
panResponderGetter = NavigationCardStackPanResponder.forVertical;
} else if (
gestures.jumpBack &&
gestures.jumpForward &&
gestures.jumpBack.direction === 'left-to-right' &&
gestures.jumpForward.direction === 'right-to-left'
) {
styleGetter = NavigationPagerStyleInterpolator.forHorizontal;
panResponderGetter = NavigationPagerPanResponder.forHorizontal;
} else if (__DEV__) {
console.warn('unsupported scene configuration', config);
}
}
}
const style = isVertical ?
NavigationCardStackStyleInterpolator.forVertical(props) :
NavigationCardStackStyleInterpolator.forHorizontal(props);
const panHandlers = isVertical ?
NavigationLinearPanResponder.forVertical(props) :
NavigationLinearPanResponder.forHorizontal(props);
const style = styleGetter(props);
const panHandlers = panResponderGetter(props);
return (
<NavigationCard
@ -421,6 +418,24 @@ class NavigationLegacyNavigator extends React.Component<any, Props, State> {
this.navigationContext.emit('didfocus', {route: route});
this.props.onDidFocus && this.props.onDidFocus(route);
}
_onNavigate(action: any): void {
switch (action) {
case NavigationCardStackPanResponder.Actions.BACK:
this.pop();
break;
case NavigationPagerPanResponder.Actions.JUMP_BACK:
this.jumpBack();
break;
case NavigationPagerPanResponder.Actions.JUMP_FORWARD:
this.jumpForward();
break;
default:
if (__DEV__) {
console.warn('unsupported gesture action', action);
}
}
}
}
// Legacy static members.

View File

@ -0,0 +1,222 @@
/**
* 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.
*
* @providesModule NavigationPagerPanResponder
* @flow
* @typechecks
*/
'use strict';
const Animated = require('Animated');
const NavigationAbstractPanResponder = require('NavigationAbstractPanResponder');
const NavigationCardStackPanResponder = require('NavigationCardStackPanResponder');
const clamp = require('clamp');
import type {
NavigationPanPanHandlers,
NavigationSceneRendererProps,
} from 'NavigationTypeDefinition';
import type {
NavigationGestureDirection,
} from 'NavigationCardStackPanResponder';
/**
* Primitive gesture directions.
*/
const {
ANIMATION_DURATION,
DISTANCE_THRESHOLD,
POSITION_THRESHOLD,
RESPOND_THRESHOLD,
Directions,
} = NavigationCardStackPanResponder;
/**
* Primitive gesture actions.
*/
const Actions = {
JUMP_BACK: {type: 'jump_back'},
JUMP_FORWARD: {type: 'jump_forward'},
};
/**
* Pan responder that handles gesture for a card in the cards list.
*
* +-------------+-------------+-------------+
* | | | |
* | | | |
* | | | |
* | Next | Focused | Previous |
* | Card | Card | Card |
* | | | |
* | | | |
* | | | |
* +-------------+-------------+-------------+
*/
class NavigationPagerPanResponder extends NavigationAbstractPanResponder {
_isResponding: boolean;
_isVertical: boolean;
_props: NavigationSceneRendererProps;
_startValue: number;
constructor(
direction: NavigationGestureDirection,
props: NavigationSceneRendererProps,
) {
super();
this._isResponding = false;
this._isVertical = direction === Directions.VERTICAL;
this._props = props;
this._startValue = 0;
}
onMoveShouldSetPanResponder(event: any, gesture: any): boolean {
const props = this._props;
if (props.navigationState.index !== props.scene.index) {
return false;
}
const layout = props.layout;
const isVertical = this._isVertical;
const axis = isVertical ? 'dy' : 'dx';
const index = props.navigationState.index;
const distance = isVertical ?
layout.height.__getValue() :
layout.width.__getValue();
return (
Math.abs(gesture[axis]) > RESPOND_THRESHOLD &&
distance > 0 &&
index > 0
);
}
onPanResponderGrant(): void {
this._isResponding = false;
this._props.position.stopAnimation((value: number) => {
this._isResponding = true;
this._startValue = value;
});
}
onPanResponderMove(event: any, gesture: any): void {
if (!this._isResponding) {
return;
}
const {
layout,
navigationState,
position,
scenes,
} = this._props;
const isVertical = this._isVertical;
const axis = isVertical ? 'dy' : 'dx';
const index = navigationState.index;
const distance = isVertical ?
layout.height.__getValue() :
layout.width.__getValue();
const prevIndex = Math.max(
0,
index - 1,
);
const nextIndex = Math.min(
index + 1,
scenes.length - 1,
);
const value = clamp(
prevIndex,
this._startValue - (gesture[axis] / distance),
nextIndex,
);
position.setValue(value);
}
onPanResponderRelease(event: any, gesture: any): void {
if (!this._isResponding) {
return;
}
this._isResponding = false;
const {
navigationState,
onNavigate,
position,
} = this._props;
const isVertical = this._isVertical;
const axis = isVertical ? 'dy' : 'dx';
const index = navigationState.index;
const distance = gesture[axis];
position.stopAnimation((value: number) => {
this._reset();
if (
distance > DISTANCE_THRESHOLD ||
value <= index - POSITION_THRESHOLD
) {
onNavigate(Actions.JUMP_BACK);
return;
}
if (
distance < -DISTANCE_THRESHOLD ||
value >= index + POSITION_THRESHOLD
) {
onNavigate(Actions.JUMP_FORWARD);
}
});
}
onPanResponderTerminate(): void {
this._isResponding = false;
this._reset();
}
_reset(): void {
const props = this._props;
Animated.timing(
props.position,
{
toValue: props.navigationState.index,
duration: ANIMATION_DURATION,
}
).start();
}
}
function createPanHandlers(
direction: NavigationGestureDirection,
props: NavigationSceneRendererProps,
): NavigationPanPanHandlers {
const responder = new NavigationPagerPanResponder(direction, props);
return responder.panHandlers;
}
function forHorizontal(
props: NavigationSceneRendererProps,
): NavigationPanPanHandlers {
return createPanHandlers(Directions.HORIZONTAL, props);
}
module.exports = {
Actions,
forHorizontal,
};

View File

@ -0,0 +1,84 @@
/**
* 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.
*
* Facebook, Inc. ("Facebook") owns all right, title and interest, including
* all intellectual property and other proprietary rights, in and to the React
* Native CustomComponents software (the "Software"). Subject to your
* compliance with these terms, you are hereby granted a non-exclusive,
* worldwide, royalty-free copyright license to (1) use and copy the Software;
* and (2) reproduce and distribute the Software as part of your own software
* ("Your Software"). Facebook reserves all rights not expressly granted to
* you in this license agreement.
*
* THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS
* OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED.
* IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR
* EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @providesModule NavigationPagerStyleInterpolator
* @flow
*/
'use strict';
import type {
NavigationSceneRendererProps,
} from 'NavigationTypeDefinition';
/**
* Utility that builds the style for the card in the cards list.
*
* +-------------+-------------+-------------+
* | | | |
* | | | |
* | | | |
* | Next | Focused | Previous |
* | Card | Card | Card |
* | | | |
* | | | |
* | | | |
* +-------------+-------------+-------------+
*/
function forHorizontal(props: NavigationSceneRendererProps): Object {
const {
layout,
position,
scene,
} = props;
const index = scene.index;
const inputRange = [index - 1, index, index + 1];
const width = layout.initWidth;
const translateX = position.interpolate({
inputRange,
outputRange: [width, 0, -width],
});
return {
opacity : 1,
shadowColor: 'transparent',
shadowRadius: 0,
transform: [
{ scale: 1 },
{ translateX },
{ translateY: 0 },
],
};
}
module.exports = {
forHorizontal,
};

View File

@ -14,11 +14,9 @@
const NavigationAnimatedView = require('NavigationAnimatedView');
const NavigationCard = require('NavigationCard');
const NavigationCardStack = require('NavigationCardStack');
const NavigationCardStackStyleInterpolator = require('NavigationCardStackStyleInterpolator');
const NavigationContainer = require('NavigationContainer');
const NavigationHeader = require('NavigationHeader');
const NavigationLegacyNavigator = require('NavigationLegacyNavigator');
const NavigationLinearPanResponder = require('NavigationLinearPanResponder');
const NavigationReducer = require('NavigationReducer');
const NavigationRootContainer = require('NavigationRootContainer');
const NavigationStateUtils = require('NavigationStateUtils');
@ -42,12 +40,6 @@ const NavigationExperimental = {
CardStack: NavigationCardStack,
Header: NavigationHeader,
LegacyNavigator: NavigationLegacyNavigator,
// Animations Style Interpolators:
CardStackStyleInterpolator: NavigationCardStackStyleInterpolator,
// Interactions:
LinearPanResponder: NavigationLinearPanResponder,
};
module.exports = NavigationExperimental;