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;