diff --git a/Examples/UIExplorer/JSNavigationStack/BreadcrumbNavSample.js b/Examples/UIExplorer/JSNavigationStack/BreadcrumbNavSample.js
new file mode 100644
index 000000000..7be93a83a
--- /dev/null
+++ b/Examples/UIExplorer/JSNavigationStack/BreadcrumbNavSample.js
@@ -0,0 +1,279 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ *
+ * @providesModule BreadcrumbNavSample
+ */
+'use strict';
+
+var BreadcrumbNavigationBar = require('BreadcrumbNavigationBar');
+var JSNavigationStack = require('JSNavigationStack');
+var React = require('React');
+var StyleSheet = require('StyleSheet');
+var ScrollView = require('ScrollView');
+var TabBarItemIOS = require('TabBarItemIOS');
+var TabBarIOS = require('TabBarIOS');
+var Text = require('Text');
+var TouchableBounce = require('TouchableBounce');
+var View = require('View');
+
+
+
+var SAMPLE_TEXT = 'Top Pushes. Middle Replaces. Bottom Pops.';
+
+var _getRandomRoute = function() {
+ return {
+ backButtonTitle: 'Back' + ('' + 10 * Math.random()).substr(0, 1),
+ content:
+ SAMPLE_TEXT + '\nHere\'s a random number ' + Math.random(),
+ title: Math.random() > 0.5 ? 'Hello' : 'There',
+ rightButtonTitle: Math.random() > 0.5 ? 'Right' : 'Button',
+ };
+};
+
+
+var SampleNavigationBarRouteMapper = {
+ rightContentForRoute: function(route, navigationOperations) {
+ if (route.rightButtonTitle) {
+ return (
+
+ {route.rightButtonTitle}
+
+ );
+ } else {
+ return null;
+ }
+ },
+ titleContentForRoute: function(route, navigationOperations) {
+ return (
+ navigationOperations.push(_getRandomRoute())}>
+
+ {route.title}
+
+
+ );
+ },
+ iconForRoute: function(route, navigationOperations) {
+ var onPress =
+ navigationOperations.popToRoute.bind(navigationOperations, route);
+ return (
+
+
+
+ );
+ },
+ separatorForRoute: function(route, navigationOperations) {
+ return (
+
+
+
+ );
+ }
+};
+
+var SampleRouteMapper = {
+
+ delay: 400, // Just to test for race conditions with native nav.
+
+ navigationItemForRoute: function(route, navigationOperations) {
+ var content = route.content;
+ return (
+
+
+
+
+ request push soon
+
+
+
+
+ {content}
+
+
+
+
+ {content}
+
+
+
+
+ {content}
+
+
+
+
+ {content}
+
+
+
+
+ {content}
+
+
+
+
+ request pop soon
+
+
+
+
+ Immediate set two routes
+
+
+
+
+ pop to top soon
+
+
+
+
+ );
+ },
+
+ _popToTopLater: function(popToTop) {
+ return () => setTimeout(popToTop, SampleRouteMapper.delay);
+ },
+
+ _pushRouteLater: function(push) {
+ return () => setTimeout(
+ () => push(_getRandomRoute()),
+ SampleRouteMapper.delay
+ );
+ },
+
+ _immediatelySetTwoItemsLater: function(immediatelyResetRouteStack) {
+ return () => setTimeout(
+ () => immediatelyResetRouteStack([
+ _getRandomRoute(),
+ _getRandomRoute(),
+ ])
+ );
+ },
+
+ _popRouteLater: function(pop) {
+ return () => setTimeout(pop, SampleRouteMapper.delay);
+ },
+};
+
+var BreadcrumbNavSample = React.createClass({
+
+ getInitialState: function() {
+ return {
+ selectedTab: 0,
+ };
+ },
+
+ render: function() {
+ var initialRoute = {
+ backButtonTitle: 'Start', // no back button for initial scene
+ content: SAMPLE_TEXT,
+ title: 'Campaigns',
+ rightButtonTitle: 'Filter',
+ };
+ return (
+
+
+
+ }
+ />
+
+
+ JSNavigationStack.AnimationConfigs.FloatFromBottom}
+ debugOverlay={false}
+ style={[styles.appContainer]}
+ initialRoute={initialRoute}
+ routeMapper={SampleRouteMapper}
+ navigationBar={
+
+ }
+ />
+
+
+ );
+ },
+
+ onTabSelect: function(tab, event) {
+ if (this.state.selectedTab !== tab) {
+ this.setState({selectedTab: tab});
+ }
+ },
+
+});
+
+var styles = StyleSheet.create({
+ navigationItem: {
+ backgroundColor: '#eeeeee',
+ },
+ scene: {
+ paddingTop: 50,
+ flex: 1,
+ },
+ button: {
+ backgroundColor: '#cccccc',
+ margin: 50,
+ marginTop: 26,
+ padding: 10,
+ },
+ buttonText: {
+ fontSize: 12,
+ textAlign: 'center',
+ },
+ appContainer: {
+ overflow: 'hidden',
+ backgroundColor: '#dddddd',
+ flex: 1,
+ },
+ titleText: {
+ fontSize: 18,
+ color: '#666666',
+ textAlign: 'center',
+ fontWeight: 'bold',
+ lineHeight: 32,
+ },
+ filterText: {
+ color: '#5577ff',
+ },
+ // TODO: Accept icons from route.
+ crumbIconPlaceholder: {
+ flex: 1,
+ backgroundColor: '#666666',
+ },
+ crumbSeparatorPlaceholder: {
+ flex: 1,
+ backgroundColor: '#aaaaaa',
+ },
+});
+
+module.exports = BreadcrumbNavSample;
diff --git a/Examples/UIExplorer/JSNavigationStack/JSNavigationStackExample.js b/Examples/UIExplorer/JSNavigationStack/JSNavigationStackExample.js
new file mode 100644
index 000000000..4e99a7ced
--- /dev/null
+++ b/Examples/UIExplorer/JSNavigationStack/JSNavigationStackExample.js
@@ -0,0 +1,101 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ */
+'use strict';
+
+var React = require('React');
+var JSNavigationStack = require('JSNavigationStack');
+var StyleSheet = require('StyleSheet');
+var Text = require('Text');
+var ScrollView = require('ScrollView');
+var TouchableHighlight = require('TouchableHighlight');
+var BreadcrumbNavSample = require('./BreadcrumbNavSample');
+var NavigationBarSample = require('./NavigationBarSample');
+var JumpingNavSample = require('./JumpingNavSample');
+
+class NavMenu extends React.Component {
+ render() {
+ return (
+
+ {
+ this.props.navigator.push({ id: 'breadcrumbs' });
+ }}>
+ Breadcrumbs Example
+
+ {
+ this.props.navigator.push({ id: 'navbar' });
+ }}>
+ Navbar Example
+
+ {
+ this.props.navigator.push({ id: 'jumping' });
+ }}>
+ Jumping Example
+
+ {
+ this.props.onExampleExit();
+ }}>
+ Exit JSNavigationStack Example
+
+
+ );
+ }
+}
+
+var TabBarExample = React.createClass({
+
+ statics: {
+ title: '',
+ description: 'JS-implemented navigation',
+ },
+
+ renderSceneForRoute: function(route, nav) {
+ switch (route.id) {
+ case 'menu':
+ return (
+
+ );
+ case 'navbar':
+ return ;
+ case 'breadcrumbs':
+ return ;
+ case 'jumping':
+ return ;
+ }
+ },
+
+ render: function() {
+ return (
+ JSNavigationStack.AnimationConfigs.FloatFromBottom}
+ />
+ );
+ },
+
+});
+
+var styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ button: {
+ backgroundColor: 'white',
+ padding: 15,
+ },
+ buttonText: {
+ },
+ scene: {
+ flex: 1,
+ paddingTop: 64,
+ }
+});
+
+module.exports = TabBarExample;
diff --git a/Examples/UIExplorer/JSNavigationStack/JumpingNavSample.js b/Examples/UIExplorer/JSNavigationStack/JumpingNavSample.js
new file mode 100644
index 000000000..a2d28e695
--- /dev/null
+++ b/Examples/UIExplorer/JSNavigationStack/JumpingNavSample.js
@@ -0,0 +1,195 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ *
+ * @providesModule JumpingNavSample
+ */
+'use strict';
+
+var JSNavigationStack = require('JSNavigationStack');
+var React = require('React');
+var StyleSheet = require('StyleSheet');
+var ScrollView = require('ScrollView');
+var Text = require('Text');
+var TouchableBounce = require('TouchableBounce');
+var View = require('View');
+
+var _getRandomRoute = function() {
+ return {
+ randNumber: Math.random(),
+ };
+};
+
+var INIT_ROUTE = _getRandomRoute();
+var ROUTE_STACK = [
+ _getRandomRoute(),
+ _getRandomRoute(),
+ INIT_ROUTE,
+ _getRandomRoute(),
+ _getRandomRoute(),
+];
+var SampleRouteMapper = {
+
+ navigationItemForRoute: function(route, navigationOperations) {
+ return (
+
+
+ {route.randNumber}
+ {
+ navigationOperations.jumpBack();
+ }}>
+
+ jumpBack
+
+
+ {
+ navigationOperations.jumpForward();
+ }}>
+
+ jumpForward
+
+
+ {
+ navigationOperations.jumpTo(INIT_ROUTE);
+ }}>
+
+ jumpTo initial route
+
+
+ {
+ navigationOperations.push(_getRandomRoute());
+ }}>
+
+ destructive: push
+
+
+ {
+ navigationOperations.replace(_getRandomRoute());
+ }}>
+
+ destructive: replace
+
+
+ {
+ navigationOperations.pop();
+ }}>
+
+ destructive: pop
+
+
+ {
+ navigationOperations.immediatelyResetRouteStack([
+ _getRandomRoute(),
+ _getRandomRoute(),
+ ]);
+ }}>
+
+ destructive: Immediate set two routes
+
+
+ {
+ navigationOperations.popToTop();
+ }}>
+
+ destructive: pop to top
+
+
+
+
+ );
+ },
+};
+
+class JumpingNavBar extends React.Component {
+ render() {
+ return (
+
+ {this.props.routeStack.map((route, index) => (
+ {
+ this.props.navigationOperations.jumpTo(route);
+ }}>
+
+
+ {index}
+
+
+
+ ))}
+
+ );
+ }
+}
+
+var JumpingNavSample = React.createClass({
+
+ render: function() {
+ return (
+ }
+ shouldJumpOnBackstackPop={true}
+ />
+ );
+ },
+
+});
+
+var styles = StyleSheet.create({
+ scene: {
+ backgroundColor: '#eeeeee',
+ },
+ scroll: {
+ flex: 1,
+ },
+ button: {
+ backgroundColor: '#cccccc',
+ margin: 50,
+ marginTop: 26,
+ padding: 10,
+ },
+ buttonText: {
+ fontSize: 12,
+ textAlign: 'center',
+ },
+ appContainer: {
+ overflow: 'hidden',
+ backgroundColor: '#dddddd',
+ flex: 1,
+ },
+ navBar: {
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ right: 0,
+ height: 90,
+ flexDirection: 'row',
+ },
+ navButton: {
+ flex: 1,
+ },
+ navButtonText: {
+ textAlign: 'center',
+ fontSize: 32,
+ marginTop: 25,
+ },
+ navButtonActive: {
+ color: 'green',
+ },
+});
+
+module.exports = JumpingNavSample;
diff --git a/Examples/UIExplorer/JSNavigationStack/NavigationBarSample.js b/Examples/UIExplorer/JSNavigationStack/NavigationBarSample.js
new file mode 100644
index 000000000..722179fad
--- /dev/null
+++ b/Examples/UIExplorer/JSNavigationStack/NavigationBarSample.js
@@ -0,0 +1,126 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ *
+ * @providesModule NavigationBarSample
+ */
+'use strict';
+
+var JSNavigationStack = require('JSNavigationStack');
+var NavigationBar = require('NavigationBar');
+var React = require('React');
+var StyleSheet = require('StyleSheet');
+var Text = require('Text');
+var TouchableOpacity = require('TouchableOpacity');
+var View = require('View');
+
+var cssVar = require('cssVar');
+
+
+var NavigationBarRouteMapper = {
+
+ LeftButton: function(route, navigationOperations, index, navState) {
+ if (index === 0) {
+ return null;
+ }
+
+ var previousRoute = navState.routeStack[index - 1];
+ return (
+ navigationOperations.pop()}>
+
+
+ {previousRoute.title}
+
+
+
+ );
+ },
+
+ RightButton: function(route, navigationOperations, index, navState) {
+ return (
+ navigationOperations.push(newRandomRoute())}>
+
+
+ Next
+
+
+
+ );
+ },
+
+ Title: function(route, navigationOperations, index, navState) {
+ return (
+
+ {route.title} [{index}]
+
+ );
+ },
+
+};
+
+function newRandomRoute() {
+ return {
+ content: 'Hello World!',
+ title: 'Random ' + Math.round(Math.random() * 100),
+ };
+}
+
+var RouteMapper = {
+
+ navigationItemForRoute: function(route, navigationOperations) {
+ return (
+
+ {route.content}
+
+ );
+ },
+
+};
+
+var NavigationBarSample = React.createClass({
+
+ render: function() {
+ return (
+
+
+ }
+ />
+
+ );
+ },
+
+});
+
+var styles = StyleSheet.create({
+ appContainer: {
+ overflow: 'hidden',
+ backgroundColor: '#ffffff',
+ flex: 1,
+ },
+ scene: {
+ paddingTop: 50,
+ flex: 1,
+ },
+ navBarText: {
+ fontSize: 16,
+ marginVertical: 10,
+ },
+ navBarTitleText: {
+ color: cssVar('fbui-bluegray-60'),
+ fontWeight: 'bold',
+ marginVertical: 9,
+ },
+ navBarButtonText: {
+ color: cssVar('fbui-accent-blue'),
+ },
+});
+
+module.exports = NavigationBarSample;
diff --git a/Examples/UIExplorer/JSNavigationStack/NestedBreadcrumbNavSample.js b/Examples/UIExplorer/JSNavigationStack/NestedBreadcrumbNavSample.js
new file mode 100644
index 000000000..fc942c6ce
--- /dev/null
+++ b/Examples/UIExplorer/JSNavigationStack/NestedBreadcrumbNavSample.js
@@ -0,0 +1,235 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ *
+ * @providesModule NestedBreadcrumbNavSample
+ */
+'use strict';
+
+var BreadcrumbNavigationBar = require('BreadcrumbNavigationBar');
+var JSNavigationStack = require('JSNavigationStack');
+var React = require('React');
+var ScrollView = require('ScrollView');
+var StyleSheet = require('StyleSheet');
+var Text = require('Text');
+var TouchableBounce = require('TouchableBounce');
+var View = require('View');
+
+var SAMPLE_TEXT = 'Top Pushes. Middle Replaces. Bottom Pops.';
+
+var _getRandomRoute = function() {
+ return {
+ backButtonTitle: 'Back' + ('' + 10 * Math.random()).substr(0, 1),
+ content:
+ SAMPLE_TEXT + '\nHere\'s a random number ' + Math.random(),
+ title: 'Pushed!',
+ rightButtonTitle: Math.random() > 0.5 ? 'Right' : 'Button',
+ };
+};
+
+
+var HorizontalNavigationBarRouteMapper = {
+ rightContentForRoute: function(route, navigationOperations) {
+ if (route.rightButtonTitle) {
+ return (
+
+ {route.rightButtonTitle}
+
+ );
+ } else {
+ return null;
+ }
+ },
+ titleContentForRoute: function(route, navigationOperations) {
+ return (
+ navigationOperations.push(_getRandomRoute())}>
+
+ {route.title}
+
+
+ );
+ },
+ iconForRoute: function(route, navigationOperations) {
+ var onPress =
+ navigationOperations.popToRoute.bind(navigationOperations, route);
+ return (
+
+
+
+ );
+ },
+ separatorForRoute: function(route, navigationOperations) {
+ return (
+
+
+
+ );
+ }
+};
+
+var ThirdDeepRouteMapper = {
+ navigationItemForRoute: function(route, navigationOperations) {
+ return (
+
+
+
+
+
+ request push soon
+
+
+
+
+
+ );
+ },
+
+ _pushRoute: function(push) {
+ return () => push(_getRandomRoute());
+ },
+};
+
+var SecondDeepRouteMapper = {
+ navigationItemForRoute: function(route, navigationOperations) {
+ return (
+
+
+
+ Push Horizontal
+
+
+
+ }
+ />
+
+ );
+ },
+
+ _pushRoute: function(push) {
+ return () => push(_getRandomRoute());
+ },
+};
+
+var FirstDeepRouteMapper = {
+ navigationItemForRoute: function(route, navigationOperations) {
+ return (
+
+
+
+ Push Outer Vertical Stack
+
+
+
+ }
+ />
+
+ );
+ },
+
+ _pushRoute: function(push) {
+ return () => push(_getRandomRoute());
+ },
+};
+
+/**
+ * The outer component.
+ */
+var NestedBreadcrumbNavSample = React.createClass({
+ render: function() {
+ var initialRoute = {title: 'Vertical'};
+ // No navigation bar.
+ return (
+ JSNavigationStack.AnimationConfigs.FloatFromBottom}
+ initialRoute={initialRoute}
+ routeMapper={FirstDeepRouteMapper}
+ />
+ );
+ }
+});
+
+var styles = StyleSheet.create({
+ navigationItem: {
+ backgroundColor: '#eeeeee',
+ shadowColor: 'black',
+ shadowRadius: 20,
+ shadowOffset: {w: 0, h: -10},
+ },
+ paddingForNavBar: {
+ paddingTop: 60,
+ },
+ paddingForMenuBar: {
+ paddingTop: 10,
+ },
+ button: {
+ backgroundColor: '#888888',
+ margin: 10,
+ marginTop: 10,
+ padding: 10,
+ marginRight: 20,
+ },
+ buttonText: {
+ fontSize: 12,
+ textAlign: 'center',
+ color: 'white',
+ },
+ appContainer: {
+ overflow: 'hidden',
+ backgroundColor: '#dddddd',
+ flex: 1,
+ },
+ titleText: {
+ fontSize: 18,
+ color: '#666666',
+ textAlign: 'center',
+ fontWeight: 'bold',
+ lineHeight: 32,
+ },
+ filterText: {
+ color: '#5577ff',
+ },
+ // TODO: Accept icons from route.
+ crumbIconPlaceholder: {
+ flex: 1,
+ backgroundColor: '#666666',
+ },
+ crumbSeparatorPlaceholder: {
+ flex: 1,
+ backgroundColor: '#aaaaaa',
+ },
+ secondDeepNavigator: {
+ margin: 0,
+ borderColor: '#666666',
+ borderWidth: 0.5,
+ height: 400,
+ },
+ thirdDeepNavigator: {
+ margin: 0,
+ borderColor: '#aaaaaa',
+ borderWidth: 0.5,
+ height: 400,
+ },
+ thirdDeepScrollContent: {
+ height: 1000,
+ }
+});
+
+module.exports = NestedBreadcrumbNavSample;
diff --git a/Examples/UIExplorer/UIExplorerApp.js b/Examples/UIExplorer/UIExplorerApp.js
index 58b84956b..82fe1bcff 100644
--- a/Examples/UIExplorer/UIExplorerApp.js
+++ b/Examples/UIExplorer/UIExplorerApp.js
@@ -13,23 +13,42 @@
var React = require('react-native');
var UIExplorerList = require('./UIExplorerList');
-
var {
AppRegistry,
NavigatorIOS,
StyleSheet,
} = React;
-
var UIExplorerApp = React.createClass({
+ getInitialState: function() {
+ return {
+ openExternalExample: (null: ?React.Component),
+ };
+ },
+
render: function() {
+ if (this.state.openExternalExample) {
+ var Example = this.state.openExternalExample;
+ return (
+ {
+ this.setState({ openExternalExample: null, });
+ }}
+ />
+ );
+ }
return (
{
+ this.setState({ openExternalExample: example, });
+ },
+ }
}}
itemWrapperStyle={styles.itemWrapper}
tintColor='#008888'
diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js
index 14de68387..3c693b42c 100644
--- a/Examples/UIExplorer/UIExplorerList.js
+++ b/Examples/UIExplorer/UIExplorerList.js
@@ -21,6 +21,7 @@ var {
TouchableHighlight,
View,
} = React;
+var JSNavigationStackExample = require('./JSNavigationStack/JSNavigationStackExample');
var createExamplePage = require('./createExamplePage');
@@ -32,6 +33,7 @@ var COMPONENTS = [
require('./ListViewSimpleExample'),
require('./MapViewExample'),
require('./NavigatorIOSExample'),
+ JSNavigationStackExample,
require('./PickerExample'),
require('./ScrollViewExample'),
require('./SliderIOSExample'),
@@ -143,6 +145,12 @@ class UIExplorerList extends React.Component {
}
_onPressRow(example) {
+ if (example === JSNavigationStackExample) {
+ this.props.onExternalExampleRequested(
+ JSNavigationStackExample
+ );
+ return;
+ }
var Component = example.examples ?
createExamplePage(null, example) :
example;
diff --git a/Libraries/CustomComponents/JSNavigationStack.js b/Libraries/CustomComponents/JSNavigationStack.js
new file mode 100644
index 000000000..f0765297c
--- /dev/null
+++ b/Libraries/CustomComponents/JSNavigationStack.js
@@ -0,0 +1,859 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ *
+ * @providesModule JSNavigationStack
+ */
+
+"use strict"
+
+var AnimationsDebugModule = require('NativeModules').AnimationsDebugModule;
+var Backstack = require('Backstack');
+var Dimensions = require('Dimensions');
+var InteractionMixin = require('InteractionMixin');
+var JSNavigationStackAnimationConfigs = require('JSNavigationStackAnimationConfigs');
+var PanResponder = require('PanResponder');
+var React = require('React');
+var StaticContainer = require('StaticContainer.react');
+var StyleSheet = require('StyleSheet');
+var Subscribable = require('Subscribable');
+var TimerMixin = require('TimerMixin');
+var View = require('View');
+
+var clamp = require('clamp');
+var invariant = require('invariant');
+var keyMirror = require('keyMirror');
+var merge = require('merge');
+var rebound = require('rebound');
+
+var PropTypes = React.PropTypes;
+
+var SCREEN_WIDTH = Dimensions.get('window').width;
+var SCREEN_HEIGHT = Dimensions.get('window').height;
+
+var OFF_SCREEN = {style: {opacity: 0}};
+
+var NAVIGATION_BAR_REF = 'navigationBar_ref';
+
+var __uid = 0;
+function getuid() {
+ return __uid++;
+}
+
+var nextComponentUid = 0;
+
+// styles moved to the top of the file so getDefaultProps can refer to it
+var styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ overflow: 'hidden',
+ },
+ defaultSceneStyle: {
+ width: SCREEN_WIDTH,
+ height: SCREEN_HEIGHT,
+ },
+ presentNavItem: {
+ position: 'absolute',
+ overflow: 'hidden',
+ left: 0,
+ right: 0,
+ bottom: 0,
+ top: 0,
+ },
+ futureNavItem: {
+ overflow: 'hidden',
+ position: 'absolute',
+ left: 0,
+ opacity: 0,
+ },
+ transitioner: {
+ flex: 1,
+ backgroundColor: '#555555',
+ overflow: 'hidden',
+ }
+});
+
+var JSNavigationStack = React.createClass({
+
+ propTypes: {
+ animationConfigRouteMapper: PropTypes.func,
+ routeMapper: PropTypes.shape({
+ navigationItemForRoute: PropTypes.func,
+ }),
+ initialRoute: PropTypes.object,
+ initialRouteStack: PropTypes.arrayOf(PropTypes.object),
+ // Will emit the target route on mounting and before each nav transition,
+ // overriding the handler in this.props.navigator
+ onWillFocus: PropTypes.func,
+ // Will emit the new route after mounting and after each nav transition,
+ // overriding the handler in this.props.navigator
+ onDidFocus: PropTypes.func,
+ // Will be called with (ref, indexInStack) when an item ref resolves
+ onItemRef: PropTypes.func,
+ // Define the component to use for the nav bar, which will get navState and navigator props
+ navigationBar: PropTypes.node,
+ // The navigator object from a parent JSNavigationStack
+ navigator: PropTypes.object,
+
+ /**
+ * Should the backstack back button "jump" back instead of pop? Set to true
+ * if a jump forward might happen after the android back button is pressed,
+ * so the scenes will remain mounted
+ */
+ shouldJumpOnBackstackPop: PropTypes.bool,
+ },
+
+ statics: {
+ AnimationConfigs: JSNavigationStackAnimationConfigs,
+ },
+
+ mixins: [TimerMixin, InteractionMixin, Subscribable.Mixin],
+
+ getDefaultProps: function() {
+ return {
+ animationConfigRouteMapper: () => JSNavigationStackAnimationConfigs.PushFromRight,
+ sceneStyle: styles.defaultSceneStyle,
+ };
+ },
+
+ getInitialState: function() {
+ var routeStack = this.props.initialRouteStack || [];
+ var initialRouteIndex = 0;
+ if (this.props.initialRoute && routeStack.length) {
+ initialRouteIndex = routeStack.indexOf(this.props.initialRoute);
+ invariant(
+ initialRouteIndex !== -1,
+ 'initialRoute is not in initialRouteStack.'
+ );
+ } else if (this.props.initialRoute) {
+ routeStack = [this.props.initialRoute];
+ } else {
+ invariant(
+ routeStack.length >= 1,
+ 'JSNavigationStack requires props.initialRoute or props.initialRouteStack.'
+ );
+ }
+ return {
+ animationConfigStack: routeStack.map(
+ (route) => this.props.animationConfigRouteMapper(route)
+ ),
+ idStack: routeStack.map(() => getuid()),
+ routeStack,
+ // These are tracked to avoid rendering everything all the time.
+ updatingRangeStart: initialRouteIndex,
+ updatingRangeLength: initialRouteIndex + 1,
+ // Either animating or gesturing.
+ isAnimating: false,
+ jumpToIndex: routeStack.length - 1,
+ presentedIndex: initialRouteIndex,
+ isResponderOnlyToBlockTouches: false,
+ fromIndex: initialRouteIndex,
+ toIndex: initialRouteIndex,
+ };
+ },
+
+ componentWillMount: function() {
+ this.memoizedNavigationOperations = {
+ jumpBack: this.jumpBack,
+ jumpForward: this.jumpForward,
+ jumpTo: this.jumpTo,
+ push: this.push,
+ pop: this.pop,
+ replace: this.replace,
+ replaceAtIndex: this.replaceAtIndex,
+ replacePrevious: this.replacePrevious,
+ replacePreviousAndPop: this.replacePreviousAndPop,
+ immediatelyResetRouteStack: this.immediatelyResetRouteStack,
+ resetTo: this.resetTo,
+ popToRoute: this.popToRoute,
+ popToTop: this.popToTop,
+ parentNavigator: this.props.navigator,
+ // We want to bubble focused routes to the top navigation stack. If we are
+ // a child, this will allow us to call this.props.navigator.on*Focus
+ onWillFocus: this.props.onWillFocus,
+ onDidFocus: this.props.onDidFocus,
+ };
+
+ this.panGesture = PanResponder.create({
+ onStartShouldSetPanResponderCapture: this._handleStartShouldSetPanResponderCapture,
+ onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder,
+ onPanResponderGrant: this._handlePanResponderGrant,
+ onPanResponderRelease: this._handlePanResponderRelease,
+ onPanResponderMove: this._handlePanResponderMove,
+ onPanResponderTerminate: this._handlePanResponderTerminate,
+ });
+ this._itemRefs = {};
+ this._interactionHandle = null;
+ this._backstackComponentKey = 'jsnavstack' + nextComponentUid;
+ nextComponentUid++;
+
+ Backstack.eventEmitter && this.addListenerOn(
+ Backstack.eventEmitter,
+ 'popNavigation',
+ this._onBackstackPopState);
+
+ this._emitWillFocus(this.state.presentedIndex);
+ },
+
+ _configureSpring: function(animationConfig) {
+ var config = this.spring.getSpringConfig();
+ config.friction = animationConfig.springFriction;
+ config.tension = animationConfig.springTension;
+ },
+
+ componentDidMount: function() {
+ this.springSystem = new rebound.SpringSystem();
+ this.spring = this.springSystem.createSpring();
+ this.spring.setRestSpeedThreshold(0.05);
+ var animationConfig = this.state.animationConfigStack[this.state.presentedIndex];
+ animationConfig && this._configureSpring(animationConfig);
+ this.spring.addListener(this);
+ this.onSpringUpdate();
+
+ // Fill up the Backstack with routes that have been provided in
+ // initialRouteStack
+ this._fillBackstackRange(0, this.state.routeStack.indexOf(this.props.initialRoute));
+ this._emitDidFocus(this.state.presentedIndex);
+ },
+
+ componentWillUnmount: function() {
+ Backstack.removeComponentHistory(this._backstackComponentKey);
+ },
+
+ _onBackstackPopState: function(componentKey, stateKey, state) {
+ if (componentKey !== this._backstackComponentKey) {
+ return;
+ }
+ if (!this._canNavigate()) {
+ // A bit hacky: if we can't actually handle the pop, just push it back on the stack
+ Backstack.pushNavigation(componentKey, stateKey, state);
+ } else {
+ if (this.props.shouldJumpOnBackstackPop) {
+ this._jumpToWithoutBackstack(state.fromRoute);
+ } else {
+ this._popToRouteWithoutBackstack(state.fromRoute);
+ }
+ }
+ },
+
+ /**
+ * @param {RouteStack} nextRouteStack Next route stack to reinitialize. This
+ * doesn't accept stack item `id`s, which implies that all existing items are
+ * destroyed, and then potentially recreated according to `routeStack`. Does
+ * not animate, immediately replaces and rerenders navigation bar and stack
+ * items.
+ */
+ immediatelyResetRouteStack: function(nextRouteStack) {
+ var destIndex = nextRouteStack.length - 1;
+ this.setState({
+ idStack: nextRouteStack.map(getuid),
+ routeStack: nextRouteStack,
+ animationConfigStack: nextRouteStack.map(
+ this.props.animationConfigRouteMapper
+ ),
+ updatingRangeStart: 0,
+ updatingRangeLength: nextRouteStack.length,
+ presentedIndex: destIndex,
+ jumpToIndex: destIndex,
+ toIndex: destIndex,
+ fromIndex: destIndex,
+ }, () => {
+ this.onSpringUpdate();
+ });
+ },
+
+ /**
+ * TODO: Accept callback for spring completion.
+ */
+ _requestTransitionTo: function(topOfStack) {
+ if (topOfStack !== this.state.presentedIndex) {
+ invariant(!this.state.isAnimating, 'Cannot navigate while transitioning');
+ this.state.fromIndex = this.state.presentedIndex;
+ this.state.toIndex = topOfStack;
+ this.spring.setOvershootClampingEnabled(false);
+ if (AnimationsDebugModule) {
+ AnimationsDebugModule.startRecordingFps();
+ }
+ this._transitionToToIndexWithVelocity(
+ this.state.animationConfigStack[this.state.fromIndex].defaultTransitionVelocity
+ );
+ }
+ },
+
+ /**
+ * `onSpring*` spring delegate. Wired up via `spring.addListener(this)`
+ */
+ onSpringEndStateChange: function() {
+ if (!this._interactionHandle) {
+ this._interactionHandle = this.createInteractionHandle();
+ }
+ },
+
+ onSpringUpdate: function() {
+ this._transitionBetween(
+ this.state.fromIndex,
+ this.state.toIndex,
+ this.spring.getCurrentValue()
+ );
+ },
+
+ onSpringAtRest: function() {
+ this.state.isAnimating = false;
+ this._completeTransition();
+ this.spring.setCurrentValue(0).setAtRest();
+ if (this._interactionHandle) {
+ this.clearInteractionHandle(this._interactionHandle);
+ this._interactionHandle = null;
+ }
+ },
+
+ _completeTransition: function() {
+ if (this.spring.getCurrentValue() === 1) {
+ var presentedIndex = this.state.toIndex;
+ this.state.fromIndex = presentedIndex;
+ this.state.presentedIndex = presentedIndex;
+ this._emitDidFocus(presentedIndex);
+ this._removePoppedRoutes();
+ if (AnimationsDebugModule) {
+ AnimationsDebugModule.stopRecordingFps();
+ }
+ this._hideOtherScenes(presentedIndex);
+ }
+ },
+
+ _transitionToToIndexWithVelocity: function(v) {
+ this._configureSpring(
+ // For visual consistency, the from index is always used to configure the spring
+ this.state.animationConfigStack[this.state.fromIndex]
+ );
+ this.state.isAnimating = true;
+ this.spring.setVelocity(v);
+ this.spring.setEndValue(1);
+ this._emitWillFocus(this.state.toIndex);
+ },
+
+ _transitionToFromIndexWithVelocity: function(v) {
+ this._configureSpring(
+ this.state.animationConfigStack[this.state.fromIndex]
+ );
+ this.state.isAnimating = true;
+ this.spring.setVelocity(v);
+ this.spring.setEndValue(0);
+ },
+
+ _emitDidFocus: function(index) {
+ var route = this.state.routeStack[index];
+ if (this.props.onDidFocus) {
+ this.props.onDidFocus(route);
+ } else if (this.props.navigator && this.props.navigator.onDidFocus) {
+ this.props.navigator.onDidFocus(route);
+ }
+ },
+
+ _emitWillFocus: function(index) {
+ var route = this.state.routeStack[index];
+ if (this.props.onWillFocus) {
+ this.props.onWillFocus(route);
+ } else if (this.props.navigator && this.props.navigator.onWillFocus) {
+ this.props.navigator.onWillFocus(route);
+ }
+ },
+
+ /**
+ * Does not delete the scenes - merely hides them.
+ */
+ _hideOtherScenes: function(activeIndex) {
+ for (var i = 0; i < this.state.routeStack.length; i++) {
+ if (i === activeIndex) {
+ continue;
+ }
+ var sceneRef = 'scene_' + i;
+ this.refs[sceneRef] &&
+ this.refs['scene_' + i].setNativeProps(OFF_SCREEN);
+ }
+ },
+
+ /**
+ * Becomes the responder on touch start (capture) while animating so that it
+ * blocks all touch interactions inside of it. However, this responder lock
+ * means nothing more than that. We record if the sole reason for being
+ * responder is to block interactions (`isResponderOnlyToBlockTouches`).
+ */
+ _handleStartShouldSetPanResponderCapture: function(e, gestureState) {
+ return this.state.isAnimating;
+ },
+
+ _handleMoveShouldSetPanResponder: function(e, gestureState) {
+ var currentRoute = this.state.routeStack[this.state.presentedIndex];
+ var animationConfig = this.state.animationConfigStack[this.state.presentedIndex];
+ if (!animationConfig.enableGestures) {
+ return false;
+ }
+ var currentLoc = animationConfig.isVertical ? gestureState.moveY : gestureState.moveX;
+ var travelDist = animationConfig.isVertical ? gestureState.dy : gestureState.dx;
+ var oppositeAxisTravelDist =
+ animationConfig.isVertical ? gestureState.dx : gestureState.dy;
+ var moveStartedInRegion = currentLoc < animationConfig.edgeHitWidth;
+ var moveTravelledFarEnough =
+ travelDist >= animationConfig.gestureDetectMovement &&
+ travelDist > oppositeAxisTravelDist * animationConfig.directionRatio;
+ return (
+ !this.state.isResponderOnlyToBlockTouches &&
+ moveStartedInRegion &&
+ !this.state.isAnimating &&
+ this.state.presentedIndex > 0 &&
+ moveTravelledFarEnough
+ );
+ },
+
+ _handlePanResponderGrant: function(e, gestureState) {
+ this.state.isResponderOnlyToBlockTouches = this.state.isAnimating;
+ if (!this.state.isAnimating) {
+ this.state.fromIndex = this.state.presentedIndex;
+ this.state.toIndex = this.state.presentedIndex - 1;
+ }
+ },
+
+ _handlePanResponderRelease: function(e, gestureState) {
+ if (this.state.isResponderOnlyToBlockTouches) {
+ this.state.isResponderOnlyToBlockTouches = false;
+ return;
+ }
+ var animationConfig = this.state.animationConfigStack[this.state.presentedIndex];
+ var velocity = animationConfig.isVertical ? gestureState.vy : gestureState.vx;
+ // It's not the real location. There is no *real* location - that's the
+ // point of the pan gesture.
+ var pseudoLocation = animationConfig.isVertical ?
+ gestureState.y0 + gestureState.dy :
+ gestureState.x0 + gestureState.dx;
+ var still = Math.abs(velocity) < animationConfig.notMoving;
+ if (this.spring.getCurrentValue() === 0) {
+ this.spring.setCurrentValue(0).setAtRest();
+ this._completeTransition();
+ return;
+ }
+ var transitionVelocity =
+ still && animationConfig.pastPointOfNoReturn(pseudoLocation) ? animationConfig.snapVelocity :
+ still && !animationConfig.pastPointOfNoReturn(pseudoLocation) ? -animationConfig.snapVelocity :
+ clamp(-10, velocity, 10); // What are Rebound UoM?
+
+ this.spring.setOvershootClampingEnabled(true);
+ if (transitionVelocity < 0) {
+ this._transitionToFromIndexWithVelocity(transitionVelocity);
+ } else {
+ this._manuallyPopBackstack(1);
+ this._transitionToToIndexWithVelocity(transitionVelocity);
+ }
+ },
+
+ _handlePanResponderTerminate: function(e, gestureState) {
+ this.state.isResponderOnlyToBlockTouches = false;
+ this._transitionToFromIndexWithVelocity(0);
+ },
+
+ _handlePanResponderMove: function(e, gestureState) {
+ if (!this.state.isResponderOnlyToBlockTouches) {
+ var animationConfig = this.state.animationConfigStack[this.state.presentedIndex];
+ var distance = animationConfig.isVertical ? gestureState.dy : gestureState.dx;
+ var gestureDetectMovement = animationConfig.gestureDetectMovement;
+ var nextProgress = (distance - gestureDetectMovement) /
+ (animationConfig.screenDimension - gestureDetectMovement);
+ this.spring.setCurrentValue(clamp(0, nextProgress, 1));
+ }
+ },
+
+ _transitionSceneStyle: function(fromIndex, toIndex, progress, index) {
+ var viewAtIndex = this.refs['scene_' + index];
+ if (viewAtIndex === null || viewAtIndex === undefined) {
+ return;
+ }
+ // Use toIndex animation when we move forwards. Use fromIndex when we move back
+ var animationIndex = this.state.presentedIndex < toIndex ? toIndex : fromIndex;
+ var animationConfig = this.state.animationConfigStack[animationIndex];
+ var styleToUse = {};
+ var useFn = index < fromIndex || index < toIndex ?
+ animationConfig.interpolators.out :
+ animationConfig.interpolators.into;
+ var directionAdjustedProgress = fromIndex < toIndex ? progress : 1 - progress;
+ var didChange = useFn(styleToUse, directionAdjustedProgress);
+ if (didChange) {
+ viewAtIndex.setNativeProps({style: styleToUse});
+ }
+ },
+
+ _transitionBetween: function(fromIndex, toIndex, progress) {
+ this._transitionSceneStyle(fromIndex, toIndex, progress, fromIndex);
+ this._transitionSceneStyle(fromIndex, toIndex, progress, toIndex);
+ var navBar = this.refs[NAVIGATION_BAR_REF];
+ if (navBar && navBar.updateProgress) {
+ navBar.updateProgress(progress, fromIndex, toIndex);
+ }
+ },
+
+ _handleResponderTerminationRequest: function() {
+ return false;
+ },
+
+ _resetUpdatingRange: function() {
+ this.state.updatingRangeStart = 0;
+ this.state.updatingRangeLength = this.state.routeStack.length;
+ },
+
+ _canNavigate: function() {
+ return !this.state.isAnimating;
+ },
+
+ _jumpNWithoutBackstack: function(n) {
+ var destIndex = this._getDestIndexWithinBounds(n);
+ if (!this._canNavigate()) {
+ return; // It's busy animating or transitioning.
+ }
+ var requestTransitionAndResetUpdatingRange = () => {
+ this._requestTransitionTo(destIndex);
+ this._resetUpdatingRange();
+ };
+ this.setState({
+ updatingRangeStart: destIndex,
+ updatingRangeLength: 1,
+ toIndex: destIndex,
+ }, requestTransitionAndResetUpdatingRange);
+ },
+
+ _fillBackstackRange: function(start, end) {
+ invariant(
+ start <= end,
+ 'Can only fill the backstack forward. Provide end index greater than start'
+ );
+ for (var i = 0; i < (end - start); i++) {
+ var fromIndex = start + i;
+ var toIndex = start + i + 1;
+ Backstack.pushNavigation(
+ this._backstackComponentKey,
+ toIndex,
+ {
+ fromRoute: this.state.routeStack[fromIndex],
+ toRoute: this.state.routeStack[toIndex],
+ }
+ );
+ }
+ },
+
+ _getDestIndexWithinBounds: function(n) {
+ var currentIndex = this.state.presentedIndex;
+ var destIndex = currentIndex + n;
+ invariant(
+ destIndex >= 0,
+ 'Cannot jump before the first route.'
+ );
+ var maxIndex = this.state.routeStack.length - 1;
+ invariant(
+ maxIndex >= destIndex,
+ 'Cannot jump past the last route.'
+ );
+ return destIndex;
+ },
+
+ _jumpN: function(n) {
+ var currentIndex = this.state.presentedIndex;
+ if (!this._canNavigate()) {
+ return; // It's busy animating or transitioning.
+ }
+ if (n > 0) {
+ this._fillBackstackRange(currentIndex, currentIndex + n);
+ } else {
+ var landingBeforeIndex = currentIndex + n + 1;
+ Backstack.resetToBefore(
+ this._backstackComponentKey,
+ landingBeforeIndex
+ );
+ }
+ this._jumpNWithoutBackstack(n);
+ },
+
+ jumpTo: function(route) {
+ var destIndex = this.state.routeStack.indexOf(route);
+ invariant(
+ destIndex !== -1,
+ 'Cannot jump to route that is not in the route stack'
+ );
+ this._jumpN(destIndex - this.state.presentedIndex);
+ },
+
+ _jumpToWithoutBackstack: function(route) {
+ var destIndex = this.state.routeStack.indexOf(route);
+ invariant(
+ destIndex !== -1,
+ 'Cannot jump to route that is not in the route stack'
+ );
+ this._jumpNWithoutBackstack(destIndex - this.state.presentedIndex);
+ },
+
+ jumpForward: function() {
+ this._jumpN(1);
+ },
+
+ jumpBack: function() {
+ this._jumpN(-1);
+ },
+
+ push: function(route) {
+ invariant(!!route, 'Must supply route to push');
+ if (!this._canNavigate()) {
+ return; // It's busy animating or transitioning.
+ }
+ var activeLength = this.state.presentedIndex + 1;
+ var activeStack = this.state.routeStack.slice(0, activeLength);
+ var activeIDStack = this.state.idStack.slice(0, activeLength);
+ var activeAnimationConfigStack = this.state.animationConfigStack.slice(0, activeLength);
+ var nextStack = activeStack.concat([route]);
+ var nextIDStack = activeIDStack.concat([getuid()]);
+ var nextAnimationConfigStack = activeAnimationConfigStack.concat([
+ this.props.animationConfigRouteMapper(route),
+ ]);
+ var requestTransitionAndResetUpdatingRange = () => {
+ this._requestTransitionTo(nextStack.length - 1);
+ this._resetUpdatingRange();
+ };
+ var navigationState = {
+ toRoute: route,
+ fromRoute: this.state.routeStack[this.state.routeStack.length - 1],
+ };
+ Backstack.pushNavigation(
+ this._backstackComponentKey,
+ this.state.routeStack.length,
+ navigationState);
+
+ this.setState({
+ idStack: nextIDStack,
+ routeStack: nextStack,
+ animationConfigStack: nextAnimationConfigStack,
+ jumpToIndex: nextStack.length - 1,
+ updatingRangeStart: nextStack.length - 1,
+ updatingRangeLength: 1,
+ }, requestTransitionAndResetUpdatingRange);
+ },
+
+ _manuallyPopBackstack: function(n) {
+ Backstack.resetToBefore(this._backstackComponentKey, this.state.routeStack.length - n);
+ },
+
+ /**
+ * Like popN, but doesn't also update the Backstack.
+ */
+ _popNWithoutBackstack: function(n) {
+ if (n === 0 || !this._canNavigate()) {
+ return;
+ }
+ invariant(
+ this.state.presentedIndex - n >= 0,
+ 'Cannot pop below zero'
+ );
+ this.state.jumpToIndex = this.state.presentedIndex - n;
+ this._requestTransitionTo(
+ this.state.presentedIndex - n
+ );
+ },
+
+ popN: function(n) {
+ if (n === 0 || !this._canNavigate()) {
+ return;
+ }
+ this._popNWithoutBackstack(n);
+ this._manuallyPopBackstack(n);
+ },
+
+ pop: function() {
+ if (this.props.navigator && this.state.routeStack.length === 1) {
+ return this.props.navigator.pop();
+ }
+ this.popN(1);
+ },
+
+ /**
+ * Replace a route in the navigation stack.
+ *
+ * `index` specifies the route in the stack that should be replaced.
+ * If it's negative, it counts from the back.
+ */
+ replaceAtIndex: function(route, index) {
+ invariant(!!route, 'Must supply route to replace');
+ if (index < 0) {
+ index += this.state.routeStack.length;
+ }
+
+ if (this.state.routeStack.length <= index) {
+ return;
+ }
+
+ // I don't believe we need to lock for a replace since there's no
+ // navigation actually happening
+ var nextIDStack = this.state.idStack.slice();
+ var nextRouteStack = this.state.routeStack.slice();
+ var nextAnimationModeStack = this.state.animationConfigStack.slice();
+ nextIDStack[index] = getuid();
+ nextRouteStack[index] = route;
+ nextAnimationModeStack[index] = this.props.animationConfigRouteMapper(route);
+
+ this.setState({
+ idStack: nextIDStack,
+ routeStack: nextRouteStack,
+ animationConfigStack: nextAnimationModeStack,
+ updatingRangeStart: index,
+ updatingRangeLength: 1,
+ }, () => {
+ this._resetUpdatingRange();
+ if (index === this.state.presentedIndex) {
+ this._emitWillFocus(this.state.presentedIndex);
+ this._emitDidFocus(this.state.presentedIndex);
+ }
+ });
+ },
+
+ /**
+ * Replaces the current scene in the stack.
+ */
+ replace: function(route) {
+ this.replaceAtIndex(route, this.state.presentedIndex);
+ },
+
+ /**
+ * Replace the current route's parent.
+ */
+ replacePrevious: function(route) {
+ this.replaceAtIndex(route, this.state.presentedIndex - 1);
+ },
+
+ popToTop: function() {
+ this.popToRoute(this.state.routeStack[0]);
+ },
+
+ _getNumToPopForRoute: function(route) {
+ var indexOfRoute = this.state.routeStack.indexOf(route);
+ invariant(
+ indexOfRoute !== -1,
+ 'Calling pop to route for a route that doesn\'t exist!'
+ );
+ return this.state.routeStack.length - indexOfRoute - 1;
+ },
+
+ /**
+ * Like popToRoute, but doesn't update the Backstack, presumably because it's already up to date.
+ */
+ _popToRouteWithoutBackstack: function(route) {
+ var numToPop = this._getNumToPopForRoute(route);
+ this._popNWithoutBackstack(numToPop);
+ },
+
+ popToRoute: function(route) {
+ var numToPop = this._getNumToPopForRoute(route);
+ this.popN(numToPop);
+ },
+
+ replacePreviousAndPop: function(route) {
+ if (this.state.routeStack.length < 2 || !this._canNavigate()) {
+ return;
+ }
+ this.replacePrevious(route);
+ this.pop();
+ },
+
+ resetTo: function(route) {
+ invariant(!!route, 'Must supply route to push');
+ if (this._canNavigate()) {
+ this.replaceAtIndex(route, 0);
+ this.popToRoute(route);
+ }
+ },
+
+ _onItemRef: function(itemId, ref) {
+ this._itemRefs[itemId] = ref;
+ var itemIndex = this.state.idStack.indexOf(itemId);
+ if (itemIndex === -1) {
+ return;
+ }
+ this.props.onItemRef && this.props.onItemRef(ref, itemIndex);
+ },
+
+ _removePoppedRoutes: function() {
+ var newStackLength = this.state.jumpToIndex + 1;
+ // Remove any unneeded rendered routes.
+ if (newStackLength < this.state.routeStack.length) {
+ var updatingRangeStart = newStackLength; // One past the top
+ var updatingRangeLength = this.state.routeStack.length - newStackLength + 1;
+ this.state.idStack.slice(newStackLength).map((removingId) => {
+ this._itemRefs[removingId] = null;
+ });
+ this.setState({
+ updatingRangeStart: updatingRangeStart,
+ updatingRangeLength: updatingRangeLength,
+ animationConfigStack: this.state.animationConfigStack.slice(0, newStackLength),
+ idStack: this.state.idStack.slice(0, newStackLength),
+ routeStack: this.state.routeStack.slice(0, newStackLength),
+ }, this._resetUpdatingRange);
+ }
+ },
+
+ _routeToOptimizedStackItem: function(route, i) {
+ var shouldUpdateChild =
+ this.state.updatingRangeLength !== 0 &&
+ i >= this.state.updatingRangeStart &&
+ i <= this.state.updatingRangeStart + this.state.updatingRangeLength;
+ var routeMapper = this.props.routeMapper;
+ var child = routeMapper.navigationItemForRoute(
+ route,
+ this.memoizedNavigationOperations,
+ this._onItemRef.bind(null, this.state.idStack[i])
+ );
+
+ var initialSceneStyle =
+ i === this.state.presentedIndex ? styles.presentNavItem : styles.futureNavItem;
+ return (
+
+
+ {child}
+
+
+ );
+ },
+
+ renderNavigationStackItems: function() {
+ var shouldRecurseToNavigator = this.state.updatingRangeLength !== 0;
+ // If not recursing update to navigator at all, may as well avoid
+ // computation of navigator children.
+ var items = shouldRecurseToNavigator ?
+ this.state.routeStack.map(this._routeToOptimizedStackItem) : null;
+
+ return (
+
+
+ {items}
+
+
+ );
+ },
+
+ renderNavigationStackBar: function() {
+ var NavigationBarClass = this.props.NavigationBarClass;
+ if (!this.props.navigationBar) {
+ return null;
+ }
+ return React.cloneElement(this.props.navigationBar, {
+ ref: NAVIGATION_BAR_REF,
+ navigationOperations: this.memoizedNavigationOperations,
+ navState: this.state,
+ });
+ },
+
+ render: function() {
+ return (
+
+ {this.renderNavigationStackItems()}
+ {this.renderNavigationStackBar()}
+
+ );
+ },
+});
+
+module.exports = JSNavigationStack;
diff --git a/Libraries/CustomComponents/JSNavigationStack/BreadcrumbNavigationBar.js b/Libraries/CustomComponents/JSNavigationStack/BreadcrumbNavigationBar.js
new file mode 100644
index 000000000..5bafc6807
--- /dev/null
+++ b/Libraries/CustomComponents/JSNavigationStack/BreadcrumbNavigationBar.js
@@ -0,0 +1,241 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ *
+ * @providesModule BreadcrumbNavigationBar
+ */
+'use strict';
+
+var BreadcrumbNavigationBarStyles = require('BreadcrumbNavigationBarStyles');
+var PixelRatio = require('PixelRatio');
+var React = require('React');
+var NavigationBarStyles = require('NavigationBarStyles');
+var StaticContainer = require('StaticContainer.react');
+var StyleSheet = require('StyleSheet');
+var View = require('View');
+
+var Interpolators = BreadcrumbNavigationBarStyles.Interpolators;
+var PropTypes = React.PropTypes;
+
+/**
+ * Reusable props objects.
+ */
+var CRUMB_PROPS = Interpolators.map(() => {return {style: {}};});
+var ICON_PROPS = Interpolators.map(() => {return {style: {}};});
+var SEPARATOR_PROPS = Interpolators.map(() => {return {style: {}};});
+var TITLE_PROPS = Interpolators.map(() => {return {style: {}};});
+var RIGHT_BUTTON_PROPS = Interpolators.map(() => {return {style: {}};});
+
+
+/**
+ * TODO: Rename `observedTopOfStack` to `presentedIndex` in `NavigationStack`.
+ */
+var navStatePresentedIndex = function(navState) {
+ if (navState.presentedIndex !== undefined) {
+ return navState.presentedIndex;
+ } else {
+ return navState.observedTopOfStack;
+ }
+};
+
+
+/**
+ * The first route is initially rendered using a different style than all
+ * future routes.
+ *
+ * @param {number} index Index of breadcrumb.
+ * @return {object} Style config for initial rendering of index.
+ */
+var initStyle = function(index, presentedIndex) {
+ return index === presentedIndex ? BreadcrumbNavigationBarStyles.Center[index] :
+ index < presentedIndex ? BreadcrumbNavigationBarStyles.Left[index] :
+ BreadcrumbNavigationBarStyles.Right[index];
+};
+
+var BreadcrumbNavigationBar = React.createClass({
+ propTypes: {
+ navigationOperations: PropTypes.shape({
+ push: PropTypes.func,
+ pop: PropTypes.func,
+ replace: PropTypes.func,
+ popToRoute: PropTypes.func,
+ popToTop: PropTypes.func,
+ }),
+ navigationBarRouteMapper: PropTypes.shape({
+ rightContentForRoute: PropTypes.func,
+ titleContentForRoute: PropTypes.func,
+ iconForRoute: PropTypes.func,
+ }),
+ navigationBarStyles: PropTypes.number,
+ },
+
+ _updateIndexProgress: function(progress, index, fromIndex, toIndex) {
+ var amount = toIndex > fromIndex ? progress : (1 - progress);
+ var oldDistToCenter = index - fromIndex;
+ var newDistToCenter = index - toIndex;
+ var interpolate;
+ if (oldDistToCenter > 0 && newDistToCenter === 0 ||
+ newDistToCenter > 0 && oldDistToCenter === 0) {
+ interpolate = Interpolators[index].RightToCenter;
+ } else if (oldDistToCenter < 0 && newDistToCenter === 0 ||
+ newDistToCenter < 0 && oldDistToCenter === 0) {
+ interpolate = Interpolators[index].CenterToLeft;
+ } else if (oldDistToCenter === newDistToCenter) {
+ interpolate = Interpolators[index].RightToCenter;
+ } else {
+ interpolate = Interpolators[index].RightToLeft;
+ }
+
+ if (interpolate.Crumb(CRUMB_PROPS[index].style, amount)) {
+ this.refs['crumb_' + index].setNativeProps(CRUMB_PROPS[index]);
+ }
+ if (interpolate.Icon(ICON_PROPS[index].style, amount)) {
+ this.refs['icon_' + index].setNativeProps(ICON_PROPS[index]);
+ }
+ if (interpolate.Separator(SEPARATOR_PROPS[index].style, amount)) {
+ this.refs['separator_' + index].setNativeProps(SEPARATOR_PROPS[index]);
+ }
+ if (interpolate.Title(TITLE_PROPS[index].style, amount)) {
+ this.refs['title_' + index].setNativeProps(TITLE_PROPS[index]);
+ }
+ var right = this.refs['right_' + index];
+ if (right &&
+ interpolate.RightItem(RIGHT_BUTTON_PROPS[index].style, amount)) {
+ right.setNativeProps(RIGHT_BUTTON_PROPS[index]);
+ }
+ },
+
+ updateProgress: function(progress, fromIndex, toIndex) {
+ var max = Math.max(fromIndex, toIndex);
+ var min = Math.min(fromIndex, toIndex);
+ for (var index = min; index <= max; index++) {
+ this._updateIndexProgress(progress, index, fromIndex, toIndex);
+ }
+ },
+
+ render: function() {
+ var navState = this.props.navState;
+ var icons = navState && navState.routeStack.map(this._renderOrReturnBreadcrumb);
+ var titles = navState.routeStack.map(this._renderOrReturnTitle);
+ var buttons = navState.routeStack.map(this._renderOrReturnRightButton);
+ return (
+
+ {titles}
+ {icons}
+ {buttons}
+
+ );
+ },
+
+ _renderOrReturnBreadcrumb: function(route, index) {
+ var uid = this.props.navState.idStack[index];
+ var navBarRouteMapper = this.props.navigationBarRouteMapper;
+ var navOps = this.props.navigationOperations;
+ var alreadyRendered = this.refs['crumbContainer' + uid];
+ if (alreadyRendered) {
+ // Don't bother re-calculating the children
+ return (
+
+ );
+ }
+ var firstStyles = initStyle(index, navStatePresentedIndex(this.props.navState));
+ return (
+
+
+
+ {navBarRouteMapper.iconForRoute(route, navOps)}
+
+
+ {navBarRouteMapper.separatorForRoute(route, navOps)}
+
+
+
+ );
+ },
+
+ _renderOrReturnTitle: function(route, index) {
+ var navState = this.props.navState;
+ var uid = navState.idStack[index];
+ var alreadyRendered = this.refs['titleContainer' + uid];
+ if (alreadyRendered) {
+ // Don't bother re-calculating the children
+ return (
+
+ );
+ }
+ var navBarRouteMapper = this.props.navigationBarRouteMapper;
+ var titleContent = navBarRouteMapper.titleContentForRoute(
+ navState.routeStack[index],
+ this.props.navigationOperations
+ );
+ var firstStyles = initStyle(index, navStatePresentedIndex(this.props.navState));
+ return (
+
+
+ {titleContent}
+
+
+ );
+ },
+
+ _renderOrReturnRightButton: function(route, index) {
+ var navState = this.props.navState;
+ var navBarRouteMapper = this.props.navigationBarRouteMapper;
+ var uid = navState.idStack[index];
+ var alreadyRendered = this.refs['rightContainer' + uid];
+ if (alreadyRendered) {
+ // Don't bother re-calculating the children
+ return (
+
+ );
+ }
+ var rightContent = navBarRouteMapper.rightContentForRoute(
+ navState.routeStack[index],
+ this.props.navigationOperations
+ );
+ if (!rightContent) {
+ return null;
+ }
+ var firstStyles = initStyle(index, navStatePresentedIndex(this.props.navState));
+ return (
+
+
+ {rightContent}
+
+
+ );
+ },
+});
+
+var styles = StyleSheet.create({
+ breadCrumbContainer: {
+ overflow: 'hidden',
+ position: 'absolute',
+ height: NavigationBarStyles.General.TotalNavHeight,
+ top: 0,
+ left: 0,
+ width: NavigationBarStyles.General.ScreenWidth,
+ },
+});
+
+module.exports = BreadcrumbNavigationBar;
diff --git a/Libraries/CustomComponents/JSNavigationStack/BreadcrumbNavigationBarStyles.ios.js b/Libraries/CustomComponents/JSNavigationStack/BreadcrumbNavigationBarStyles.ios.js
new file mode 100644
index 000000000..c71772c23
--- /dev/null
+++ b/Libraries/CustomComponents/JSNavigationStack/BreadcrumbNavigationBarStyles.ios.js
@@ -0,0 +1,207 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ *
+ * @providesModule BreadcrumbNavigationBarStyles
+ */
+'use strict';
+
+var NavigationBarStyles = require('NavigationBarStyles');
+
+var buildStyleInterpolator = require('buildStyleInterpolator');
+var merge = require('merge');
+
+var SCREEN_WIDTH = NavigationBarStyles.General.ScreenWidth;
+var STATUS_BAR_HEIGHT = NavigationBarStyles.General.StatusBarHeight;
+var NAV_BAR_HEIGHT = NavigationBarStyles.General.NavBarHeight;
+
+var SPACING = 4;
+var ICON_WIDTH = 40;
+var SEPARATOR_WIDTH = 9;
+var CRUMB_WIDTH = ICON_WIDTH + SEPARATOR_WIDTH;
+var RIGHT_BUTTON_WIDTH = 58;
+
+var OPACITY_RATIO = 100;
+var ICON_INACTIVE_OPACITY = 0.6;
+var MAX_BREADCRUMBS = 10;
+
+var CRUMB_BASE = {
+ position: 'absolute',
+ flexDirection: 'row',
+ top: STATUS_BAR_HEIGHT,
+ width: CRUMB_WIDTH,
+ height: NAV_BAR_HEIGHT,
+ backgroundColor: 'transparent',
+};
+
+var ICON_BASE = {
+ width: ICON_WIDTH,
+ height: NAV_BAR_HEIGHT,
+};
+
+var SEPARATOR_BASE = {
+ width: SEPARATOR_WIDTH,
+ height: NAV_BAR_HEIGHT,
+};
+
+var TITLE_BASE = {
+ position: 'absolute',
+ top: STATUS_BAR_HEIGHT,
+ height: NAV_BAR_HEIGHT,
+ backgroundColor: 'transparent',
+};
+
+// For first title styles, make sure first title is centered
+var FIRST_TITLE_BASE = merge(TITLE_BASE, {
+ left: 0,
+ alignItems: 'center',
+ width: SCREEN_WIDTH,
+ height: NAV_BAR_HEIGHT,
+});
+
+var RIGHT_BUTTON_BASE = {
+ position: 'absolute',
+ top: STATUS_BAR_HEIGHT,
+ left: SCREEN_WIDTH - SPACING - RIGHT_BUTTON_WIDTH,
+ overflow: 'hidden',
+ opacity: 1,
+ width: RIGHT_BUTTON_WIDTH,
+ height: NAV_BAR_HEIGHT,
+ backgroundColor: 'transparent',
+};
+
+/**
+ * Precompute crumb styles so that they don't need to be recomputed on every
+ * interaction.
+ */
+var LEFT = [];
+var CENTER = [];
+var RIGHT = [];
+for (var i = 0; i < MAX_BREADCRUMBS; i++) {
+ var crumbLeft = CRUMB_WIDTH * i + SPACING;
+ LEFT[i] = {
+ Crumb: merge(CRUMB_BASE, { left: crumbLeft }),
+ Icon: merge(ICON_BASE, { opacity: ICON_INACTIVE_OPACITY }),
+ Separator: merge(SEPARATOR_BASE, { opacity: 1 }),
+ Title: merge(TITLE_BASE, { left: crumbLeft, opacity: 0 }),
+ RightItem: merge(RIGHT_BUTTON_BASE, { opacity: 0 }),
+ };
+ CENTER[i] = {
+ Crumb: merge(CRUMB_BASE, { left: crumbLeft }),
+ Icon: merge(ICON_BASE, { opacity: 1 }),
+ Separator: merge(SEPARATOR_BASE, { opacity: 0 }),
+ Title: merge(TITLE_BASE, {
+ left: crumbLeft + ICON_WIDTH,
+ opacity: 1,
+ }),
+ RightItem: merge(RIGHT_BUTTON_BASE, { opacity: 1 }),
+ };
+ var crumbRight = SCREEN_WIDTH - 100;
+ RIGHT[i] = {
+ Crumb: merge(CRUMB_BASE, { left: crumbRight}),
+ Icon: merge(ICON_BASE, { opacity: 0 }),
+ Separator: merge(SEPARATOR_BASE, { opacity: 0 }),
+ Title: merge(TITLE_BASE, {
+ left: crumbRight + ICON_WIDTH,
+ opacity: 0,
+ }),
+ RightItem: merge(RIGHT_BUTTON_BASE, { opacity: 0 }),
+ };
+}
+
+// Special case the CENTER state of the first scene.
+CENTER[0] = {
+ Crumb: merge(CRUMB_BASE, {left: SCREEN_WIDTH / 4}),
+ Icon: merge(ICON_BASE, {opacity: 0}),
+ Separator: merge(SEPARATOR_BASE, {opacity: 0}),
+ Title: merge(FIRST_TITLE_BASE, {opacity: 1}),
+ RightItem: CENTER[0].RightItem,
+};
+LEFT[0].Title = merge(FIRST_TITLE_BASE, {left: - SCREEN_WIDTH / 4, opacity: 0});
+RIGHT[0].Title = merge(FIRST_TITLE_BASE, {opacity: 0});
+
+
+var buildIndexSceneInterpolator = function(startStyles, endStyles) {
+ return {
+ Crumb: buildStyleInterpolator({
+ left: {
+ type: 'linear',
+ from: startStyles.Crumb.left,
+ to: endStyles.Crumb.left,
+ min: 0,
+ max: 1,
+ extrapolate: true,
+ },
+ }),
+ Icon: buildStyleInterpolator({
+ opacity: {
+ type: 'linear',
+ from: startStyles.Icon.opacity,
+ to: endStyles.Icon.opacity,
+ min: 0,
+ max: 1,
+ },
+ }),
+ Separator: buildStyleInterpolator({
+ opacity: {
+ type: 'linear',
+ from: startStyles.Separator.opacity,
+ to: endStyles.Separator.opacity,
+ min: 0,
+ max: 1,
+ },
+ }),
+ Title: buildStyleInterpolator({
+ opacity: {
+ type: 'linear',
+ from: startStyles.Title.opacity,
+ to: endStyles.Title.opacity,
+ min: 0,
+ max: 1,
+ },
+ left: {
+ type: 'linear',
+ from: startStyles.Title.left,
+ to: endStyles.Title.left,
+ min: 0,
+ max: 1,
+ extrapolate: true,
+ },
+ }),
+ RightItem: buildStyleInterpolator({
+ opacity: {
+ type: 'linear',
+ from: startStyles.RightItem.opacity,
+ to: endStyles.RightItem.opacity,
+ min: 0,
+ max: 1,
+ round: OPACITY_RATIO,
+ },
+ }),
+ };
+};
+
+var Interpolators = CENTER.map(function(_, ii) {
+ return {
+ // Animating *into* the center stage from the right
+ RightToCenter: buildIndexSceneInterpolator(RIGHT[ii], CENTER[ii]),
+ // Animating out of the center stage, to the left
+ CenterToLeft: buildIndexSceneInterpolator(CENTER[ii], LEFT[ii]),
+ // Both stages (animating *past* the center stage)
+ RightToLeft: buildIndexSceneInterpolator(RIGHT[ii], LEFT[ii]),
+ };
+});
+
+/**
+ * Contains constants that are used in constructing both `StyleSheet`s and
+ * inline styles during transitions.
+ */
+module.exports = {
+ Interpolators,
+ Left: LEFT,
+ Center: CENTER,
+ Right: RIGHT,
+ IconWidth: ICON_WIDTH,
+ IconHeight: NAV_BAR_HEIGHT,
+ SeparatorWidth: SEPARATOR_WIDTH,
+ SeparatorHeight: NAV_BAR_HEIGHT,
+};
diff --git a/Libraries/CustomComponents/JSNavigationStack/NavigationBar.js b/Libraries/CustomComponents/JSNavigationStack/NavigationBar.js
new file mode 100644
index 000000000..b62d4b0cf
--- /dev/null
+++ b/Libraries/CustomComponents/JSNavigationStack/NavigationBar.js
@@ -0,0 +1,164 @@
+/**
+ * @providesModule NavigationBar
+ * @typechecks
+ */
+'use strict';
+
+var React = require('React');
+var NavigationBarStyles = require('NavigationBarStyles');
+var StaticContainer = require('StaticContainer.react');
+var StyleSheet = require('StyleSheet');
+var View = require('View');
+var Text = require('Text');
+
+var COMPONENT_NAMES = ['Title', 'LeftButton', 'RightButton'];
+
+/**
+ * TODO (janzer): Rename `observedTopOfStack` to `presentedIndex` in `NavigationStack`.
+ */
+var navStatePresentedIndex = function(navState) {
+ if (navState.presentedIndex !== undefined) {
+ return navState.presentedIndex;
+ } else {
+ return navState.observedTopOfStack;
+ }
+};
+
+var NavigationBar = React.createClass({
+
+ _getReusableProps: function(
+ /*string*/componentName,
+ /*number*/index
+ ) /*object*/ {
+ if (!this._reusableProps) {
+ this._reusableProps = {};
+ };
+ var propStack = this._reusableProps[componentName];
+ if (!propStack) {
+ propStack = this._reusableProps[componentName] = [];
+ }
+ var props = propStack[index];
+ if (!props) {
+ props = propStack[index] = {style:{}};
+ }
+ return props;
+ },
+
+ _updateIndexProgress: function(
+ /*number*/progress,
+ /*number*/index,
+ /*number*/fromIndex,
+ /*number*/toIndex
+ ) {
+ var amount = toIndex > fromIndex ? progress : (1 - progress);
+ var oldDistToCenter = index - fromIndex;
+ var newDistToCenter = index - toIndex;
+ var interpolate;
+ if (oldDistToCenter > 0 && newDistToCenter === 0 ||
+ newDistToCenter > 0 && oldDistToCenter === 0) {
+ interpolate = NavigationBarStyles.Interpolators.RightToCenter;
+ } else if (oldDistToCenter < 0 && newDistToCenter === 0 ||
+ newDistToCenter < 0 && oldDistToCenter === 0) {
+ interpolate = NavigationBarStyles.Interpolators.CenterToLeft;
+ } else if (oldDistToCenter === newDistToCenter) {
+ interpolate = NavigationBarStyles.Interpolators.RightToCenter;
+ } else {
+ interpolate = NavigationBarStyles.Interpolators.RightToLeft;
+ }
+
+ COMPONENT_NAMES.forEach(function (componentName) {
+ var component = this.refs[componentName + index];
+ var props = this._getReusableProps(componentName, index);
+ if (component && interpolate[componentName](props.style, amount)) {
+ component.setNativeProps(props);
+ }
+ }, this);
+ },
+
+ updateProgress: function(
+ /*number*/progress,
+ /*number*/fromIndex,
+ /*number*/toIndex
+ ) {
+ var max = Math.max(fromIndex, toIndex);
+ var min = Math.min(fromIndex, toIndex);
+ for (var index = min; index <= max; index++) {
+ this._updateIndexProgress(progress, index, fromIndex, toIndex);
+ }
+ },
+
+ render: function() {
+ var navState = this.props.navState;
+ var components = COMPONENT_NAMES.map(function (componentName) {
+ return navState.routeStack.map(
+ this._renderOrReturnComponent.bind(this, componentName)
+ );
+ }, this);
+
+ return (
+
+ {components}
+
+ );
+ },
+
+ _renderOrReturnComponent: function(
+ /*string*/componentName,
+ /*object*/route,
+ /*number*/index
+ ) /*object*/ {
+ var navState = this.props.navState;
+ var navBarRouteMapper = this.props.navigationBarRouteMapper;
+ var uid = navState.idStack[index];
+ var containerRef = componentName + 'Container' + uid;
+ var alreadyRendered = this.refs[containerRef];
+ if (alreadyRendered) {
+ // Don't bother re-calculating the children
+ return (
+
+ );
+ }
+
+ var content = navBarRouteMapper[componentName](
+ navState.routeStack[index],
+ this.props.navigationOperations,
+ index,
+ this.props.navState
+ );
+ if (!content) {
+ return null;
+ }
+
+ var initialStage = index === navStatePresentedIndex(this.props.navState) ?
+ NavigationBarStyles.Stages.Center : NavigationBarStyles.Stages.Left;
+ return (
+
+
+ {content}
+
+
+ );
+ },
+
+});
+
+
+var styles = StyleSheet.create({
+ navBarContainer: {
+ position: 'absolute',
+ height: NavigationBarStyles.General.TotalNavHeight,
+ top: 0,
+ left: 0,
+ width: NavigationBarStyles.General.ScreenWidth,
+ backgroundColor: 'transparent',
+ },
+});
+
+module.exports = NavigationBar;
diff --git a/Libraries/CustomComponents/JSNavigationStack/NavigationBarStyles.js b/Libraries/CustomComponents/JSNavigationStack/NavigationBarStyles.js
new file mode 100644
index 000000000..d27c60db4
--- /dev/null
+++ b/Libraries/CustomComponents/JSNavigationStack/NavigationBarStyles.js
@@ -0,0 +1,155 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ *
+ * @providesModule NavigationBarStyles
+ */
+'use strict';
+
+var Dimensions = require('Dimensions');
+
+var buildStyleInterpolator = require('buildStyleInterpolator');
+var merge = require('merge');
+
+var SCREEN_WIDTH = Dimensions.get('window').width;
+var NAV_BAR_HEIGHT = 44;
+var STATUS_BAR_HEIGHT = 20;
+var NAV_HEIGHT = NAV_BAR_HEIGHT + STATUS_BAR_HEIGHT;
+
+var BASE_STYLES = {
+ Title: {
+ position: 'absolute',
+ top: STATUS_BAR_HEIGHT,
+ left: 0,
+ alignItems: 'center',
+ width: SCREEN_WIDTH,
+ height: NAV_BAR_HEIGHT,
+ backgroundColor: 'transparent',
+ },
+ LeftButton: {
+ position: 'absolute',
+ top: STATUS_BAR_HEIGHT,
+ left: 0,
+ overflow: 'hidden',
+ opacity: 1,
+ width: SCREEN_WIDTH / 3,
+ height: NAV_BAR_HEIGHT,
+ backgroundColor: 'transparent',
+ },
+ RightButton: {
+ position: 'absolute',
+ top: STATUS_BAR_HEIGHT,
+ left: 2 * SCREEN_WIDTH / 3,
+ overflow: 'hidden',
+ opacity: 1,
+ alignItems: 'flex-end',
+ width: SCREEN_WIDTH / 3,
+ height: NAV_BAR_HEIGHT,
+ backgroundColor: 'transparent',
+ },
+};
+
+// There are 3 stages: left, center, right. All previous navigation
+// items are in the left stage. The current navigation item is in the
+// center stage. All upcoming navigation items are in the right stage.
+// Another way to think of the stages is in terms of transitions. When
+// we move forward in the navigation stack, we perform a
+// right-to-center transition on the new navigation item and a
+// center-to-left transition on the current navigation item.
+var Stages = {
+ Left: {
+ Title: merge(BASE_STYLES.Title, { left: - SCREEN_WIDTH / 2, opacity: 0 }),
+ LeftButton: merge(BASE_STYLES.LeftButton, { left: - SCREEN_WIDTH / 3, opacity: 1 }),
+ RightButton: merge(BASE_STYLES.RightButton, { left: SCREEN_WIDTH / 3, opacity: 0 }),
+ },
+ Center: {
+ Title: merge(BASE_STYLES.Title, { left: 0, opacity: 1 }),
+ LeftButton: merge(BASE_STYLES.LeftButton, { left: 0, opacity: 1 }),
+ RightButton: merge(BASE_STYLES.RightButton, { left: 2 * SCREEN_WIDTH / 3 - 0, opacity: 1 }),
+ },
+ Right: {
+ Title: merge(BASE_STYLES.Title, { left: SCREEN_WIDTH / 2, opacity: 0 }),
+ LeftButton: merge(BASE_STYLES.LeftButton, { left: 0, opacity: 0 }),
+ RightButton: merge(BASE_STYLES.RightButton, { left: SCREEN_WIDTH, opacity: 0 }),
+ },
+};
+
+
+var opacityRatio = 100;
+
+function buildSceneInterpolators(startStyles, endStyles) {
+ return {
+ Title: buildStyleInterpolator({
+ opacity: {
+ type: 'linear',
+ from: startStyles.Title.opacity,
+ to: endStyles.Title.opacity,
+ min: 0,
+ max: 1,
+ },
+ left: {
+ type: 'linear',
+ from: startStyles.Title.left,
+ to: endStyles.Title.left,
+ min: 0,
+ max: 1,
+ extrapolate: true,
+ },
+ }),
+ LeftButton: buildStyleInterpolator({
+ opacity: {
+ type: 'linear',
+ from: startStyles.LeftButton.opacity,
+ to: endStyles.LeftButton.opacity,
+ min: 0,
+ max: 1,
+ round: opacityRatio,
+ },
+ left: {
+ type: 'linear',
+ from: startStyles.LeftButton.left,
+ to: endStyles.LeftButton.left,
+ min: 0,
+ max: 1,
+ },
+ }),
+ RightButton: buildStyleInterpolator({
+ opacity: {
+ type: 'linear',
+ from: startStyles.RightButton.opacity,
+ to: endStyles.RightButton.opacity,
+ min: 0,
+ max: 1,
+ round: opacityRatio,
+ },
+ left: {
+ type: 'linear',
+ from: startStyles.RightButton.left,
+ to: endStyles.RightButton.left,
+ min: 0,
+ max: 1,
+ extrapolate: true,
+ },
+ }),
+ };
+}
+
+var Interpolators = {
+ // Animating *into* the center stage from the right
+ RightToCenter: buildSceneInterpolators(Stages.Right, Stages.Center),
+ // Animating out of the center stage, to the left
+ CenterToLeft: buildSceneInterpolators(Stages.Center, Stages.Left),
+ // Both stages (animating *past* the center stage)
+ RightToLeft: buildSceneInterpolators(Stages.Right, Stages.Left),
+};
+
+
+module.exports = {
+ General: {
+ NavBarHeight: NAV_BAR_HEIGHT,
+ StatusBarHeight: STATUS_BAR_HEIGHT,
+ TotalNavHeight: NAV_HEIGHT,
+ ScreenWidth: SCREEN_WIDTH,
+ },
+ Interpolators,
+ Stages,
+};
diff --git a/Libraries/CustomComponents/JSNavigationStackAnimationConfigs.js b/Libraries/CustomComponents/JSNavigationStackAnimationConfigs.js
new file mode 100644
index 000000000..b2d813704
--- /dev/null
+++ b/Libraries/CustomComponents/JSNavigationStackAnimationConfigs.js
@@ -0,0 +1,279 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ *
+ * @providesModule JSNavigationStackAnimationConfigs
+ */
+'use strict';
+
+var Dimensions = require('Dimensions');
+var PixelRatio = require('PixelRatio');
+
+var buildStyleInterpolator = require('buildStyleInterpolator');
+var merge = require('merge');
+
+var SCREEN_WIDTH = Dimensions.get('window').width;
+var SCREEN_HEIGHT = Dimensions.get('window').height;
+
+var ToTheLeft = {
+ // Rotate *requires* you to break out each individual component of
+ // rotation (x, y, z, w)
+ transformTranslate: {
+ from: {x: 0, y: 0, z: 0},
+ to: {x: -Math.round(Dimensions.get('window').width * 0.3), y: 0, z: 0},
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: true,
+ round: PixelRatio.get(),
+ },
+ // Uncomment to try rotation:
+ // Quick guide to reasoning about rotations:
+ // http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-17-quaternions/#Quaternions
+ // transformRotateRadians: {
+ // from: {x: 0, y: 0, z: 0, w: 1},
+ // to: {x: 0, y: 0, z: -0.47, w: 0.87},
+ // min: 0,
+ // max: 1,
+ // type: 'linear',
+ // extrapolate: true
+ // },
+ transformScale: {
+ from: {x: 1, y: 1, z: 1},
+ to: {x: 0.95, y: 0.95, z: 1},
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: true
+ },
+ opacity: {
+ from: 1,
+ to: 0.3,
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: false,
+ round: 100,
+ },
+ translateX: {
+ from: 0,
+ to: -Math.round(Dimensions.get('window').width * 0.3),
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: true,
+ round: PixelRatio.get(),
+ },
+ scaleX: {
+ from: 1,
+ to: 0.95,
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: true
+ },
+ scaleY: {
+ from: 1,
+ to: 0.95,
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: true
+ },
+};
+
+var FromTheRight = {
+ opacity: {
+ value: 1.0,
+ type: 'constant',
+ },
+
+ transformTranslate: {
+ from: {x: Dimensions.get('window').width, y: 0, z: 0},
+ to: {x: 0, y: 0, z: 0},
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: true,
+ round: PixelRatio.get(),
+ },
+
+ translateX: {
+ from: Dimensions.get('window').width,
+ to: 0,
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: true,
+ round: PixelRatio.get(),
+ },
+
+ scaleX: {
+ value: 1,
+ type: 'constant',
+ },
+ scaleY: {
+ value: 1,
+ type: 'constant',
+ },
+};
+
+
+var ToTheBack = {
+ // Rotate *requires* you to break out each individual component of
+ // rotation (x, y, z, w)
+ transformTranslate: {
+ from: {x: 0, y: 0, z: 0},
+ to: {x: 0, y: 0, z: 0},
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: true,
+ round: PixelRatio.get(),
+ },
+ transformScale: {
+ from: {x: 1, y: 1, z: 1},
+ to: {x: 0.95, y: 0.95, z: 1},
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: true
+ },
+ opacity: {
+ from: 1,
+ to: 0.3,
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: false,
+ round: 100,
+ },
+ scaleX: {
+ from: 1,
+ to: 0.95,
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: true
+ },
+ scaleY: {
+ from: 1,
+ to: 0.95,
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: true
+ },
+};
+
+var FromTheFront = {
+ opacity: {
+ value: 1.0,
+ type: 'constant',
+ },
+
+ transformTranslate: {
+ from: {x: 0, y: Dimensions.get('window').height, z: 0},
+ to: {x: 0, y: 0, z: 0},
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: true,
+ round: PixelRatio.get(),
+ },
+ translateY: {
+ from: Dimensions.get('window').height,
+ to: 0,
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: true,
+ round: PixelRatio.get(),
+ },
+ scaleX: {
+ value: 1,
+ type: 'constant',
+ },
+ scaleY: {
+ value: 1,
+ type: 'constant',
+ },
+};
+
+
+var Interpolators = {
+ Vertical: {
+ into: buildStyleInterpolator(FromTheFront),
+ out: buildStyleInterpolator(ToTheBack),
+ },
+ Horizontal: {
+ into: buildStyleInterpolator(FromTheRight),
+ out: buildStyleInterpolator(ToTheLeft),
+ },
+};
+
+
+// These are meant to mimic iOS default behavior
+var PastPointOfNoReturn = {
+ horizontal: function(location) {
+ return location > SCREEN_WIDTH * 3 / 5;
+ },
+ vertical: function(location) {
+ return location > SCREEN_HEIGHT * 3 / 5;
+ },
+};
+
+var BaseConfig = {
+ // When false, all gestures are ignored for this scene
+ enableGestures: true,
+
+ // How far the swipe must drag to start transitioning
+ gestureDetectMovement: 2,
+
+ // Amplitude of release velocity that is considered still
+ notMoving: 0.3,
+
+ // Velocity to start at when transitioning without gesture
+ defaultTransitionVelocity: 1.5,
+
+ // Fraction of directional move required.
+ directionRatio: 0.66,
+
+ // Velocity to transition with when the gesture release was "not moving"
+ snapVelocity: 2,
+
+ // Rebound spring parameters when transitioning FROM this scene
+ springFriction: 26,
+ springTension: 200,
+
+ // Defaults for horizontal transitioning:
+
+ isVertical: false,
+ screenDimension: SCREEN_WIDTH,
+
+ // Region that can trigger swipe. iOS default is 30px from the left edge
+ edgeHitWidth: 30,
+
+ // Point at which a non-velocity release will cause nav pop
+ pastPointOfNoReturn: PastPointOfNoReturn.horizontal,
+
+ // Animation interpolators for this transition
+ interpolators: Interpolators.Horizontal,
+};
+
+var JSNavigationStackAnimationConfigs = {
+ PushFromRight: merge(BaseConfig, {
+ // We will want to customize this soon
+ }),
+ FloatFromRight: merge(BaseConfig, {
+ // We will want to customize this soon
+ }),
+ FloatFromBottom: merge(BaseConfig, {
+ edgeHitWidth: 150,
+ interpolators: Interpolators.Vertical,
+ isVertical: true,
+ pastPointOfNoReturn: PastPointOfNoReturn.vertical,
+ screenDimension: SCREEN_HEIGHT,
+ }),
+};
+
+module.exports = JSNavigationStackAnimationConfigs;
diff --git a/Libraries/Interaction/InteractionMixin.js b/Libraries/Interaction/InteractionMixin.js
new file mode 100644
index 000000000..633159c1f
--- /dev/null
+++ b/Libraries/Interaction/InteractionMixin.js
@@ -0,0 +1,49 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ *
+ * @providesModule InteractionMixin
+ */
+'use strict';
+
+var InteractionManager = require('InteractionManager');
+
+/**
+ * This mixin provides safe versions of InteractionManager start/end methods
+ * that ensures `clearInteractionHandle` is always called
+ * once per start, even if the component is unmounted.
+ */
+var InteractionMixin = {
+ componentWillUnmount: function() {
+ while (this._interactionMixinHandles.length) {
+ InteractionManager.clearInteractionHandle(
+ this._interactionMixinHandles.pop()
+ );
+ }
+ },
+
+ _interactionMixinHandles: [],
+
+ createInteractionHandle: function() {
+ var handle = InteractionManager.createInteractionHandle();
+ this._interactionMixinHandles.push(handle);
+ return handle;
+ },
+
+ clearInteractionHandle: function(clearHandle) {
+ InteractionManager.clearInteractionHandle(clearHandle);
+ this._interactionMixinHandles = this._interactionMixinHandles.filter(
+ handle => handle !== clearHandle
+ );
+ },
+
+ /**
+ * Schedule work for after all interactions have completed.
+ *
+ * @param {function} callback
+ */
+ runAfterInteractions: function(callback) {
+ InteractionManager.runAfterInteractions(callback);
+ },
+};
+
+module.exports = InteractionMixin;
diff --git a/Libraries/Utilities/Backstack.ios.js b/Libraries/Utilities/Backstack.ios.js
new file mode 100644
index 000000000..00a538668
--- /dev/null
+++ b/Libraries/Utilities/Backstack.ios.js
@@ -0,0 +1,16 @@
+/**
+ * To lower the risk of breaking things on iOS, we are stubbing out the
+ * BackStack for now. See Backstack.android.js
+ *
+ * @providesModule Backstack
+ */
+
+'use strict';
+
+var Backstack = {
+ pushNavigation: () => {},
+ resetToBefore: () => {},
+ removeComponentHistory: () => {},
+};
+
+module.exports = Backstack;
diff --git a/Libraries/Utilities/CSSVarConfig.js b/Libraries/Utilities/CSSVarConfig.js
new file mode 100644
index 000000000..cb018be0a
--- /dev/null
+++ b/Libraries/Utilities/CSSVarConfig.js
@@ -0,0 +1,62 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ *
+ * @providesModule CSSVarConfig
+ */
+'use strict';
+
+// this a partial list of the contants in CSSConstants:: from PHP that are applicable to mobile
+
+module.exports = {
+ 'fbui-accent-blue': '#5890ff',
+ 'fbui-blue-90': '#4e69a2',
+ 'fbui-blue-80': '#627aad',
+ 'fbui-blue-70': '#758ab7',
+ 'fbui-blue-60': '#899bc1',
+ 'fbui-blue-50': '#9daccb',
+ 'fbui-blue-40': '#b1bdd6',
+ 'fbui-blue-30': '#c4cde0',
+ 'fbui-blue-20': '#d8deea',
+ 'fbui-blue-10': '#ebeef4',
+ 'fbui-blue-5': '#f5f7fa',
+ 'fbui-blue-2': '#fbfcfd',
+ 'fbui-blueblack-90': '#06090f',
+ 'fbui-blueblack-80': '#0c121e',
+ 'fbui-blueblack-70': '#121b2e',
+ 'fbui-blueblack-60': '#18243d',
+ 'fbui-blueblack-50': '#1e2d4c',
+ 'fbui-blueblack-40': '#23355b',
+ 'fbui-blueblack-30': '#293e6b',
+ 'fbui-blueblack-20': '#2f477a',
+ 'fbui-blueblack-10': '#355089',
+ 'fbui-blueblack-5': '#385490',
+ 'fbui-blueblack-2': '#3a5795',
+ 'fbui-bluegray-90': '#080a10',
+ 'fbui-bluegray-80': '#141823',
+ 'fbui-bluegray-70': '#232937',
+ 'fbui-bluegray-60': '#373e4d',
+ 'fbui-bluegray-50': '#4e5665',
+ 'fbui-bluegray-40': '#6a7180',
+ 'fbui-bluegray-30': '#9197a3',
+ 'fbui-bluegray-20': '#bdc1c9',
+ 'fbui-bluegray-10': '#dcdee3',
+ 'fbui-bluegray-5': '#e9eaed',
+ 'fbui-bluegray-2': '#f6f7f8',
+ 'fbui-gray-90': '#191919',
+ 'fbui-gray-80': '#333333',
+ 'fbui-gray-70': '#4c4c4c',
+ 'fbui-gray-60': '#666666',
+ 'fbui-gray-50': '#7f7f7f',
+ 'fbui-gray-40': '#999999',
+ 'fbui-gray-30': '#b2b2b2',
+ 'fbui-gray-20': '#cccccc',
+ 'fbui-gray-10': '#e5e5e5',
+ 'fbui-gray-5': '#f2f2f2',
+ 'fbui-gray-2': '#fafafa',
+ 'fbui-red': '#da2929',
+ 'fbui-error': '#ce0d24',
+ 'x-mobile-dark-text': '#4e5665',
+ 'x-mobile-medium-text': '#6a7180',
+ 'x-mobile-light-text': '#9197a3',
+ 'x-mobile-base-wash': '#dcdee3',
+};
diff --git a/Libraries/Utilities/buildStyleInterpolator.js b/Libraries/Utilities/buildStyleInterpolator.js
new file mode 100644
index 000000000..67f07cb41
--- /dev/null
+++ b/Libraries/Utilities/buildStyleInterpolator.js
@@ -0,0 +1,559 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ *
+ * @providesModule buildStyleInterpolator
+ */
+
+/**
+ * Cannot "use strict" because we must use eval in this file.
+ */
+
+var keyOf = require('keyOf');
+
+var X_DIM = keyOf({x: null});
+var Y_DIM = keyOf({y: null});
+var Z_DIM = keyOf({z: null});
+var W_DIM = keyOf({w: null});
+
+var TRANSFORM_ROTATE_NAME = keyOf({transformRotateRadians: null});
+
+var ShouldAllocateReusableOperationVars = {
+ transformRotateRadians: true,
+ transformScale: true,
+ transformTranslate: true,
+};
+
+var InitialOperationField = {
+ transformRotateRadians: [0, 0, 0, 1],
+ transformTranslate: [0, 0, 0],
+ transformScale: [1, 1, 1],
+};
+
+
+/**
+ * Creates a highly specialized animation function that may be evaluated every
+ * frame. For example:
+ *
+ * var ToTheLeft = {
+ * opacity: {
+ * from: 1,
+ * to: 0.7,
+ * min: 0,
+ * max: 1,
+ * type: 'linear',
+ * extrapolate: false,
+ * round: 100,
+ * },
+ * left: {
+ * from: 0,
+ * to: -SCREEN_WIDTH * 0.3,
+ * min: 0,
+ * max: 1,
+ * type: 'linear',
+ * extrapolate: true,
+ * round: PixelRatio.get(),
+ * },
+ * };
+ *
+ * var toTheLeft = buildStyleInterpolator(ToTheLeft);
+ *
+ * Would returns a specialized function of the form:
+ *
+ * function(result, value) {
+ * var didChange = false;
+ * var nextScalarVal;
+ * var ratio;
+ * ratio = (value - 0) / 1;
+ * ratio = ratio > 1 ? 1 : (ratio < 0 ? 0 : ratio);
+ * nextScalarVal = Math.round(100 * (1 * (1 - ratio) + 0.7 * ratio)) / 100;
+ * if (!didChange) {
+ * var prevVal = result.opacity;
+ * result.opacity = nextScalarVal;
+ * didChange = didChange || (nextScalarVal !== prevVal);
+ * } else {
+ * result.opacity = nextScalarVal;
+ * }
+ * ratio = (value - 0) / 1;
+ * nextScalarVal = Math.round(2 * (0 * (1 - ratio) + -30 * ratio)) / 2;
+ * if (!didChange) {
+ * var prevVal = result.left;
+ * result.left = nextScalarVal;
+ * didChange = didChange || (nextScalarVal !== prevVal);
+ * } else {
+ * result.left = nextScalarVal;
+ * }
+ * return didChange;
+ * }
+ */
+
+var ARGUMENT_NAMES_RE = /([^\s,]+)/g;
+/**
+ * This is obviously a huge hack. Proper tooling would allow actual inlining.
+ * This only works in a few limited cases (where there is no function return
+ * value, and the function operates mutatively on parameters).
+ *
+ * Example:
+ *
+ *
+ * var inlineMe(a, b) {
+ * a = b + b;
+ * };
+ *
+ * inline(inlineMe, ['hi', 'bye']); // "hi = bye + bye;"
+ *
+ * @param {function} func Any simple function whos arguments can be replaced via a regex.
+ * @param {array} replaceWithArgs Corresponding names of variables
+ * within an environment, to replace `func` args with.
+ * @return {string} Resulting function body string.
+ */
+var inline = function(func, replaceWithArgs) {
+ var fnStr = func.toString();
+ var parameterNames = fnStr.slice(fnStr.indexOf('(') + 1, fnStr.indexOf(')'))
+ .match(ARGUMENT_NAMES_RE) ||
+ [];
+ var replaceRegexStr = parameterNames.map(function(paramName) {
+ return '\\b' + paramName + '\\b';
+ }).join('|');
+ var replaceRegex = new RegExp(replaceRegexStr, 'g');
+ var fnBody = fnStr.substring(fnStr.indexOf('{') + 1, fnStr.lastIndexOf('}') - 1);
+ var newFnBody = fnBody.replace(replaceRegex, function(parameterName) {
+ var indexInParameterNames = parameterNames.indexOf(parameterName);
+ var replacementName = replaceWithArgs[indexInParameterNames];
+ return replacementName;
+ });
+ return newFnBody.split('\n');
+};
+
+/**
+ * Simply a convenient way to inline functions using the function's toString
+ * method.
+ */
+var MatrixOps = {
+ unroll: function(matVar, m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15) {
+ m0 = matVar[0];
+ m1 = matVar[1];
+ m2 = matVar[2];
+ m3 = matVar[3];
+ m4 = matVar[4];
+ m5 = matVar[5];
+ m6 = matVar[6];
+ m7 = matVar[7];
+ m8 = matVar[8];
+ m9 = matVar[9];
+ m10 = matVar[10];
+ m11 = matVar[11];
+ m12 = matVar[12];
+ m13 = matVar[13];
+ m14 = matVar[14];
+ m15 = matVar[15];
+ },
+
+ matrixDiffers: function(retVar, matVar, m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15) {
+ retVar = retVar ||
+ m0 !== matVar[0] ||
+ m1 !== matVar[1] ||
+ m2 !== matVar[2] ||
+ m3 !== matVar[3] ||
+ m4 !== matVar[4] ||
+ m5 !== matVar[5] ||
+ m6 !== matVar[6] ||
+ m7 !== matVar[7] ||
+ m8 !== matVar[8] ||
+ m9 !== matVar[9] ||
+ m10 !== matVar[10] ||
+ m11 !== matVar[11] ||
+ m12 !== matVar[12] ||
+ m13 !== matVar[13] ||
+ m14 !== matVar[14] ||
+ m15 !== matVar[15];
+ },
+
+ transformScale: function(matVar, opVar) {
+ // Scaling matVar by opVar
+ var x = opVar[0];
+ var y = opVar[1];
+ var z = opVar[2];
+ matVar[0] = matVar[0] * x;
+ matVar[1] = matVar[1] * x;
+ matVar[2] = matVar[2] * x;
+ matVar[3] = matVar[3] * x;
+ matVar[4] = matVar[4] * y;
+ matVar[5] = matVar[5] * y;
+ matVar[6] = matVar[6] * y;
+ matVar[7] = matVar[7] * y;
+ matVar[8] = matVar[8] * z;
+ matVar[9] = matVar[9] * z;
+ matVar[10] = matVar[10] * z;
+ matVar[11] = matVar[11] * z;
+ matVar[12] = matVar[12];
+ matVar[13] = matVar[13];
+ matVar[14] = matVar[14];
+ matVar[15] = matVar[15];
+ },
+
+ /**
+ * All of these matrix transforms are not general purpose utilities, and are
+ * only suitable for being inlined for the use of building up interpolators.
+ */
+ transformTranslate: function(matVar, opVar) {
+ // Translating matVar by opVar
+ var x = opVar[0];
+ var y = opVar[1];
+ var z = opVar[2];
+ matVar[12] = matVar[0] * x + matVar[4] * y + matVar[8] * z + matVar[12];
+ matVar[13] = matVar[1] * x + matVar[5] * y + matVar[9] * z + matVar[13];
+ matVar[14] = matVar[2] * x + matVar[6] * y + matVar[10] * z + matVar[14];
+ matVar[15] = matVar[3] * x + matVar[7] * y + matVar[11] * z + matVar[15];
+ },
+
+ /**
+ * @param {array} matVar Both the input, and the output matrix.
+ * @param {quaternion specification} q Four element array describing rotation.
+ */
+ transformRotateRadians: function(matVar, q) {
+ // Rotating matVar by q
+ var xQuat = q[0], yQuat = q[1], zQuat = q[2], wQuat = q[3];
+ var x2Quat = xQuat + xQuat;
+ var y2Quat = yQuat + yQuat;
+ var z2Quat = zQuat + zQuat;
+ var xxQuat = xQuat * x2Quat;
+ var xyQuat = xQuat * y2Quat;
+ var xzQuat = xQuat * z2Quat;
+ var yyQuat = yQuat * y2Quat;
+ var yzQuat = yQuat * z2Quat;
+ var zzQuat = zQuat * z2Quat;
+ var wxQuat = wQuat * x2Quat;
+ var wyQuat = wQuat * y2Quat;
+ var wzQuat = wQuat * z2Quat;
+ // Step 1: Inlines the construction of a quaternion matrix (`quatMat`)
+ var quatMat0 = 1 - (yyQuat + zzQuat);
+ var quatMat1 = xyQuat + wzQuat;
+ var quatMat2 = xzQuat - wyQuat;
+ var quatMat4 = xyQuat - wzQuat;
+ var quatMat5 = 1 - (xxQuat + zzQuat);
+ var quatMat6 = yzQuat + wxQuat;
+ var quatMat8 = xzQuat + wyQuat;
+ var quatMat9 = yzQuat - wxQuat;
+ var quatMat10 = 1 - (xxQuat + yyQuat);
+ // quatMat3/7/11/12/13/14 = 0, quatMat15 = 1
+
+ // Step 2: Inlines multiplication, takes advantage of constant quatMat cells
+ var a00 = matVar[0];
+ var a01 = matVar[1];
+ var a02 = matVar[2];
+ var a03 = matVar[3];
+ var a10 = matVar[4];
+ var a11 = matVar[5];
+ var a12 = matVar[6];
+ var a13 = matVar[7];
+ var a20 = matVar[8];
+ var a21 = matVar[9];
+ var a22 = matVar[10];
+ var a23 = matVar[11];
+
+ var b0 = quatMat0, b1 = quatMat1, b2 = quatMat2;
+ matVar[0] = b0 * a00 + b1 * a10 + b2 * a20;
+ matVar[1] = b0 * a01 + b1 * a11 + b2 * a21;
+ matVar[2] = b0 * a02 + b1 * a12 + b2 * a22;
+ matVar[3] = b0 * a03 + b1 * a13 + b2 * a23;
+ b0 = quatMat4; b1 = quatMat5; b2 = quatMat6;
+ matVar[4] = b0 * a00 + b1 * a10 + b2 * a20;
+ matVar[5] = b0 * a01 + b1 * a11 + b2 * a21;
+ matVar[6] = b0 * a02 + b1 * a12 + b2 * a22;
+ matVar[7] = b0 * a03 + b1 * a13 + b2 * a23;
+ b0 = quatMat8; b1 = quatMat9; b2 = quatMat10;
+ matVar[8] = b0 * a00 + b1 * a10 + b2 * a20;
+ matVar[9] = b0 * a01 + b1 * a11 + b2 * a21;
+ matVar[10] = b0 * a02 + b1 * a12 + b2 * a22;
+ matVar[11] = b0 * a03 + b1 * a13 + b2 * a23;
+ }
+};
+
+// Optimized version of general operation applications that can be used when
+// the target matrix is known to be the identity matrix.
+var MatrixOpsInitial = {
+ transformScale: function(matVar, opVar) {
+ // Scaling matVar known to be identity by opVar
+ matVar[0] = opVar[0];
+ matVar[1] = 0;
+ matVar[2] = 0;
+ matVar[3] = 0;
+ matVar[4] = 0;
+ matVar[5] = opVar[1];
+ matVar[6] = 0;
+ matVar[7] = 0;
+ matVar[8] = 0;
+ matVar[9] = 0;
+ matVar[10] = opVar[2];
+ matVar[11] = 0;
+ matVar[12] = 0;
+ matVar[13] = 0;
+ matVar[14] = 0;
+ matVar[15] = 1;
+ },
+
+ transformTranslate: function(matVar, opVar) {
+ // Translating matVar known to be identity by opVar';
+ matVar[0] = 1;
+ matVar[1] = 0;
+ matVar[2] = 0;
+ matVar[3] = 0;
+ matVar[4] = 0;
+ matVar[5] = 1;
+ matVar[6] = 0;
+ matVar[7] = 0;
+ matVar[8] = 0;
+ matVar[9] = 0;
+ matVar[10] = 1;
+ matVar[11] = 0;
+ matVar[12] = opVar[0];
+ matVar[13] = opVar[1];
+ matVar[14] = opVar[2];
+ matVar[15] = 1;
+ },
+
+ /**
+ * @param {array} matVar Both the input, and the output matrix - assumed to be
+ * identity.
+ * @param {quaternion specification} q Four element array describing rotation.
+ */
+ transformRotateRadians: function(matVar, q) {
+
+ // Rotating matVar which is known to be identity by q
+ var xQuat = q[0], yQuat = q[1], zQuat = q[2], wQuat = q[3];
+ var x2Quat = xQuat + xQuat;
+ var y2Quat = yQuat + yQuat;
+ var z2Quat = zQuat + zQuat;
+ var xxQuat = xQuat * x2Quat;
+ var xyQuat = xQuat * y2Quat;
+ var xzQuat = xQuat * z2Quat;
+ var yyQuat = yQuat * y2Quat;
+ var yzQuat = yQuat * z2Quat;
+ var zzQuat = zQuat * z2Quat;
+ var wxQuat = wQuat * x2Quat;
+ var wyQuat = wQuat * y2Quat;
+ var wzQuat = wQuat * z2Quat;
+ // Step 1: Inlines the construction of a quaternion matrix (`quatMat`)
+ var quatMat0 = 1 - (yyQuat + zzQuat);
+ var quatMat1 = xyQuat + wzQuat;
+ var quatMat2 = xzQuat - wyQuat;
+ var quatMat4 = xyQuat - wzQuat;
+ var quatMat5 = 1 - (xxQuat + zzQuat);
+ var quatMat6 = yzQuat + wxQuat;
+ var quatMat8 = xzQuat + wyQuat;
+ var quatMat9 = yzQuat - wxQuat;
+ var quatMat10 = 1 - (xxQuat + yyQuat);
+ // quatMat3/7/11/12/13/14 = 0, quatMat15 = 1
+
+ // Step 2: Inlines the multiplication with identity matrix.
+ var b0 = quatMat0, b1 = quatMat1, b2 = quatMat2;
+ matVar[0] = b0;
+ matVar[1] = b1;
+ matVar[2] = b2;
+ matVar[3] = 0;
+ b0 = quatMat4; b1 = quatMat5; b2 = quatMat6;
+ matVar[4] = b0;
+ matVar[5] = b1;
+ matVar[6] = b2;
+ matVar[7] = 0;
+ b0 = quatMat8; b1 = quatMat9; b2 = quatMat10;
+ matVar[8] = b0;
+ matVar[9] = b1;
+ matVar[10] = b2;
+ matVar[11] = 0;
+ matVar[12] = 0;
+ matVar[13] = 0;
+ matVar[14] = 0;
+ matVar[15] = 1;
+ }
+};
+
+
+var setNextValAndDetectChange = function(name, tmpVarName) {
+ return (
+ ' if (!didChange) {\n' +
+ ' var prevVal = result.' + name +';\n' +
+ ' result.' + name + ' = ' + tmpVarName + ';\n' +
+ ' didChange = didChange || (' + tmpVarName + ' !== prevVal);\n' +
+ ' } else {\n' +
+ ' result.' + name + ' = ' + tmpVarName + ';\n' +
+ ' }\n'
+ );
+};
+
+var computeNextValLinear = function(anim, from, to, tmpVarName) {
+ var hasRoundRatio = 'round' in anim;
+ var roundRatio = anim.round;
+ var fn = ' ratio = (value - ' + anim.min + ') / ' + (anim.max - anim.min) + ';\n';
+ if (!anim.extrapolate) {
+ fn += ' ratio = ratio > 1 ? 1 : (ratio < 0 ? 0 : ratio);\n';
+ }
+
+ var roundOpen = (hasRoundRatio ? 'Math.round(' + roundRatio + ' * ' : '' );
+ var roundClose = (hasRoundRatio ? ') / ' + roundRatio : '' );
+ fn +=
+ ' ' + tmpVarName + ' = ' +
+ roundOpen +
+ '(' + from + ' * (1 - ratio) + ' + to + ' * ratio)' +
+ roundClose + ';\n';
+ return fn;
+};
+
+var computeNextValLinearScalar = function(anim) {
+ return computeNextValLinear(anim, anim.from, anim.to, 'nextScalarVal');
+};
+
+var computeNextValConstant = function(anim) {
+ var constantExpression = JSON.stringify(anim.value);
+ return ' nextScalarVal = ' + constantExpression + ';\n';
+};
+
+var computeNextValStep = function(anim) {
+ return (
+ ' nextScalarVal = value >= ' +
+ (anim.threshold + ' ? ' + anim.to + ' : ' + anim.from) + ';\n'
+ );
+};
+
+var computeNextValIdentity = function(anim) {
+ return ' nextScalarVal = value;\n';
+};
+
+var operationVar = function(name) {
+ return name + 'ReuseOp';
+};
+
+var createReusableOperationVars = function(anims) {
+ var ret = '';
+ for (var name in anims) {
+ if (ShouldAllocateReusableOperationVars[name]) {
+ ret += 'var ' + operationVar(name) + ' = [];\n';
+ }
+ }
+ return ret;
+};
+
+var newlines = function(statements) {
+ return '\n' + statements.join('\n') + '\n';
+};
+
+/**
+ * @param {Animation} anim Configuration entry.
+ * @param {key} dimension Key to examine in `from`/`to`.
+ * @param {number} index Field in operationVar to set.
+ * @return {string} Code that sets the operation variable's field.
+ */
+var computeNextMatrixOperationField = function(anim, name, dimension, index) {
+ var fieldAccess = operationVar(name) + '[' + index + ']';
+ if (anim.from[dimension] !== undefined && anim.to[dimension] !== undefined) {
+ return ' ' + anim.from[dimension] !== anim.to[dimension] ?
+ computeNextValLinear(anim, anim.from[dimension], anim.to[dimension], fieldAccess) :
+ fieldAccess + ' = ' + anim.from[dimension] + ';';
+ } else {
+ return ' ' + fieldAccess + ' = ' + InitialOperationField[name][index] + ';';
+ }
+};
+
+var unrolledVars = [];
+for (var varIndex = 0; varIndex < 16; varIndex++) {
+ unrolledVars.push('m' + varIndex);
+}
+var setNextMatrixAndDetectChange = function(orderedMatrixOperations) {
+ var fn = [
+ ' var transformMatrix = result.transformMatrix !== undefined ? ' +
+ 'result.transformMatrix : (result.transformMatrix = []);'
+ ];
+ fn.push.apply(
+ fn,
+ inline(MatrixOps.unroll, ['transformMatrix'].concat(unrolledVars))
+ );
+ for (var i = 0; i < orderedMatrixOperations.length; i++) {
+ var opName = orderedMatrixOperations[i];
+ if (i === 0) {
+ fn.push.apply(
+ fn,
+ inline(MatrixOpsInitial[opName], ['transformMatrix', operationVar(opName)])
+ );
+ } else {
+ fn.push.apply(
+ fn,
+ inline(MatrixOps[opName], ['transformMatrix', operationVar(opName)])
+ );
+ }
+ }
+ fn.push.apply(
+ fn,
+ inline(MatrixOps.matrixDiffers, ['didChange', 'transformMatrix'].concat(unrolledVars))
+ );
+ return fn;
+};
+
+var InterpolateMatrix = {
+ transformTranslate: true,
+ transformRotateRadians: true,
+ transformScale: true,
+};
+
+var createFunctionString = function(anims) {
+ // We must track the order they appear in so transforms are applied in the
+ // correct order.
+ var orderedMatrixOperations = [];
+
+ // Wrapping function allows the final function to contain state (for
+ // caching).
+ var fn = 'return (function() {\n';
+ fn += createReusableOperationVars(anims);
+ fn += 'return function(result, value) {\n';
+ fn += ' var didChange = false;\n';
+ fn += ' var nextScalarVal;\n';
+ fn += ' var ratio;\n';
+
+ for (var name in anims) {
+ var anim = anims[name];
+ if (anim.type === 'linear') {
+ if (InterpolateMatrix[name]) {
+ orderedMatrixOperations.push(name);
+ var setOperations = [
+ computeNextMatrixOperationField(anim, name, X_DIM, 0),
+ computeNextMatrixOperationField(anim, name, Y_DIM, 1),
+ computeNextMatrixOperationField(anim, name, Z_DIM, 2)
+ ];
+ if (name === TRANSFORM_ROTATE_NAME) {
+ setOperations.push(computeNextMatrixOperationField(anim, name, W_DIM, 3));
+ }
+ fn += newlines(setOperations);
+ } else {
+ fn += computeNextValLinearScalar(anim, 'nextScalarVal');
+ fn += setNextValAndDetectChange(name, 'nextScalarVal');
+ }
+ } else if (anim.type === 'constant') {
+ fn += computeNextValConstant(anim);
+ fn += setNextValAndDetectChange(name, 'nextScalarVal');
+ } else if (anim.type === 'step') {
+ fn += computeNextValStep(anim);
+ fn += setNextValAndDetectChange(name, 'nextScalarVal');
+ } else if (anim.type === 'identity') {
+ fn += computeNextValIdentity(anim);
+ fn += setNextValAndDetectChange(name, 'nextScalarVal');
+ }
+ }
+ if (orderedMatrixOperations.length) {
+ fn += newlines(setNextMatrixAndDetectChange(orderedMatrixOperations));
+ }
+ fn += ' return didChange;\n';
+ fn += '};\n';
+ fn += '})()';
+ return fn;
+};
+
+/**
+ * @param {object} anims Animation configuration by style property name.
+ * @return {function} Function accepting style object, that mutates that style
+ * object and returns a boolean describing if any update was actually applied.
+ */
+var buildStyleInterpolator = function(anims) {
+ return Function(createFunctionString(anims))();
+};
+
+
+module.exports = buildStyleInterpolator;
diff --git a/Libraries/Utilities/cssVar.js b/Libraries/Utilities/cssVar.js
new file mode 100644
index 000000000..0ab64e7f9
--- /dev/null
+++ b/Libraries/Utilities/cssVar.js
@@ -0,0 +1,18 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ *
+ * @providesModule cssVar
+ * @typechecks
+ */
+'use strict';
+
+var invariant = require('invariant');
+var CSSVarConfig = require('CSSVarConfig');
+
+var cssVar = function(/*string*/ key) /*string*/ {
+ invariant(CSSVarConfig[key], 'invalid css variable ' + key);
+
+ return CSSVarConfig[key];
+};
+
+module.exports = cssVar;
diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js
index 96080d09f..2ad33b5d4 100644
--- a/Libraries/react-native/react-native.js
+++ b/Libraries/react-native/react-native.js
@@ -25,6 +25,7 @@ var ReactNative = Object.assign(Object.create(require('React')), {
ListView: require('ListView'),
MapView: require('MapView'),
NavigatorIOS: require('NavigatorIOS'),
+ JSNavigationStack: require('JSNavigationStack'),
PickerIOS: require('PickerIOS'),
ScrollView: require('ScrollView'),
SliderIOS: require('SliderIOS'),
@@ -50,6 +51,7 @@ var ReactNative = Object.assign(Object.create(require('React')), {
NetInfo: require('NetInfo'),
PixelRatio: require('PixelRatio'),
PushNotificationIOS: require('PushNotificationIOS'),
+ PanResponder: require('PanResponder'),
StatusBarIOS: require('StatusBarIOS'),
StyleSheet: require('StyleSheet'),
TimerMixin: require('TimerMixin'),
diff --git a/Libraries/vendor/react/browser/eventPlugins/PanResponder.js b/Libraries/vendor/react/browser/eventPlugins/PanResponder.js
new file mode 100644
index 000000000..22bcaeffe
--- /dev/null
+++ b/Libraries/vendor/react/browser/eventPlugins/PanResponder.js
@@ -0,0 +1,275 @@
+/**
+ * @providesModule PanResponder
+ */
+
+"use strict";
+
+var TouchHistoryMath = require('TouchHistoryMath');
+
+var currentCentroidXOfTouchesChangedAfter =
+ TouchHistoryMath.currentCentroidXOfTouchesChangedAfter;
+var currentCentroidYOfTouchesChangedAfter =
+ TouchHistoryMath.currentCentroidYOfTouchesChangedAfter;
+var previousCentroidXOfTouchesChangedAfter =
+ TouchHistoryMath.previousCentroidXOfTouchesChangedAfter;
+var previousCentroidYOfTouchesChangedAfter =
+ TouchHistoryMath.previousCentroidYOfTouchesChangedAfter;
+var currentCentroidX = TouchHistoryMath.currentCentroidX;
+var currentCentroidY = TouchHistoryMath.currentCentroidY;
+
+/**
+ *
+ * +----------------------------+ +--------------------------------+
+ * | ResponderTouchHistoryStore | |TouchHistoryMath |
+ * +----------------------------+ +----------+---------------------+
+ * |Global store of touchHistory| |Allocation-less math util |
+ * |including activeness, start | |on touch history (centroids |
+ * |position, prev/cur position.| |and multitouch movement etc) |
+ * | | | |
+ * +----^-----------------------+ +----^---------------------------+
+ * | |
+ * | (records relevant history |
+ * | of touches relevant for |
+ * | implementing higher level |
+ * | gestures) |
+ * | |
+ * +----+-----------------------+ +----|---------------------------+
+ * | ResponderEventPlugin | | | Your App/Component |
+ * +----------------------------+ +----|---------------------------+
+ * |Negotiates which view gets | Low level | | High level |
+ * |onResponderMove events. | events w/ | +-+-------+ events w/ |
+ * |Also records history into | touchHistory| | Pan | multitouch + |
+ * |ResponderTouchHistoryStore. +---------------->Responder+-----> accumulative|
+ * +----------------------------+ attached to | | | distance and |
+ * each event | +---------+ velocity. |
+ * | |
+ * | |
+ * +--------------------------------+
+ *
+ *
+ *
+ * Gesture that calculates cumulative movement over time in a way that just
+ * "does the right thing" for multiple touches. The "right thing" is very
+ * nuanced. When moving two touches in opposite directions, the cumulative
+ * distance is zero in each dimension. When two touches move in parallel five
+ * pixels in the same direction, the cumulative distance is five, not ten. If
+ * two touches start, one moves five in a direction, then stops and the other
+ * touch moves fives in the same direction, the cumulative distance is ten.
+ *
+ * This logic requires a kind of processing of time "clusters" of touch events
+ * so that two touch moves that essentially occur in parallel but move every
+ * other frame respectively, are considered part of the same movement.
+ *
+ * Explanation of some of the non-obvious fields:
+ *
+ * - moveX/moveY: If no move event has been observed, then `(moveX, moveY)` is
+ * invalid. If a move event has been observed, `(moveX, moveY)` is the
+ * centroid of the most recently moved "cluster" of active touches.
+ * (Currently all move have the same timeStamp, but later we should add some
+ * threshold for what is considered to be "moving"). If a palm is
+ * accidentally counted as a touch, but a finger is moving greatly, the palm
+ * will move slightly, but we only want to count the single moving touch.
+ * - x0/y0: Centroid location (non-cumulative) at the time of becoming
+ * responder.
+ * - dx/dy: Cumulative touch distance - not the same thing as sum of each touch
+ * distance. Accounts for touch moves that are clustered together in time,
+ * moving the same direction. Only valid when currently responder (otherwise,
+ * it only represents the drag distance below the threshold).
+ * - vx/vy: Velocity.
+ */
+var PanResponder = {
+ _initializeGestureState: function(gestureState) {
+ gestureState.moveX = 0;
+ gestureState.moveY = 0;
+ gestureState.x0 = 0;
+ gestureState.y0 = 0;
+ gestureState.dx = 0;
+ gestureState.dy = 0;
+ gestureState.vx = 0;
+ gestureState.vy = 0;
+ gestureState.numberActiveTouches = 0;
+ // All `gestureState` accounts for timeStamps up until:
+ gestureState._accountsForMovesUpTo = 0;
+ },
+
+ /**
+ * This is nuanced and is necessary. It is incorrect to continuously take all
+ * active *and* recently moved touches, find the centroid, and track how that
+ * result changes over time. Instead, we must take all recently moved
+ * touches, and calculate how the centroid has changed just for those
+ * recently moved touches, and append that change to an accumulator. This is
+ * to (at least) handle the case where the user is moving three fingers, and
+ * then one of the fingers stops but the other two continue.
+ *
+ * This is very different than taking all of the recently moved touches and
+ * storing their centroid as `dx/dy`. For correctness, we must *accumulate
+ * changes* in the centroid of recently moved touches.
+ *
+ * There is also some nuance with how we handle multiple moved touches in a
+ * single event. With the way `ReactIOSEventEmitter` dispatches touches as
+ * individual events, multiple touches generate two 'move' events, each of
+ * them triggering `onResponderMove`. But with the way `PanResponder` works,
+ * all of the gesture inference is performed on the first dispatch, since it
+ * looks at all of the touches (even the ones for which there hasn't been a
+ * native dispatch yet). Therefore, `PanResponder` does not call
+ * `onResponderMove` passed the first dispatch. This diverges from the
+ * typical responder callback pattern (without using `PanResponder`), but
+ * avoids more dispatches than necessary.
+ */
+ _updateGestureStateOnMove: function(gestureState, touchHistory) {
+ gestureState.numberActiveTouches = touchHistory.numberActiveTouches;
+ gestureState.moveX = currentCentroidXOfTouchesChangedAfter(
+ touchHistory,
+ gestureState._accountsForMovesUpTo
+ );
+ gestureState.moveY = currentCentroidYOfTouchesChangedAfter(
+ touchHistory,
+ gestureState._accountsForMovesUpTo
+ );
+ var movedAfter = gestureState._accountsForMovesUpTo;
+ var prevX = previousCentroidXOfTouchesChangedAfter(touchHistory, movedAfter);
+ var x = currentCentroidXOfTouchesChangedAfter(touchHistory, movedAfter);
+ var prevY = previousCentroidYOfTouchesChangedAfter(touchHistory, movedAfter);
+ var y = currentCentroidYOfTouchesChangedAfter(touchHistory, movedAfter);
+ var nextDX = gestureState.dx + (x - prevX);
+ var nextDY = gestureState.dy + (y - prevY);
+
+ // TODO: This must be filtered intelligently.
+ var dt =
+ (touchHistory.mostRecentTimeStamp - gestureState._accountsForMovesUpTo);
+ gestureState.vx = (nextDX - gestureState.dx) / dt;
+ gestureState.vy = (nextDY - gestureState.dy) / dt;
+
+ gestureState.dx = nextDX;
+ gestureState.dy = nextDY;
+ gestureState._accountsForMovesUpTo = touchHistory.mostRecentTimeStamp;
+ },
+
+ /**
+ * @param {object} config Enhanced versions of all of the responder callbacks
+ * that accept not only the typical `ResponderSyntheticEvent`, but also the
+ * `PanResponder` gesture state. Simply replace the word `Responder` with
+ * `PanResponder` in each of the typical `onResponder*` callbacks. For
+ * example, the `config` object would look like:
+ *
+ * - onMoveShouldSetPanResponder: (e, gestureState) => {...}
+ * - onMoveShouldSetPanResponderCapture: (e, gestureState) => {...}
+ * - onStartShouldSetPanResponder: (e, gestureState) => {...}
+ * - onStartShouldSetPanResponderCapture: (e, gestureState) => {...}
+ * - onPanResponderReject: (e, gestureState) => {...}
+ * - onPanResponderGrant: (e, gestureState) => {...}
+ * - onPanResponderStart: (e, gestureState) => {...}
+ * - onPanResponderEnd: (e, gestureState) => {...}
+ * - onPanResponderRelease: (e, gestureState) => {...}
+ * - onPanResponderMove: (e, gestureState) => {...}
+ * - onPanResponderTerminate: (e, gestureState) => {...}
+ * - onPanResponderTerminationRequest: (e, gestureState) => {...}
+ *
+ * - In general, for events that have capture equivalents, we update the
+ * gestureState once in the capture phase and can use it in the bubble phase
+ * as well.
+ *
+ * - Be careful with onStartShould* callbacks. They only reflect updated
+ * `gestureState` for start/end events that bubble/capture to the Node.
+ * Once the node is the responder, you can rely on every start/end event
+ * being processed by the gesture and `gestureState` being updated
+ * accordingly. (numberActiveTouches) may not be totally accurate unless you
+ * are the responder.
+ */
+ create: function(config) {
+ var gestureState = {
+ // Useful for debugging
+ stateID: Math.random(),
+ };
+ PanResponder._initializeGestureState(gestureState);
+ var panHandlers = {
+ onStartShouldSetResponder: function(e) {
+ return config.onStartShouldSetPanResponder === undefined ? false :
+ config.onStartShouldSetPanResponder(e, gestureState);
+ },
+ onMoveShouldSetResponder: function(e) {
+ return config.onMoveShouldSetPanResponder === undefined ? false :
+ config.onMoveShouldSetPanResponder(e, gestureState);
+ },
+ onStartShouldSetResponderCapture: function(e) {
+ // TODO: Actually, we should reinitialize the state any time
+ // touches.length increases from 0 active to > 0 active.
+ if (e.nativeEvent.touches.length === 1) {
+ PanResponder._initializeGestureState(gestureState);
+ }
+ gestureState.numberActiveTouches = e.touchHistory.numberActiveTouches;
+ return config.onStartShouldSetPanResponderCapture !== undefined ?
+ config.onStartShouldSetPanResponderCapture(e, gestureState) : false;
+ },
+
+ onMoveShouldSetResponderCapture: function(e) {
+ var touchHistory = e.touchHistory;
+ // Responder system incorrectly dispatches should* to current responder
+ // Filter out any touch moves past the first one - we would have
+ // already processed multi-touch geometry during the first event.
+ if (gestureState._accountsForMovesUpTo === touchHistory.mostRecentTimeStamp) {
+ return false;
+ }
+ PanResponder._updateGestureStateOnMove(gestureState, touchHistory);
+ return config.onMoveShouldSetResponderCapture ?
+ config.onMoveShouldSetPanResponderCapture(e, gestureState) : false;
+ },
+
+ onResponderGrant: function(e) {
+ gestureState.x0 = currentCentroidX(e.touchHistory);
+ gestureState.y0 = currentCentroidY(e.touchHistory);
+ gestureState.dx = 0;
+ gestureState.dy = 0;
+ config.onPanResponderGrant && config.onPanResponderGrant(e, gestureState);
+ },
+
+ onResponderReject: function(e) {
+ config.onPanResponderReject && config.onPanResponderReject(e, gestureState);
+ },
+
+ onResponderRelease: function(e) {
+ config.onPanResponderRelease && config.onPanResponderRelease(e, gestureState);
+ PanResponder._initializeGestureState(gestureState);
+ },
+
+ onResponderStart: function(e) {
+ var touchHistory = e.touchHistory;
+ gestureState.numberActiveTouches = touchHistory.numberActiveTouches;
+ config.onPanResponderStart && config.onPanResponderStart(e, gestureState);
+ },
+
+ onResponderMove: function(e) {
+ var touchHistory = e.touchHistory;
+ // Guard against the dispatch of two touch moves when there are two
+ // simultaneously changed touches.
+ if (gestureState._accountsForMovesUpTo === touchHistory.mostRecentTimeStamp) {
+ return;
+ }
+ // Filter out any touch moves past the first one - we would have
+ // already processed multi-touch geometry during the first event.
+ PanResponder._updateGestureStateOnMove(gestureState, touchHistory);
+ config.onPanResponderMove && config.onPanResponderMove(e, gestureState);
+ },
+
+ onResponderEnd: function(e) {
+ var touchHistory = e.touchHistory;
+ gestureState.numberActiveTouches = touchHistory.numberActiveTouches;
+ config.onPanResponderEnd && config.onPanResponderEnd(e, gestureState);
+ },
+
+ onResponderTerminate: function(e) {
+ config.onPanResponderTerminate &&
+ config.onPanResponderTerminate(e, gestureState);
+ PanResponder._initializeGestureState(gestureState);
+ },
+
+ onResponderTerminationRequest: function(e) {
+ return config.onPanResponderTerminationRequest === undefined ? true :
+ config.onPanResponderTerminationRequest(e, gestureState);
+ },
+ };
+ return {panHandlers: panHandlers};
+ },
+};
+
+module.exports = PanResponder;
diff --git a/Libraries/vendor/react/browser/eventPlugins/ResponderTouchHistoryStore.js b/Libraries/vendor/react/browser/eventPlugins/ResponderTouchHistoryStore.js
index 2576528c1..805b9471c 100644
--- a/Libraries/vendor/react/browser/eventPlugins/ResponderTouchHistoryStore.js
+++ b/Libraries/vendor/react/browser/eventPlugins/ResponderTouchHistoryStore.js
@@ -39,6 +39,13 @@ var touchHistory = {
mostRecentTimeStamp: 0,
};
+var timestampForTouch = function(touch) {
+ // The legacy internal implementation provides "timeStamp", which has been
+ // renamed to "timestamp". Let both work for now while we iron it out
+ // TODO (evv): rename timeStamp to timestamp in internal code
+ return touch.timeStamp || touch.timestamp;
+};
+
/**
* TODO: Instead of making gestures recompute filtered velocity, we could
* include a built in velocity computation that can be reused globally.
@@ -47,29 +54,29 @@ var touchHistory = {
var initializeTouchData = function(touch) {
return {
touchActive: true,
- startTimeStamp: touch.timeStamp,
+ startTimeStamp: timestampForTouch(touch),
startPageX: touch.pageX,
startPageY: touch.pageY,
currentPageX: touch.pageX,
currentPageY: touch.pageY,
- currentTimeStamp: touch.timeStamp,
+ currentTimeStamp: timestampForTouch(touch),
previousPageX: touch.pageX,
previousPageY: touch.pageY,
- previousTimeStamp: touch.timeStamp,
+ previousTimeStamp: timestampForTouch(touch),
};
};
var reinitializeTouchTrack = function(touchTrack, touch) {
touchTrack.touchActive = true;
- touchTrack.startTimeStamp = touch.timeStamp;
+ touchTrack.startTimeStamp = timestampForTouch(touch);
touchTrack.startPageX = touch.pageX;
touchTrack.startPageY = touch.pageY;
touchTrack.currentPageX = touch.pageX;
touchTrack.currentPageY = touch.pageY;
- touchTrack.currentTimeStamp = touch.timeStamp;
+ touchTrack.currentTimeStamp = timestampForTouch(touch);
touchTrack.previousPageX = touch.pageX;
touchTrack.previousPageY = touch.pageY;
- touchTrack.previousTimeStamp = touch.timeStamp;
+ touchTrack.previousTimeStamp = timestampForTouch(touch);
};
var validateTouch = function(touch) {
@@ -96,7 +103,7 @@ var recordStartTouchData = function(touch) {
} else {
reinitializeTouchTrack(touchTrack, touch);
}
- touchHistory.mostRecentTimeStamp = touch.timeStamp;
+ touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
};
var recordMoveTouchData = function(touch) {
@@ -112,8 +119,8 @@ var recordMoveTouchData = function(touch) {
touchTrack.previousTimeStamp = touchTrack.currentTimeStamp;
touchTrack.currentPageX = touch.pageX;
touchTrack.currentPageY = touch.pageY;
- touchTrack.currentTimeStamp = touch.timeStamp;
- touchHistory.mostRecentTimeStamp = touch.timeStamp;
+ touchTrack.currentTimeStamp = timestampForTouch(touch);
+ touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
};
var recordEndTouchData = function(touch) {
@@ -128,9 +135,9 @@ var recordEndTouchData = function(touch) {
touchTrack.previousTimeStamp = touchTrack.currentTimeStamp;
touchTrack.currentPageX = touch.pageX;
touchTrack.currentPageY = touch.pageY;
- touchTrack.currentTimeStamp = touch.timeStamp;
+ touchTrack.currentTimeStamp = timestampForTouch(touch);
touchTrack.touchActive = false;
- touchHistory.mostRecentTimeStamp = touch.timeStamp;
+ touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
};
var ResponderTouchHistoryStore = {
diff --git a/Libraries/vendor/react/browser/eventPlugins/TouchHistoryMath.js b/Libraries/vendor/react/browser/eventPlugins/TouchHistoryMath.js
new file mode 100644
index 000000000..1507d21f5
--- /dev/null
+++ b/Libraries/vendor/react/browser/eventPlugins/TouchHistoryMath.js
@@ -0,0 +1,122 @@
+/**
+ * @providesModule TouchHistoryMath
+ */
+
+"use strict";
+
+var TouchHistoryMath = {
+ /**
+ * This code is optimized and not intended to look beautiful. This allows
+ * computing of touch centroids that have moved after `touchesChangedAfter`
+ * timeStamp. You can compute the current centroid involving all touches
+ * moves after `touchesChangedAfter`, or you can compute the previous
+ * centroid of all touches that were moved after `touchesChangedAfter`.
+ *
+ * @param {TouchHistoryMath} touchHistory Standard Responder touch track
+ * data.
+ * @param {number} touchesChangedAfter timeStamp after which moved touches
+ * are considered "actively moving" - not just "active".
+ * @param {boolean} isXAxis Consider `x` dimension vs. `y` dimension.
+ * @param {boolean} ofCurrent Compute current centroid for actively moving
+ * touches vs. previous centroid of now actively moving touches.
+ * @return {number} value of centroid in specified dimension.
+ */
+ centroidDimension: function(touchHistory, touchesChangedAfter, isXAxis, ofCurrent) {
+ var touchBank = touchHistory.touchBank;
+ var total = 0;
+ var count = 0;
+
+ var oneTouchData = touchHistory.numberActiveTouches === 1 ?
+ touchHistory.touchBank[touchHistory.indexOfSingleActiveTouch] : null;
+
+ if (oneTouchData !== null) {
+ if (oneTouchData.touchActive && oneTouchData.currentTimeStamp > touchesChangedAfter) {
+ total += ofCurrent && isXAxis ? oneTouchData.currentPageX :
+ ofCurrent && !isXAxis ? oneTouchData.currentPageY :
+ !ofCurrent && isXAxis ? oneTouchData.previousPageX :
+ oneTouchData.previousPageY;
+ count = 1;
+ }
+ } else {
+ for (var i = 0; i < touchBank.length; i++) {
+ var touchTrack = touchBank[i];
+ if (touchTrack !== null &&
+ touchTrack !== undefined &&
+ touchTrack.touchActive &&
+ touchTrack.currentTimeStamp >= touchesChangedAfter) {
+ var toAdd; // Yuck, program temporarily in invalid state.
+ if (ofCurrent && isXAxis) {
+ toAdd = touchTrack.currentPageX;
+ } else if (ofCurrent && !isXAxis) {
+ toAdd = touchTrack.currentPageY;
+ } else if (!ofCurrent && isXAxis) {
+ toAdd = touchTrack.previousPageX;
+ } else {
+ toAdd = touchTrack.previousPageY;
+ }
+ total += toAdd;
+ count++;
+ }
+ }
+ }
+ return count > 0 ? total / count : TouchHistoryMath.noCentroid;
+ },
+
+ currentCentroidXOfTouchesChangedAfter: function(touchHistory, touchesChangedAfter) {
+ return TouchHistoryMath.centroidDimension(
+ touchHistory,
+ touchesChangedAfter,
+ true, // isXAxis
+ true // ofCurrent
+ );
+ },
+
+ currentCentroidYOfTouchesChangedAfter: function(touchHistory, touchesChangedAfter) {
+ return TouchHistoryMath.centroidDimension(
+ touchHistory,
+ touchesChangedAfter,
+ false, // isXAxis
+ true // ofCurrent
+ );
+ },
+
+ previousCentroidXOfTouchesChangedAfter: function(touchHistory, touchesChangedAfter) {
+ return TouchHistoryMath.centroidDimension(
+ touchHistory,
+ touchesChangedAfter,
+ true, // isXAxis
+ false // ofCurrent
+ );
+ },
+
+ previousCentroidYOfTouchesChangedAfter: function(touchHistory, touchesChangedAfter) {
+ return TouchHistoryMath.centroidDimension(
+ touchHistory,
+ touchesChangedAfter,
+ false, // isXAxis
+ false // ofCurrent
+ );
+ },
+
+ currentCentroidX: function(touchHistory) {
+ return TouchHistoryMath.centroidDimension(
+ touchHistory,
+ 0, // touchesChangedAfter
+ true, // isXAxis
+ true // ofCurrent
+ );
+ },
+
+ currentCentroidY: function(touchHistory) {
+ return TouchHistoryMath.centroidDimension(
+ touchHistory,
+ 0, // touchesChangedAfter
+ false, // isXAxis
+ true // ofCurrent
+ );
+ },
+
+ noCentroid: -1,
+};
+
+module.exports = TouchHistoryMath;
diff --git a/Libraries/vendor/react/core/clamp.js b/Libraries/vendor/react/core/clamp.js
new file mode 100644
index 000000000..cb7578cee
--- /dev/null
+++ b/Libraries/vendor/react/core/clamp.js
@@ -0,0 +1,35 @@
+/**
+ * @generated SignedSource<>
+ *
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ * !! This file is a check-in of a static_upstream project! !!
+ * !! !!
+ * !! You should not modify this file directly. Instead: !!
+ * !! 1) Use `fjs use-upstream` to temporarily replace this with !!
+ * !! the latest version from upstream. !!
+ * !! 2) Make your changes, test them, etc. !!
+ * !! 3) Use `fjs push-upstream` to copy your changes back to !!
+ * !! static_upstream. !!
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ *
+ * @providesModule clamp
+ * @typechecks
+ */
+
+ /**
+ * @param {number} value
+ * @param {number} min
+ * @param {number} max
+ * @return {number}
+ */
+function clamp(min, value, max) {
+ if (value < min) {
+ return min;
+ }
+ if (value > max) {
+ return max;
+ }
+ return value;
+}
+
+module.exports = clamp;