2
0
mirror of https://github.com/status-im/react-native.git synced 2025-01-18 13:31:18 +00:00

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

@ -33,9 +33,11 @@
'use strict'; 'use strict';
const Animated = require('Animated'); const Animated = require('Animated');
const NavigationCardStackPanResponder = require('NavigationCardStackPanResponder');
const NavigationCardStackStyleInterpolator = require('NavigationCardStackStyleInterpolator'); const NavigationCardStackStyleInterpolator = require('NavigationCardStackStyleInterpolator');
const NavigationContainer = require('NavigationContainer'); const NavigationContainer = require('NavigationContainer');
const NavigationLinearPanResponder = require('NavigationLinearPanResponder'); const NavigationPagerPanResponder = require('NavigationPagerPanResponder');
const NavigationPagerStyleInterpolator = require('NavigationPagerStyleInterpolator');
const NavigationPropTypes = require('NavigationPropTypes'); const NavigationPropTypes = require('NavigationPropTypes');
const React = require('react-native'); const React = require('react-native');
const ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin'); const ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin');
@ -91,7 +93,7 @@ class NavigationCard extends React.Component<any, Props, any> {
} }
if (panHandlers === undefined) { if (panHandlers === undefined) {
// fall back to default pan handlers. // fall back to default pan handlers.
panHandlers = NavigationLinearPanResponder.forHorizontal(props); panHandlers = NavigationCardStackPanResponder.forHorizontal(props);
} }
return ( 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;

@ -37,7 +37,7 @@ const NavigationAnimatedView = require('NavigationAnimatedView');
const NavigationCard = require('NavigationCard'); const NavigationCard = require('NavigationCard');
const NavigationCardStackStyleInterpolator = require('NavigationCardStackStyleInterpolator'); const NavigationCardStackStyleInterpolator = require('NavigationCardStackStyleInterpolator');
const NavigationContainer = require('NavigationContainer'); const NavigationContainer = require('NavigationContainer');
const NavigationLinearPanResponder = require('NavigationLinearPanResponder'); const NavigationCardStackPanResponder = require('NavigationCardStackPanResponder');
const NavigationPropTypes = require('NavigationPropTypes'); const NavigationPropTypes = require('NavigationPropTypes');
const React = require('React'); const React = require('React');
const ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin'); const ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin');
@ -46,7 +46,7 @@ const StyleSheet = require('StyleSheet');
const emptyFunction = require('fbjs/lib/emptyFunction'); const emptyFunction = require('fbjs/lib/emptyFunction');
const {PropTypes} = React; const {PropTypes} = React;
const {Directions} = NavigationLinearPanResponder; const {Directions} = NavigationCardStackPanResponder;
import type { import type {
NavigationAnimatedValue, NavigationAnimatedValue,
@ -58,7 +58,7 @@ import type {
import type { import type {
NavigationGestureDirection, NavigationGestureDirection,
} from 'NavigationLinearPanResponder'; } from 'NavigationCardStackPanResponder';
type Props = { type Props = {
direction: NavigationGestureDirection, 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 { class NavigationCardStack extends React.Component {
_applyAnimation: NavigationAnimationSetter; _applyAnimation: NavigationAnimationSetter;
@ -123,8 +134,8 @@ class NavigationCardStack extends React.Component {
NavigationCardStackStyleInterpolator.forHorizontal(props); NavigationCardStackStyleInterpolator.forHorizontal(props);
const panHandlers = isVertical ? const panHandlers = isVertical ?
NavigationLinearPanResponder.forVertical(props) : NavigationCardStackPanResponder.forVertical(props) :
NavigationLinearPanResponder.forHorizontal(props); NavigationCardStackPanResponder.forHorizontal(props);
return ( return (
<NavigationCard <NavigationCard

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

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

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

@ -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,
};

@ -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,
};

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