diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationCardStackExample.js b/Examples/UIExplorer/NavigationExperimental/NavigationCardStackExample.js
index ae08ac75d..e7feb7c60 100644
--- a/Examples/UIExplorer/NavigationExperimental/NavigationCardStackExample.js
+++ b/Examples/UIExplorer/NavigationExperimental/NavigationCardStackExample.js
@@ -14,6 +14,7 @@
'use strict';
const NavigationExampleRow = require('./NavigationExampleRow');
+const NavigationRootContainer = require('NavigationRootContainer');
const React = require('react-native');
const {
@@ -25,62 +26,68 @@ const {
const NavigationCardStack = NavigationExperimental.CardStack;
const NavigationStateUtils = NavigationExperimental.StateUtils;
+function reduceNavigationState(initialState) {
+ return (currentState, action) => {
+ switch (action.type) {
+ case 'RootContainerInitialAction':
+ return initialState;
+
+ case 'push':
+ return NavigationStateUtils.push(currentState, {key: action.key});
+
+ case 'back':
+ case 'pop':
+ return currentState.index > 0 ?
+ NavigationStateUtils.pop(currentState) :
+ currentState;
+
+ default:
+ return currentState;
+ }
+ };
+}
+
+const ExampleReducer = reduceNavigationState({
+ index: 0,
+ children: [{key: 'First Route'}],
+});
+
class NavigationCardStackExample extends React.Component {
constructor(props, context) {
super(props, context);
- this.state = this._getInitialState();
+
+ this._renderNavigation = this._renderNavigation.bind(this);
this._renderScene = this._renderScene.bind(this);
- this._push = this._push.bind(this);
- this._pop = this._pop.bind(this);
this._toggleDirection = this._toggleDirection.bind(this);
+
+ this.state = {isHorizontal: true};
}
render() {
+ return (
+
+ );
+ }
+
+ _renderNavigation(navigationState, onNavigate) {
return (
);
}
- _getInitialState() {
- const navigationState = {
- index: 0,
- children: [{key: 'First Route'}],
- };
- return {
- isHorizontal: true,
- navigationState,
- };
- }
-
- _push() {
- const state = this.state.navigationState;
- const nextState = NavigationStateUtils.push(
- state,
- {key: 'Route ' + (state.index + 1)},
- );
- this.setState({
- navigationState: nextState,
- });
- }
-
- _pop() {
- const state = this.state.navigationState;
- const nextState = state.index > 0 ?
- NavigationStateUtils.pop(state) :
- state;
-
- this.setState({
- navigationState: nextState,
- });
- }
-
_renderScene(props) {
+ const {navigationParentState, onNavigate} = props;
return (
{
+ onNavigate({
+ type: 'push',
+ key: 'Route ' + navigationParentState.children.length,
+ });
+ }}
/>
{
+ onNavigate({
+ type: 'pop',
+ });
+ }}
/>
.
*/
@@ -119,9 +160,8 @@ class NavigationCardStackItem extends React.Component {
const {
direction,
index,
- navigationState,
+ navigationParentState,
position,
- layout,
} = this.props;
const {
height,
@@ -161,8 +201,16 @@ class NavigationCardStackItem extends React.Component {
],
};
+ let panHandlers = null;
+ if (navigationParentState.index === index) {
+ const delegate = new PanResponderDelegate(this.props);
+ const panResponder = new NavigationLinearPanResponder(delegate);
+ panHandlers = panResponder.panHandlers;
+ }
+
return (
{this.props.renderScene(this.props)}
@@ -200,16 +248,12 @@ class NavigationCardStackItem extends React.Component {
}
}
-const Directions = {
- HORIZONTAL: 'horizontal',
- VERTICAL: 'vertical',
-};
-
NavigationCardStackItem.propTypes = {
direction: PropTypes.oneOf([Directions.HORIZONTAL, Directions.VERTICAL]),
index: PropTypes.number.isRequired,
layout: PropTypes.object.isRequired,
navigationState: PropTypes.object.isRequired,
+ navigationParentState: PropTypes.object.isRequired,
position: PropTypes.object.isRequired,
renderScene: PropTypes.func.isRequired,
};
@@ -218,10 +262,6 @@ NavigationCardStackItem.defaultProps = {
direction: Directions.HORIZONTAL,
};
-NavigationCardStackItem = NavigationContainer.create(NavigationCardStackItem);
-
-NavigationCardStackItem.Directions = Directions;
-
const styles = StyleSheet.create({
main: {
backgroundColor: '#E9E9EF',
@@ -237,6 +277,4 @@ const styles = StyleSheet.create({
},
});
-
-
-module.exports = NavigationCardStackItem;
+module.exports = NavigationContainer.create(NavigationCardStackItem);
diff --git a/Libraries/NavigationExperimental/NavigationAbstractPanResponder.js b/Libraries/NavigationExperimental/NavigationAbstractPanResponder.js
new file mode 100644
index 000000000..d6000a4ce
--- /dev/null
+++ b/Libraries/NavigationExperimental/NavigationAbstractPanResponder.js
@@ -0,0 +1,47 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ *
+ * @providesModule NavigationAbstractPanResponder
+ * @flow
+ */
+'use strict';
+
+const PanResponder = require('PanResponder');
+
+const invariant = require('invariant');
+
+const EmptyPanHandlers = {
+ onMoveShouldSetPanResponder: null,
+ onPanResponderGrant: null,
+ onPanResponderMove: null,
+ onPanResponderRelease: null,
+ onPanResponderTerminate: null,
+};
+
+/**
+ * Abstract class that defines the common interface of PanResponder that handles
+ * the gesture actions.
+ */
+class NavigationAbstractPanResponder {
+
+ panHandlers: Object;
+
+ constructor() {
+ const config = {};
+ Object.keys(EmptyPanHandlers).forEach(name => {
+ const fn: any = (this: any)[name];
+
+ invariant(
+ typeof fn === 'function',
+ 'subclass of `NavigationAbstractPanResponder` must implement method %s',
+ name
+ );
+
+ config[name] = fn.bind(this);
+ }, this);
+
+ this.panHandlers = PanResponder.create(config).panHandlers;
+ }
+}
+
+module.exports = NavigationAbstractPanResponder;
diff --git a/Libraries/NavigationExperimental/NavigationAnimatedView.js b/Libraries/NavigationExperimental/NavigationAnimatedView.js
index bf3024ce8..4d1f5b68f 100644
--- a/Libraries/NavigationExperimental/NavigationAnimatedView.js
+++ b/Libraries/NavigationExperimental/NavigationAnimatedView.js
@@ -87,6 +87,8 @@ type NavigationStateRendererProps = {
layout: Layout,
// The state of the the containing navigation view.
navigationParentState: NavigationParentState,
+
+ onNavigate: (action: any) => void,
};
type NavigationStateRenderer = (
@@ -100,11 +102,12 @@ type TimingSetter = (
) => void;
type Props = {
- navigationState: NavigationParentState;
- renderScene: NavigationStateRenderer;
- renderOverlay: ?NavigationStateRenderer;
- style: any;
- setTiming: ?TimingSetter;
+ navigationState: NavigationParentState,
+ onNavigate: (action: any) => void,
+ renderScene: NavigationStateRenderer,
+ renderOverlay: ?NavigationStateRenderer,
+ style: any,
+ setTiming: ?TimingSetter,
};
class NavigationAnimatedView extends React.Component {
@@ -224,18 +227,23 @@ class NavigationAnimatedView extends React.Component {
layout: this._getLayout(),
navigationParentState: this.props.navigationState,
navigationState: scene.state,
+ onNavigate: this.props.onNavigate,
position: this.state.position,
});
}
_renderOverlay() {
- const {renderOverlay} = this.props;
+ const {
+ onNavigate,
+ renderOverlay,
+ navigationState,
+ } = this.props;
if (renderOverlay) {
- const parentState = this.props.navigationState;
return renderOverlay({
- index: parentState.index,
+ index: navigationState.index,
layout: this._getLayout(),
- navigationParentState: parentState,
- navigationState: parentState.children[parentState.index],
+ navigationParentState: navigationState,
+ navigationState: navigationState.children[navigationState.index],
+ onNavigate: onNavigate,
position: this.state.position,
});
}
diff --git a/Libraries/NavigationExperimental/NavigationLinearPanResponder.js b/Libraries/NavigationExperimental/NavigationLinearPanResponder.js
new file mode 100644
index 000000000..704f305c7
--- /dev/null
+++ b/Libraries/NavigationExperimental/NavigationLinearPanResponder.js
@@ -0,0 +1,183 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ *
+ * @providesModule NavigationLinearPanResponder
+ * @flow
+ * @typechecks
+ */
+'use strict';
+
+const Animated = require('Animated');
+const NavigationAbstractPanResponder = require('NavigationAbstractPanResponder');
+
+const clamp = require('clamp');
+
+/**
+ * The duration of the card animation in milliseconds.
+ */
+const ANIMATION_DURATION = 250;
+
+/**
+ * The threshold to invoke the `onNavigate` action.
+ * For instance, `1 / 3` means that moving greater than 1 / 3 of the width of
+ * the view will navigate.
+ */
+const POSITION_THRESHOLD = 1 / 3;
+
+/**
+ * The threshold (in pixels) to start the gesture action.
+ */
+const RESPOND_THRESHOLD = 15;
+
+/**
+ * The threshold (in speed) to finish the gesture action.
+ */
+const VELOCITY_THRESHOLD = 100;
+
+/**
+ * Primitive gesture directions.
+ */
+const Directions = {
+ 'HORIZONTAL': 'horizontal',
+ 'VERTICAL': 'vertical',
+};
+
+/**
+ * Primitive gesture actions.
+ */
+const Actions = {
+ // The gesture to navigate backward.
+ // This is done by swiping from the left to the right or from the top to the
+ // bottom.
+ BACK: {type: 'back'},
+};
+
+import type {
+ Layout,
+ Position,
+} from 'NavigationAnimatedView';
+
+export type OnNavigateHandler = (action: {type: string}) => void;
+
+export type Direction = $Enum;
+
+/**
+ * The type interface of the object that provides the information required by
+ * NavigationLinearPanResponder.
+ */
+export type NavigationLinearPanResponderDelegate = {
+ getDirection: () => Direction;
+ getIndex: () => number,
+ getLayout: () => Layout,
+ getPosition: () => Position,
+ onNavigate: OnNavigateHandler,
+};
+
+/**
+ * Pan responder that handles the One-dimensional gesture (horizontal or
+ * vertical).
+ */
+class NavigationLinearPanResponder extends NavigationAbstractPanResponder {
+ static Actions: Object;
+ static Directions: Object;
+
+ _isResponding: boolean;
+ _startValue: number;
+ _delegate: NavigationLinearPanResponderDelegate;
+
+ constructor(delegate: NavigationLinearPanResponderDelegate) {
+ super();
+ this._isResponding = false;
+ this._startValue = 0;
+ this._delegate = delegate;
+ }
+
+ onMoveShouldSetPanResponder(event: any, gesture: any): boolean {
+ const delegate = this._delegate;
+ const layout = delegate.getLayout();
+ const isVertical = delegate.getDirection() === Directions.VERTICAL;
+ const axis = isVertical ? 'dy' : 'dx';
+ const index = delegate.getIndex();
+ 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._delegate.getPosition().stopAnimation((value: number) => {
+ this._isResponding = true;
+ this._startValue = value;
+ });
+ }
+
+ onPanResponderMove(event: any, gesture: any): void {
+ if (!this._isResponding) {
+ return;
+ }
+
+ const delegate = this._delegate;
+ const layout = delegate.getLayout();
+ const isVertical = delegate.getDirection() === Directions.VERTICAL;
+ const axis = isVertical ? 'dy' : 'dx';
+ const index = delegate.getIndex();
+ const distance = isVertical ?
+ layout.height.__getValue() :
+ layout.width.__getValue();
+
+ const value = clamp(
+ index - 1,
+ this._startValue - (gesture[axis] / distance),
+ index
+ );
+
+ this._delegate.getPosition().setValue(value);
+ }
+
+ onPanResponderRelease(event: any, gesture: any): void {
+ if (!this._isResponding) {
+ return;
+ }
+
+ this._isResponding = false;
+
+ const delegate = this._delegate;
+ const isVertical = delegate.getDirection() === Directions.VERTICAL;
+ const axis = isVertical ? 'dy' : 'dx';
+ const index = delegate.getIndex();
+ const velocity = gesture[axis];
+
+ delegate.getPosition().stopAnimation((value: number) => {
+ this._reset();
+ if (velocity > VELOCITY_THRESHOLD || value <= index - POSITION_THRESHOLD) {
+ delegate.onNavigate(Actions.BACK);
+ }
+ });
+ }
+
+ onPanResponderTerminate(): void {
+ this._isResponding = false;
+ this._reset();
+ }
+
+ _reset(): void {
+ Animated.timing(
+ this._delegate.getPosition(),
+ {
+ toValue: this._delegate.getIndex(),
+ duration: ANIMATION_DURATION,
+ }
+ ).start();
+ }
+}
+
+NavigationLinearPanResponder.Actions = Actions;
+NavigationLinearPanResponder.Directions = Directions;
+
+module.exports = NavigationLinearPanResponder;