NavigationExperimental

Summary:
A new API to unify internal navigation. Also addresses a highly-rated community 'pain': https://productpains.com/post/react-native/better-navigator-api-and-docs/

Offers the following improvements:

- Redux-style navigation logic is easy to reason about
- Navigation state can be easily saved and restored through refreshes
- Declarative navigation views can be implemented in native or JS
- Animations and gestures are isolated and now use the Animated library

public

Reviewed By: hedgerwang

Differential Revision: D2798048

fb-gh-sync-id: 88027ef9ead8a80afa38354252bc377455cc6dbb
This commit is contained in:
Eric Vicenti
2016-02-05 14:25:17 -08:00
committed by facebook-github-bot-9
parent 2f73ad0241
commit a3085464f6
26 changed files with 2504 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
/**
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
* Facebook reserves all rights not expressly granted.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
'use strict';
var React = require('react-native');
var {
NavigationExperimental,
StyleSheet,
ScrollView,
} = React;
var NavigationExampleRow = require('./NavigationExampleRow');
var {
AnimatedView: NavigationAnimatedView,
Card: NavigationCard,
RootContainer: NavigationRootContainer,
Reducer: NavigationReducer,
Header: NavigationHeader,
} = NavigationExperimental;
const NavigationBasicReducer = NavigationReducer.StackReducer({
initialStates: [
{key: 'First Route'}
],
matchAction: action => true,
actionStateMap: actionString => ({key: actionString}),
});
class NavigationAnimatedExample extends React.Component {
componentWillMount() {
this._renderNavigated = this._renderNavigated.bind(this);
}
render() {
return (
<NavigationRootContainer
reducer={NavigationBasicReducer}
persistenceKey="NavigationAnimatedExampleState"
renderNavigation={this._renderNavigated}
/>
);
}
_renderNavigated(navigationState, onNavigate) {
if (!navigationState) {
return null;
}
return (
<NavigationAnimatedView
navigationState={navigationState}
style={styles.animatedView}
renderOverlay={(position, layout) => (
<NavigationHeader
navigationState={navigationState}
position={position}
getTitle={state => state.key}
/>
)}
renderScene={(state, index, position, layout) => (
<NavigationCard
key={state.key}
index={index}
navigationState={navigationState}
position={position}
layout={layout}>
<ScrollView style={styles.scrollView}>
<NavigationExampleRow
text={navigationState.children[navigationState.index].key}
/>
<NavigationExampleRow
text="Push!"
onPress={() => {
onNavigate('Route #' + navigationState.children.length);
}}
/>
<NavigationExampleRow
text="Exit Animated Nav Example"
onPress={this.props.onExampleExit}
/>
</ScrollView>
</NavigationCard>
)}
/>
);
}
}
const styles = StyleSheet.create({
animatedView: {
flex: 1,
},
scrollView: {
marginTop: 64
},
});
module.exports = NavigationAnimatedExample;

View File

@@ -0,0 +1,82 @@
/**
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
* Facebook reserves all rights not expressly granted.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
'use strict';
const React = require('react-native');
const {
NavigationExperimental,
ScrollView,
StyleSheet,
} = React;
const NavigationExampleRow = require('./NavigationExampleRow');
const {
RootContainer: NavigationRootContainer,
Reducer: NavigationReducer,
} = NavigationExperimental;
const StackReducer = NavigationReducer.StackReducer;
const NavigationBasicReducer = StackReducer({
initialStates: [
{key: 'first_page'}
],
matchAction: action => true,
actionStateMap: action => ({key: action}),
});
const NavigationBasicExample = React.createClass({
render: function() {
return (
<NavigationRootContainer
reducer={NavigationBasicReducer}
persistenceKey="NavigationBasicExampleState"
renderNavigation={(navState, onNavigate) => {
if (!navState) { return null; }
return (
<ScrollView style={styles.topView}>
<NavigationExampleRow
text={`Current page: ${navState.children[navState.index].key}`}
/>
<NavigationExampleRow
text={`Push page #${navState.children.length}`}
onPress={() => {
onNavigate('page #' + navState.children.length);
}}
/>
<NavigationExampleRow
text="pop"
onPress={() => {
onNavigate(StackReducer.PopAction());
}}
/>
<NavigationExampleRow
text="Exit Basic Nav Example"
onPress={this.props.onExampleExit}
/>
</ScrollView>
);
}}
/>
);
},
});
const styles = StyleSheet.create({
topView: {
backgroundColor: '#E9E9EF',
flex: 1,
paddingTop: 30,
},
});
module.exports = NavigationBasicExample;

View File

@@ -0,0 +1,264 @@
/**
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
* Facebook reserves all rights not expressly granted.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
'use strict';
const React = require('react-native');
const {
NavigationExperimental,
ScrollView,
StyleSheet,
View,
} = React;
const {
AnimatedView: NavigationAnimatedView,
Card: NavigationCard,
Container: NavigationContainer,
RootContainer: NavigationRootContainer,
Header: NavigationHeader,
Reducer: NavigationReducer,
View: NavigationView,
} = NavigationExperimental;
const NavigationExampleRow = require('./NavigationExampleRow');
const NavigationExampleTabBar = require('./NavigationExampleTabBar');
const ExampleExitAction = () => ({
isExitAction: true,
});
ExampleExitAction.match = (action) => (
action && action.isExitAction === true
);
const ExamplePageAction = (type) => ({
type,
isPageAction: true,
});
ExamplePageAction.match = (action) => (
action && action.isPageAction === true
);
const ExampleSettingsPageAction = (type) => ({
...ExamplePageAction(type),
isSettingsPageAction: true,
});
ExampleSettingsPageAction.match = (action) => (
action && action.isSettingsPageAction === true
);
const ExampleInfoAction = () => ExamplePageAction('InfoPage');
const ExampleNotifSettingsAction = () => ExampleSettingsPageAction('NotifSettingsPage');
const _jsInstanceUniqueId = '' + Date.now();
let _uniqueIdCount = 0;
function pageStateActionMap(action) {
return {
key: 'page-' + _jsInstanceUniqueId + '-' + (_uniqueIdCount++),
type: action.type,
};
}
function getTabActionMatcher(key) {
return function (action) {
if (!ExamplePageAction.match(action)) {
return false;
}
if (ExampleSettingsPageAction.match(action)) {
return key === 'settings';
}
return true;
};
}
var ExampleTabs = [
{
label: 'Account',
reducer: NavigationReducer.StackReducer({
initialStates: [
{type: 'AccountPage', key: 'base'}
],
key: 'account',
matchAction: getTabActionMatcher('account'),
actionStateMap: pageStateActionMap,
}),
},
{
label: 'Notifications',
reducer: NavigationReducer.StackReducer({
initialStates: [
{type: 'NotifsPage', key: 'base'}
],
key: 'notifs',
matchAction: getTabActionMatcher('notifs'),
actionStateMap: pageStateActionMap,
}),
},
{
label: 'Settings',
reducer: NavigationReducer.StackReducer({
initialStates: [
{type: 'SettingsPage', key: 'base'}
],
key: 'settings',
matchAction: getTabActionMatcher('settings'),
actionStateMap: pageStateActionMap,
}),
},
];
const ExampleAppReducer = NavigationReducer.TabsReducer({
tabReducers: ExampleTabs.map(tab => tab.reducer),
});
function stateTypeTitleMap(pageState) {
switch (pageState.type) {
case 'AccountPage':
return 'Account Page';
case 'NotifsPage':
return 'Notifications';
case 'SettingsPage':
return 'Settings';
case 'InfoPage':
return 'Info Page';
case 'NotifSettingsPage':
return 'Notification Settings';
}
}
let ExampleTabScreen = React.createClass({
render: function() {
return (
<NavigationAnimatedView
style={styles.tabContent}
navigationState={this.props.navigationState}
renderOverlay={this._renderHeader}
renderScene={this._renderScene}
/>
);
},
_renderHeader: function(position, layout) {
return (
<NavigationHeader
navigationState={this.props.navigationState}
position={position}
layout={layout}
getTitle={state => stateTypeTitleMap(state)}
/>
);
},
_renderScene: function(child, index, position, layout) {
return (
<NavigationCard
key={child.key}
index={index}
childState={child}
navigationState={this.props.navigationState}
position={position}
layout={layout}>
<ScrollView style={styles.scrollView}>
<NavigationExampleRow
text="Open page"
onPress={() => {
this.props.onNavigate(ExampleInfoAction());
}}
/>
<NavigationExampleRow
text="Open notifs settings in settings tab"
onPress={() => {
this.props.onNavigate(ExampleNotifSettingsAction());
}}
/>
<NavigationExampleRow
text="Exit Composition Example"
onPress={() => {
this.props.onNavigate(ExampleExitAction());
}}
/>
</ScrollView>
</NavigationCard>
);
},
});
ExampleTabScreen = NavigationContainer.create(ExampleTabScreen);
const NavigationCompositionExample = React.createClass({
render: function() {
return (
<NavigationRootContainer
reducer={ExampleAppReducer}
persistenceKey="NavigationCompositionExampleState"
renderNavigation={this.renderApp}
/>
);
},
renderApp: function(navigationState, onNavigate) {
if (!navigationState) {
return null;
}
return (
<View style={styles.topView}>
<ExampleMainView
navigationState={navigationState}
onExampleExit={this.props.onExampleExit}
/>
<NavigationExampleTabBar
tabs={navigationState.children}
index={navigationState.index}
/>
</View>
);
},
});
let ExampleMainView = React.createClass({
render: function() {
return (
<NavigationView
navigationState={this.props.navigationState}
style={styles.tabsContent}
renderScene={(tabState, index) => (
<ExampleTabScreen
key={tabState.key}
navigationState={tabState}
onNavigate={this._handleNavigation.bind(null, tabState.key)}
/>
)}
/>
);
},
_handleNavigation: function(tabKey, action) {
if (ExampleExitAction.match(action)) {
this.props.onExampleExit();
return;
}
this.props.onNavigate(action);
},
});
ExampleMainView = NavigationContainer.create(ExampleMainView);
const styles = StyleSheet.create({
topView: {
flex: 1,
},
tabsContent: {
flex: 1,
},
scrollView: {
marginTop: 64
},
tabContent: {
flex: 1,
},
});
module.exports = NavigationCompositionExample;

View File

@@ -0,0 +1,65 @@
/**
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
* Facebook reserves all rights not expressly granted.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
'use strict';
var React = require('react-native');
var {
Text,
PixelRatio,
StyleSheet,
View,
TouchableHighlight,
} = React;
var NavigationExampleRow = React.createClass({
render: function() {
if (this.props.onPress) {
return (
<TouchableHighlight
style={styles.row}
underlayColor="#D0D0D0"
onPress={this.props.onPress}>
<Text style={styles.buttonText}>
{this.props.text}
</Text>
</TouchableHighlight>
);
}
return (
<View style={styles.row}>
<Text style={styles.rowText}>
{this.props.text}
</Text>
</View>
);
},
});
const styles = StyleSheet.create({
row: {
padding: 15,
backgroundColor: 'white',
borderBottomWidth: 1 / PixelRatio.get(),
borderBottomColor: '#CDCDCD',
},
rowText: {
fontSize: 17,
},
buttonText: {
fontSize: 17,
fontWeight: '500',
},
});
module.exports = NavigationExampleRow;

View File

@@ -0,0 +1,81 @@
/**
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
* Facebook reserves all rights not expressly granted.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
'use strict';
var React = require('react-native');
var {
NavigationExperimental,
StyleSheet,
Text,
TouchableOpacity,
View,
} = React;
const {
Container: NavigationContainer,
Reducer: NavigationReducer,
} = NavigationExperimental;
const {
JumpToAction,
} = NavigationReducer.TabsReducer;
var NavigationExampleTabBar = React.createClass({
render: function() {
return (
<View style={styles.tabBar}>
{this.props.tabs.map(this._renderTab)}
</View>
);
},
_renderTab: function(tab, index) {
var textStyle = [styles.tabButtonText];
if (this.props.index === index) {
textStyle.push(styles.selectedTabButtonText);
}
return (
<TouchableOpacity
style={styles.tabButton}
key={tab.key}
onPress={() => {
this.props.onNavigate(JumpToAction(index));
}}>
<Text style={textStyle}>
{tab.key}
</Text>
</TouchableOpacity>
);
},
});
NavigationExampleTabBar = NavigationContainer.create(NavigationExampleTabBar);
const styles = StyleSheet.create({
tabBar: {
height: 50,
flexDirection: 'row',
},
tabButton: {
flex: 1,
},
tabButtonText: {
paddingTop: 20,
textAlign: 'center',
fontSize: 17,
fontWeight: '500',
},
selectedTabButtonText: {
color: 'blue',
},
});
module.exports = NavigationExampleTabBar;

View File

@@ -0,0 +1,129 @@
/**
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
* Facebook reserves all rights not expressly granted.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
'use strict';
var React = require('react-native');
var {
AsyncStorage,
ScrollView,
StyleSheet,
View,
} = React;
var NavigationExampleRow = require('./NavigationExampleRow');
/*
* Heads up! This file is not the real navigation example- only a utility to switch between them.
*
* To learn how to use the Navigation API, take a look at the following exmample files:
*/
var EXAMPLES = {
'Tabs': require('./NavigationTabsExample'),
'Basic': require('./NavigationBasicExample'),
'Animated Card Stack': require('./NavigationAnimatedExample'),
'Composition': require('./NavigationCompositionExample'),
};
var EXAMPLE_STORAGE_KEY = 'NavigationExampleExample';
var NavigationExperimentalExample = React.createClass({
statics: {
title: 'Navigation (Experimental)',
description: 'Upcoming navigation APIs and animated navigation views',
external: true,
},
getInitialState: function() {
return {
exampe: null,
};
},
componentDidMount() {
AsyncStorage.getItem(EXAMPLE_STORAGE_KEY, (err, example) => {
if (err || !example) {
this.setState({
example: 'menu',
});
return;
}
this.setState({
example,
});
});
},
setExample: function(example) {
this.setState({
example,
});
AsyncStorage.setItem(EXAMPLE_STORAGE_KEY, example);
},
_renderMenu: function() {
var exitRow = null;
if (this.props.onExampleExit) {
exitRow = (
<NavigationExampleRow
text="Exit Navigation Examples"
onPress={this.props.onExampleExit}
/>
);
}
return (
<View style={styles.menu}>
<ScrollView>
{this._renderExampleList()}
{exitRow}
</ScrollView>
</View>
);
},
_renderExampleList: function() {
return Object.keys(EXAMPLES).map(exampleName => (
<NavigationExampleRow
key={exampleName}
text={exampleName}
onPress={() => {
this.setExample(exampleName);
}}
/>
));
},
_exitInnerExample: function() {
this.setExample('menu');
},
render: function() {
if (this.state.example === 'menu') {
return this._renderMenu();
}
if (EXAMPLES[this.state.example]) {
var Component = EXAMPLES[this.state.example];
return <Component onExampleExit={this._exitInnerExample} />;
}
return null;
},
});
const styles = StyleSheet.create({
menu: {
backgroundColor: '#E9E9EF',
flex: 1,
marginTop: 20,
},
});
module.exports = NavigationExperimentalExample;

View File

@@ -0,0 +1,104 @@
/**
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
* Facebook reserves all rights not expressly granted.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
'use strict';
const React = require('react-native');
const {
NavigationExperimental,
ScrollView,
StyleSheet,
View,
} = React;
const {
Container: NavigationContainer,
RootContainer: NavigationRootContainer,
Reducer: NavigationReducer,
} = NavigationExperimental;
const NavigationExampleRow = require('./NavigationExampleRow');
const NavigationExampleTabBar = require('./NavigationExampleTabBar');
class ExmpleTabPage extends React.Component {
render() {
const currentTabRoute = this.props.tabs[this.props.index];
return (
<ScrollView style={styles.tabPage}>
<NavigationExampleRow
text={`Current Tab is: ${currentTabRoute.key}`}
/>
{this.props.tabs.map((tab, index) => (
<NavigationExampleRow
key={tab.key}
text={`Go to ${tab.key}`}
onPress={() => {
this.props.onNavigate(NavigationReducer.TabsReducer.JumpToAction(index));
}}
/>
))}
<NavigationExampleRow
text="Exit Tabs Example"
onPress={this.props.onExampleExit}
/>
</ScrollView>
);
}
}
ExmpleTabPage = NavigationContainer.create(ExmpleTabPage);
const ExampleTabsReducer = NavigationReducer.TabsReducer({
tabReducers: [
(lastRoute) => lastRoute || {key: 'one'},
(lastRoute) => lastRoute || {key: 'two'},
(lastRoute) => lastRoute || {key: 'three'},
],
});
const NavigationTabsExample = React.createClass({
render: function() {
return (
<NavigationRootContainer
reducer={ExampleTabsReducer}
persistenceKey="NAV_EXAMPLE_STATE_TABS"
renderNavigation={(navigationState) => {
if (!navigationState) { return null; }
return (
<View style={styles.topView}>
<ExmpleTabPage
tabs={navigationState.children}
index={navigationState.index}
onExampleExit={this.props.onExampleExit}
/>
<NavigationExampleTabBar
tabs={navigationState.children}
index={navigationState.index}
/>
</View>
);
}}
/>
);
},
});
const styles = StyleSheet.create({
topView: {
flex: 1,
paddingTop: 30,
},
tabPage: {
backgroundColor: '#E9E9EF',
},
});
module.exports = NavigationTabsExample;

View File

@@ -74,6 +74,7 @@ var APIS = [
require('./GeolocationExample'),
require('./ImageEditingExample'),
require('./LayoutExample'),
require('./NavigationExperimental/NavigationExperimentalExample'),
require('./NetInfoExample'),
require('./PanResponderExample'),
require('./PointerEventsExample'),

View File

@@ -0,0 +1,160 @@
/**
* Copyright (c) 2015, Facebook, Inc. All rights reserved.
*
* Facebook, Inc. ("Facebook") owns all right, title and interest, including
* all intellectual property and other proprietary rights, in and to the React
* Native CustomComponents software (the "Software"). Subject to your
* compliance with these terms, you are hereby granted a non-exclusive,
* worldwide, royalty-free copyright license to (1) use and copy the Software;
* and (2) reproduce and distribute the Software as part of your own software
* ("Your Software"). Facebook reserves all rights not expressly granted to
* you in this license agreement.
*
* THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS
* OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED.
* IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR
* EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @providesModule NavigationCard
* @flow
*/
'use strict';
const Animated = require('Animated');
const NavigationReducer = require('NavigationReducer');
const NavigationContainer = require('NavigationContainer');
const PanResponder = require('PanResponder');
const React = require('React');
const StyleSheet = require('StyleSheet');
const View = require('View');
import type {
NavigationParentState
} from 'NavigationState';
type Layout = {
initWidth: number,
initHeight: number,
width: Animated.Value;
height: Animated.Value;
};
type Props = {
navigationState: NavigationParentState;
index: number;
position: Animated.Value;
layout: Layout;
onNavigate: Function;
children: Object;
};
class NavigationCard extends React.Component {
_responder: ?Object;
_lastHeight: number;
_lastWidth: number;
_widthListener: string;
_heightListener: string;
props: Props;
componentWillMount(props) {
this._responder = PanResponder.create({
onMoveShouldSetPanResponder: (e, {dx, dy, moveX, moveY, x0, y0}) => {
if (this.props.navigationState.index === 0) {
return false;
}
if (moveX > 30) {
return false;
}
if (dx > 5 && Math.abs(dy) < 4) {
return true;
}
return false;
},
onPanResponderGrant: (e, {dx, dy, moveX, moveY, x0, y0}) => {
},
onPanResponderMove: (e, {dx}) => {
const a = (-dx / this._lastWidth) + this.props.navigationState.index;
this.props.position.setValue(a);
},
onPanResponderRelease: (e, {vx, dx}) => {
const xRatio = dx / this._lastWidth;
const doesPop = (xRatio + vx) > 0.45;
if (doesPop) {
// todo: add an action which accepts velocity of the pop action/gesture, which is caught and used by NavigationAnimatedView
this.props.onNavigate(NavigationReducer.StackReducer.PopAction());
return;
}
Animated.spring(this.props.position, {
toValue: this.props.navigationState.index,
}).start();
},
onPanResponderTerminate: (e, {vx, dx}) => {
Animated.spring(this.props.position, {
toValue: this.props.navigationState.index,
}).start();
},
});
}
componentDidMount() {
this._lastHeight = this.props.layout.initHeight;
this._lastWidth = this.props.layout.initWidth;
this._widthListener = this.props.layout.width.addListener(({value}) => {
this._lastWidth = value;
});
this._heightListener = this.props.layout.height.addListener(({value}) => {
this._lastHeight = value;
});
// todo: fix listener and last layout dimentsions when props change. potential bugs here
}
componentWillUnmount() {
this.props.layout.width.removeListener(this._widthListener);
this.props.layout.height.removeListener(this._heightListener);
}
render() {
const cardPosition = Animated.add(this.props.position, new Animated.Value(-this.props.index));
const gestureValue = Animated.multiply(cardPosition, this.props.layout.width);
return (
<Animated.View
{...this._responder.panHandlers}
style={[
styles.card,
{
right: gestureValue,
left: gestureValue.interpolate({
inputRange: [0, 1],
outputRange: [0, -1],
}),
opacity: cardPosition.interpolate({
inputRange: [-1,0,1],
outputRange: [0,1,1],
}),
}
]}>
{this.props.children}
</Animated.View>
);
}
}
NavigationCard = NavigationContainer.create(NavigationCard);
const styles = StyleSheet.create({
card: {
backgroundColor: '#E9E9EF',
shadowColor: 'black',
shadowOpacity: 0.4,
shadowOffset: {width: 0, height: 0},
shadowRadius: 10,
top: 0,
bottom: 0,
position: 'absolute',
},
});
module.exports = NavigationCard;

View File

@@ -0,0 +1,149 @@
/**
* Copyright (c) 2015, Facebook, Inc. All rights reserved.
*
* Facebook, Inc. ("Facebook") owns all right, title and interest, including
* all intellectual property and other proprietary rights, in and to the React
* Native CustomComponents software (the "Software"). Subject to your
* compliance with these terms, you are hereby granted a non-exclusive,
* worldwide, royalty-free copyright license to (1) use and copy the Software;
* and (2) reproduce and distribute the Software as part of your own software
* ("Your Software"). Facebook reserves all rights not expressly granted to
* you in this license agreement.
*
* THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS
* OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED.
* IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR
* EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @providesModule NavigationHeader
* @flow
*/
'use strict';
const Animated = require('Animated');
const Image = require('Image');
const NavigationContainer = require('NavigationContainer');
const NavigationReducer = require('NavigationReducer');
const React = require('react-native');
const StyleSheet = require('StyleSheet');
const Text = require('Text');
const TouchableOpacity = require('TouchableOpacity');
const View = require('View');
import type {
NavigationState,
NavigationParentState
} from 'NavigationState';
type Props = {
navigationState: NavigationParentState,
onNavigate: Function,
position: Animated.Value,
getTitle: (navState: NavigationState) => string,
};
class NavigationHeader extends React.Component {
_handleBackPress: Function;
props: Props;
componentWillMount() {
this._handleBackPress = this._handleBackPress.bind(this);
}
render() {
var state = this.props.navigationState;
return (
<Animated.View
style={[
styles.header,
]}>
{state.children.map(this._renderTitle, this)}
{this._renderBackButton()}
</Animated.View>
);
}
_renderBackButton() {
if (this.props.navigationState.index === 0) {
return null;
}
return (
<TouchableOpacity style={styles.backButton} onPress={this._handleBackPress}>
<Image source={require('./back_chevron.png')} style={styles.backButtonImage} />
</TouchableOpacity>
);
}
_renderTitle(childState, index) {
return (
<Animated.Text
key={childState.key}
style={[
styles.title,
{
opacity: this.props.position.interpolate({
inputRange: [index - 1, index, index + 1],
outputRange: [0, 1, 0],
}),
left: this.props.position.interpolate({
inputRange: [index - 1, index + 1],
outputRange: [200, -200],
}),
right: this.props.position.interpolate({
inputRange: [index - 1, index + 1],
outputRange: [-200, 200],
}),
},
]}>
{this.props.getTitle(childState)}
</Animated.Text>
);
}
_handleBackPress() {
this.props.onNavigate(NavigationReducer.StackReducer.PopAction());
}
}
NavigationHeader = NavigationContainer.create(NavigationHeader);
const styles = StyleSheet.create({
title: {
textAlign: 'center',
marginTop: 10,
fontSize: 18,
fontWeight: '500',
color: '#0A0A0A',
position: 'absolute',
top: 20,
left: 0,
right: 0,
},
header: {
backgroundColor: '#EFEFF2',
paddingTop: 20,
top: 0,
height: 64,
right: 0,
left: 0,
borderBottomWidth: 0.5,
borderBottomColor: '#828287',
position: 'absolute',
},
backButton: {
width: 29,
height: 37,
position: 'absolute',
bottom: 4,
left: 2,
padding: 8,
},
backButtonImage: {
width: 13,
height: 21,
},
});
module.exports = NavigationHeader;

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 B

View File

@@ -0,0 +1,225 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule NavigationAnimatedView
* @flow
*/
'use strict';
var Animated = require('Animated');
var Map = require('Map');
var NavigationStateUtils = require('NavigationState');
var NavigationContainer = require('NavigationContainer');
var React = require('react-native');
var View = require('View');
import type {
NavigationState,
NavigationParentState,
} from 'NavigationState';
type NavigationScene = {
index: number,
state: NavigationState,
isStale: boolean,
};
/**
* Helper function to compare route keys (e.g. "9", "11").
*/
function compareKey(one: string, two: string): number {
var delta = one.length - two.length;
if (delta > 0) {
return 1;
}
if (delta < 0) {
return -1;
}
return one > two ? 1 : -1;
}
/**
* Helper function to sort scenes based on their index and view key.
*/
function compareScenes(
one: NavigationScene,
two: NavigationScene
): number {
if (one.index > two.index) {
return 1;
}
if (one.index < two.index) {
return -1;
}
return compareKey(
one.state.key,
two.state.key
);
}
type Layout = {
initWidth: number,
initHeight: number,
width: Animated.Value;
height: Animated.Value;
};
type OverlayRenderer = (
position: Animated.Value,
layout: Layout
) => ReactElement;
type SceneRenderer = (
state: NavigationState,
index: number,
position: Animated.Value,
layout: Layout
) => ReactElement
type Props = {
navigationState: NavigationParentState;
renderScene: SceneRenderer;
renderOverlay: ?OverlayRenderer;
style: any;
};
class NavigationAnimatedView extends React.Component {
_animatedHeight: Animated.Value;
_animatedWidth: Animated.Value;
_lastHeight: number;
_lastWidth: number;
props: Props;
constructor(props) {
super(props);
this._animatedHeight = new Animated.Value(0);
this._animatedWidth = new Animated.Value(0);
this.state = {
position: new Animated.Value(this.props.navigationState.index),
scenes: new Map(),
};
}
componentWillMount() {
this.setState({
scenes: this._reduceScenes(this.state.scenes, this.props.navigationState),
});
}
componentDidMount() {
this.postionListener = this.state.position.addListener(this._onProgressChange.bind(this));
}
componentWillReceiveProps(nextProps) {
if (nextProps.navigationState !== this.props.navigationState) {
this.setState({
scenes: this._reduceScenes(this.state.scenes, nextProps.navigationState, this.props.navigationState),
});
}
}
componentDidUpdate(lastProps) {
if (lastProps.navigationState.index !== this.props.navigationState.index) {
Animated.spring(
this.state.position,
{toValue: this.props.navigationState.index}
).start();
}
}
componentWillUnmount() {
if (this.postionListener) {
this.state.position.removeListener(this.postionListener);
this.postionListener = null;
}
}
_onProgressChange(data: Object): void {
if (Math.abs(data.value - this.props.navigationState.index) > Number.EPSILON) {
return;
}
this.state.scenes.forEach((scene, index) => {
if (scene.isStale) {
const scenes = this.state.scenes.slice();
scenes.splice(index, 1);
this.setState({ scenes, });
}
});
}
_reduceScenes(
scenes: Array<NavigationScene>,
nextState: NavigationParentState,
lastState: ?NavigationParentState
): Array<NavigationScene> {
let nextScenes = nextState.children.map((child, index) => {
return {
index,
state: child,
isStale: false,
};
});
if (lastState) {
lastState.children.forEach((child, index) => {
if (!NavigationStateUtils.get(nextState, child.key)) {
nextScenes.push({
index,
state: child,
isStale: true,
});
}
});
}
nextScenes = nextScenes.sort(compareScenes);
return nextScenes;
}
render() {
return (
<View
onLayout={(e) => {
const {height, width} = e.nativeEvent.layout;
this._animatedHeight &&
this._animatedHeight.setValue(height);
this._animatedWidth &&
this._animatedWidth.setValue(width);
this._lastHeight = height;
this._lastWidth = width;
}}
style={this.props.style}>
{this.state.scenes.map(this._renderScene, this)}
{this._renderOverlay(this._renderOverlay, this)}
</View>
);
}
_getLayout() {
return {
height: this._animatedHeight,
width: this._animatedWidth,
initWidth: this._lastWidth,
initHeight: this._lastHeight,
};
}
_renderScene(scene: NavigationScene) {
return this.props.renderScene(
scene.state,
scene.index,
this.state.position,
this._getLayout()
);
}
_renderOverlay() {
const {renderOverlay} = this.props;
if (renderOverlay) {
return renderOverlay(
this.state.position,
this._getLayout()
);
}
return null;
}
}
NavigationAnimatedView = NavigationContainer.create(NavigationAnimatedView);
module.exports = NavigationAnimatedView;

View File

@@ -0,0 +1,51 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule NavigationContainer
* @flow
*/
'use strict';
var React = require('react-native');
var NavigationRootContainer = require('NavigationRootContainer');
function createNavigationContainer(Component: React.Component): React.Component {
class NavigationComponent extends React.Component {
render() {
return (
<Component
onNavigate={this.getNavigationHandler()}
{...this.props}
/>
);
}
getNavigationHandler() {
return this.props.onNavigate || this.context.onNavigate;
}
getChildContext() {
return {
onNavigate: this.getNavigationHandler(),
};
}
}
NavigationComponent.contextTypes = {
onNavigate: React.PropTypes.func,
};
NavigationComponent.childContextTypes = {
onNavigate: React.PropTypes.func,
};
return NavigationComponent;
}
var NavigationContainer = {
create: createNavigationContainer,
RootContainer: NavigationRootContainer,
};
module.exports = NavigationContainer;

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule NavigationExperimental
* @flow
*/
'use strict';
const NavigationAnimatedView = require('NavigationAnimatedView');
const NavigationCard = require('NavigationCard');
const NavigationContainer = require('NavigationContainer');
const NavigationHeader = require('NavigationHeader');
const NavigationRootContainer = require('NavigationRootContainer');
const NavigationReducer = require('NavigationReducer');
const NavigationState = require('NavigationState');
const NavigationView = require('NavigationView');
const NavigationExperimental = {
// Core
State: NavigationState,
Reducer: NavigationReducer,
// Containers
Container: NavigationContainer,
RootContainer: NavigationRootContainer,
// Views
View: NavigationView,
AnimatedView: NavigationAnimatedView,
// CustomComponents:
Header: NavigationHeader,
Card: NavigationCard,
};
module.exports = NavigationExperimental;

View File

@@ -0,0 +1,86 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule NavigationRootContainer
* @flow
*/
'use strict';
var AsyncStorage = require('AsyncStorage');
var React = require('React');
import type {
NavigationState,
NavigationReducer
} from 'NavigationState';
type NavigationRenderer = (
navigationState: NavigationState,
onNavigate: Function
) => ReactElement;
type Props = {
renderNavigation: NavigationRenderer;
reducer: NavigationReducer;
persistenceKey: ?string;
};
class NavigationRootContainer extends React.Component {
props: Props;
constructor(props: Props) {
super(props);
this.handleNavigation = this.handleNavigation.bind(this);
let navState = null;
if (!this.props.persistenceKey) {
navState = this.props.reducer(null, null);
}
this.state = { navState };
}
componentDidMount() {
if (this.props.persistenceKey) {
AsyncStorage.getItem(this.props.persistenceKey, (err, storedString) => {
if (err || !storedString) {
this.setState({
navState: this.props.reducer(null, null),
});
return;
}
this.setState({
navState: JSON.parse(storedString),
});
});
}
}
getChildContext(): Object {
return {
onNavigate: this.handleNavigation,
};
}
handleNavigation(action: Object) {
const navState = this.props.reducer(this.state.navState, action);
this.setState({
navState,
});
if (this.props.persistenceKey) {
AsyncStorage.setItem(this.props.persistenceKey, JSON.stringify(navState));
}
}
render(): ReactElement {
var navigation = this.props.renderNavigation(
this.state.navState,
this.handleNavigation
);
return navigation;
}
}
NavigationRootContainer.childContextTypes = {
onNavigate: React.PropTypes.func,
};
module.exports = NavigationRootContainer;

View File

@@ -0,0 +1,196 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule NavigationState
* @flow
*/
'use strict';
const invariant = require('invariant');
export type NavigationState = {
key: string;
};
export type NavigationParentState = {
key: string;
index: number;
children: Array<NavigationState>;
};
export type NavigationReducer = (
state: ?NavigationState,
action: ?any
) => ?NavigationState;
export type NavigationReducerWithDefault = (
state: ?NavigationState,
action: ?any
) => NavigationState;
export function getParent(state: NavigationState): ?NavigationParentState {
if (
(state instanceof Object) &&
(state.children instanceof Array) &&
(state.children[0] !== undefined) &&
(typeof state.index === 'number') &&
(state.children[state.index] !== undefined)
) {
return state;
}
return null;
}
export function get(state: NavigationState, key: string): ?NavigationState {
const parentState = getParent(state);
if (!parentState) {
return null;
}
const childState = parentState.children.find(child => child.key === key);
return childState || null;
}
export function indexOf(state: NavigationState, key: string): ?number {
const parentState = getParent(state);
if (!parentState) {
return null;
}
const index = parentState.children.map(child => child.key).indexOf(key);
if (index === -1) {
return null;
}
return index;
}
export function push(state: NavigationState, newChildState: NavigationState): NavigationState {
const parentState = getParent(state);
if (!parentState) {
return state;
}
var lastChildren: Array<NavigationState> = parentState.children;
return {
...parentState,
children: [
...lastChildren,
newChildState,
],
index: lastChildren.length,
};
}
export function pop(state: NavigationState): NavigationState {
const parentState = getParent(state);
if (!parentState) {
return state;
}
const lastChildren = parentState.children;
return {
...parentState,
children: lastChildren.slice(0, lastChildren.length - 1),
index: lastChildren.length - 2,
};
}
export function reset(state: NavigationState, nextChildren: ?Array<NavigationState>, nextIndex: ?number): NavigationState {
const parentState = getParent(state);
if (!parentState) {
return state;
}
const children = nextChildren || parentState.children;
const index = nextIndex == null ? parentState.index : nextIndex;
if (children === parentState.children && index === parentState.index) {
return state;
}
return {
...parentState,
children,
index,
};
}
export function set(state: ?NavigationState, key: string, nextChildren: Array<NavigationState>, nextIndex: number): NavigationState {
if (!state) {
return {
children: nextChildren,
index: nextIndex,
key,
};
}
const parentState = getParent(state);
if (!parentState) {
return {
children: nextChildren,
index: nextIndex,
key,
};
}
if (nextChildren === parentState.children && nextIndex === parentState.index && key === parentState.key) {
return parentState;
}
return {
...parentState,
children: nextChildren,
index: nextIndex,
key,
};
}
export function jumpToIndex(state: NavigationState, index: number): NavigationState {
const parentState = getParent(state);
return {
...parentState,
index,
};
}
export function jumpTo(state: NavigationState, key: string): NavigationState {
const parentState = getParent(state);
if (!parentState) {
return state;
}
const index = parentState.children.indexOf(parentState.children.find(child => child.key === key));
invariant(
index !== -1,
'Cannot find child with matching key in this NavigationState'
);
return {
...parentState,
index,
};
}
export function replaceAt(state: NavigationState, key: string, newState: NavigationState): NavigationState {
const parentState = getParent(state);
if (!parentState) {
return state;
}
const children = [...parentState.children];
const index = parentState.children.indexOf(parentState.children.find(child => child.key === key));
invariant(
index !== -1,
'Cannot find child with matching key in this NavigationState'
);
children[index] = newState;
return {
...parentState,
children,
};
}
export function replaceAtIndex(state: NavigationState, index: number, newState: NavigationState): NavigationState {
const parentState = getParent(state);
if (!parentState) {
return state;
}
const children = [...parentState.children];
children[index] = newState;
return {
...parentState,
children,
};
}

View File

@@ -0,0 +1,63 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule NavigationView
* @flow
*/
'use strict';
var React = require('react-native');
var NavigationContainer = require('NavigationContainer');
var {
StyleSheet,
View,
} = React;
var NavigationView = React.createClass({
propTypes: {
// todo, figure out a propType for getK
navigationState: React.PropTypes.object.isRequired,
renderScene: React.PropTypes.func.isRequired,
},
render: function() {
return (
<View
style={this.props.style}>
{this.props.navigationState.children.map(this._renderScene)}
</View>
);
},
_renderScene: function(route, index) {
var isSelected = index === this.props.navigationState.index;
return (
<View
key={route.key}
pointerEvents={isSelected ? 'auto' : 'none'}
style={[
styles.navView,
{opacity: isSelected ? 1 : 0},
]}>
{this.props.renderScene(route, index)}
</View>
);
},
});
NavigationView = NavigationContainer.create(NavigationView);
var styles = StyleSheet.create({
navView: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
});
module.exports = NavigationView;

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule NavigationFindReducer
* @flow
*/
'use strict';
/*
* NavigationFindReducer takes an array of reducers, and returns a reducer that
* iterates through all of the reducers and the result of the first reducer
* that modifies the input
*/
import type {
NavigationState,
NavigationReducer
} from 'NavigationState';
function NavigationFindReducer(reducers: Array<NavigationReducer>): ?NavigationReducer {
return function(lastState: ?NavigationState, action: ?any): ?NavigationState {
for (let i = 0; i < reducers.length; i++) {
let reducer = reducers[i];
let newState = reducer(lastState, action);
if (newState !== lastState) {
return newState;
}
}
return lastState;
};
}
module.exports = NavigationFindReducer;

View File

@@ -0,0 +1,24 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule NavigationReducer
* @flow
*/
'use strict';
var NavigationFindReducer = require('NavigationFindReducer');
var NavigationStackReducer = require('NavigationStackReducer');
var NavigationTabsReducer = require('NavigationTabsReducer');
const NavigationReducer = {
FindReducer: NavigationFindReducer,
StackReducer: NavigationStackReducer,
TabsReducer: NavigationTabsReducer,
};
module.exports = NavigationReducer;

View File

@@ -0,0 +1,141 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule NavigationStackReducer
* @flow
*/
'use strict';
var NavigationStateUtils = require('NavigationState');
import type {
NavigationState,
NavigationReducer,
} from 'NavigationState';
export type NavigationStackReducerAction = {
type: string,
};
const ActionTypes = {
PUSH: 'react-native/NavigationExperimental/stack-push',
POP: 'react-native/NavigationExperimental/stack-pop',
JUMP_TO: 'react-native/NavigationExperimental/stack-jumpTo',
JUMP_TO_INDEX: 'react-native/NavigationExperimental/stack-jumpToIndex',
RESET: 'react-native/NavigationExperimental/stack-reset',
};
const DEFAULT_KEY = 'NAV_STACK_DEFAULT_KEY';
function NavigationStackPushAction(state: NavigationState): NavigationStackReducerAction {
return {
type: ActionTypes.PUSH,
state,
};
}
function NavigationStackPopAction(): NavigationStackReducerAction {
return {
type: ActionTypes.POP,
};
}
function NavigationStackJumpToAction(key: string): NavigationStackReducerAction {
return {
type: ActionTypes.JUMP_TO,
key,
};
}
function NavigationStackJumpToIndexAction(index: number): NavigationStackReducerAction {
return {
type: ActionTypes.JUMP_TO_INDEX,
index,
};
}
function NavigationStackResetAction(children: Array<NavigationState>, index: number): NavigationStackReducerAction {
return {
type: ActionTypes.RESET,
index,
children,
};
}
type StackReducerConfig = {
initialStates: Array<NavigationState>;
initialIndex: ?number;
key: ?string;
matchAction: (action: any) => boolean;
actionStateMap: (action: any) => NavigationState;
};
function NavigationStackReducer({initialStates, initialIndex, key, matchAction, actionStateMap}: StackReducerConfig): NavigationReducer {
return function (lastState: ?NavigationState, action: any): NavigationState {
if (key == null) {
key = DEFAULT_KEY;
}
if (initialIndex == null) {
initialIndex = initialStates.length - 1;
}
if (!lastState) {
lastState = {
index: initialIndex,
children: initialStates,
key,
};
}
const lastParentState = NavigationStateUtils.getParent(lastState);
if (!action || !lastParentState) {
return lastState;
}
switch (action.type) {
case ActionTypes.PUSH:
return NavigationStateUtils.push(
lastParentState,
action.state
);
case ActionTypes.POP:
if (lastParentState.index === 0 || lastParentState.children.length === 1) {
return lastParentState;
}
return NavigationStateUtils.pop(lastParentState);
case ActionTypes.JUMP_TO:
return NavigationStateUtils.jumpTo(
lastParentState,
action.key
);
case ActionTypes.JUMP_TO_INDEX:
return NavigationStateUtils.jumpToIndex(
lastParentState,
action.index
);
case ActionTypes.RESET:
return {
...lastParentState,
index: action.index,
children: action.children,
};
}
if (matchAction(action)) {
return NavigationStateUtils.push(
lastParentState,
actionStateMap(action)
);
}
return lastParentState;
};
}
NavigationStackReducer.PushAction = NavigationStackPushAction;
NavigationStackReducer.PopAction = NavigationStackPopAction;
NavigationStackReducer.JumpToAction = NavigationStackJumpToAction;
NavigationStackReducer.JumpToIndexAction = NavigationStackJumpToIndexAction;
NavigationStackReducer.ResetAction = NavigationStackResetAction;
module.exports = NavigationStackReducer;

View File

@@ -0,0 +1,143 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule NavigationTabsReducer
* @flow
*/
'use strict';
const NavigationFindReducer = require('NavigationFindReducer');
const NavigationStateUtils = require('NavigationState');
import type {
NavigationReducer,
NavigationReducerWithDefault,
NavigationState
} from 'NavigationState';
const ActionTypes = {
JUMP_TO: 'react-native/NavigationExperimental/tabs-jumpTo',
ON_TAB_ACTION: 'react-native/NavigationExperimental/tabs-onTabAction',
};
const DEFAULT_KEY = 'TABS_STATE_DEFAULT_KEY';
export type JumpToAction = {
type: typeof ActionTypes.JUMP_TO,
index: number,
};
function NavigationTabsJumpToAction(index: number): JumpToAction {
return {
type: ActionTypes.JUMP_TO,
index,
};
}
export type OnTabAction = {
type: string,
index: number,
action: any,
};
function NavigationTabsOnTabAction(index: number, action: any): OnTabAction {
return {
type: ActionTypes.ON_TAB_ACTION,
index,
action,
};
}
type TabsReducerConfig = {
key: string;
initialIndex: ?number;
tabReducers: Array<NavigationReducerWithDefault>;
};
function NavigationTabsReducer({key, initialIndex, tabReducers}: TabsReducerConfig): NavigationReducer {
if (initialIndex == null) {
initialIndex = 0;
}
if (key == null) {
key = DEFAULT_KEY;
}
return function(lastNavState: ?NavigationState, action: ?any): ?NavigationState {
if (!lastNavState) {
lastNavState = {
children: tabReducers.map(reducer => reducer(null, null)),
index: initialIndex,
key,
};
}
const lastParentNavState = NavigationStateUtils.getParent(lastNavState);
if (!action || !lastParentNavState) {
return lastNavState;
}
if (
action.type === ActionTypes.JUMP_TO &&
action.index !== lastParentNavState.index
) {
return NavigationStateUtils.jumpToIndex(
lastParentNavState,
action.index,
);
}
if (action.type === ActionTypes.ON_TAB_ACTION) {
const onTabAction: OnTabAction = action;
const lastTabRoute = lastParentNavState.children[onTabAction.index];
const tabReducer = tabReducers[onTabAction.index];
if (tabReducer) {
const newTabRoute = tabReducer(lastTabRoute, action.action);
if (newTabRoute && newTabRoute !== lastTabRoute) {
let navState = NavigationStateUtils.replaceAtIndex(
lastParentNavState,
onTabAction.index,
newTabRoute
);
navState = NavigationStateUtils.jumpToIndex(
navState,
onTabAction.index
);
return navState;
}
}
}
const subReducers = tabReducers.map((tabReducer, tabIndex) => {
return function reduceTab(lastTabState: ?NavigationState, tabAction: ?any): ?NavigationState {
if (!lastTabState) {
return tabReducer(lastTabState, tabAction);
}
if (!lastParentNavState) {
return lastTabState;
}
const lastSubTabState = lastParentNavState.children[tabIndex];
const nextSubTabState = tabReducer(lastSubTabState, tabAction);
if (nextSubTabState && lastSubTabState !== nextSubTabState) {
const tabs = lastParentNavState.children;
tabs[tabIndex] = nextSubTabState;
return {
...lastParentNavState,
tabs,
index: tabIndex,
};
}
return lastParentNavState;
};
});
let selectedTabReducer = subReducers.splice(lastParentNavState.index, 1)[0];
subReducers.unshift(selectedTabReducer);
const findReducer = NavigationFindReducer(subReducers);
if (findReducer) {
return findReducer(lastParentNavState, action);
}
return lastParentNavState;
};
}
NavigationTabsReducer.JumpToAction = NavigationTabsJumpToAction;
NavigationTabsReducer.OnTabAction = NavigationTabsOnTabAction;
module.exports = NavigationTabsReducer;

View File

@@ -0,0 +1,46 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow-broken
*/
'use strict';
jest
.autoMockOff()
.mock('ErrorUtils');
const NavigationFindReducer = require('NavigationFindReducer');
describe('NavigationFindReducer', () => {
it('handles basic find reducing with strings', () => {
let reducer = NavigationFindReducer([
s => s,
s => s + '_yes',
s => 'nope',
]);
let route = reducer('input');
expect(route).toBe('input_yes');
reducer = NavigationFindReducer([
(s, action) => s,
(s, action) => 'origRoute',
(s, action) => 'firstChangedState',
]);
route = reducer('origRoute', 'action1');
expect(route).toBe('firstChangedState');
reducer = NavigationFindReducer([
(s, action) => s,
(s, action) => action,
]);
route = reducer('inputState', 'action2');
expect(route).toBe('action2');
});
});

View File

@@ -0,0 +1,197 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow-broken
*/
'use strict';
jest
.autoMockOff()
.mock('ErrorUtils');
const NavigationStackReducer = require('NavigationStackReducer');
const {
JumpToAction,
JumpToIndexAction,
PopAction,
PushAction,
ResetAction,
} = NavigationStackReducer;
describe('NavigationStackReducer', () => {
it('handles PushAction', () => {
const initialStates = [
{key: 'route0'},
{key: 'route1'},
];
let reducer = NavigationStackReducer({
initialStates,
matchAction: () => true,
actionStateMap: (action) => action,
});
let state = reducer();
expect(state.children).toBe(initialStates);
expect(state.index).toBe(1);
expect(state.key).toBe('NAV_STACK_DEFAULT_KEY');
state = reducer(state, PushAction({key: 'route2'}));
expect(state.children[0].key).toBe('route0');
expect(state.children[1].key).toBe('route1');
expect(state.children[2].key).toBe('route2');
expect(state.index).toBe(2);
});
it('handles PopAction', () => {
let reducer = NavigationStackReducer({
initialStates: [
{key: 'a'},
{key: 'b'},
],
initialIndex: 1,
key: 'myStack',
matchAction: () => true,
actionStateMap: (action) => action,
});
let state = reducer();
expect(state.children[0].key).toBe('a');
expect(state.children[1].key).toBe('b');
expect(state.children.length).toBe(2);
expect(state.index).toBe(1);
expect(state.key).toBe('myStack');
state = reducer(state, PopAction());
expect(state.children[0].key).toBe('a');
expect(state.children.length).toBe(1);
expect(state.index).toBe(0);
expect(state.key).toBe('myStack');
// make sure Pop on an single-route state is a no-op
state = reducer(state, PopAction());
expect(state.children[0].key).toBe('a');
expect(state.children.length).toBe(1);
expect(state.index).toBe(0);
expect(state.key).toBe('myStack');
});
it('handles JumpToAction', () => {
let reducer = NavigationStackReducer({
initialStates: [
{key: 'a'},
{key: 'b'},
{key: 'c'},
],
initialIndex: 0,
key: 'myStack',
matchAction: () => true,
actionStateMap: (action) => action,
});
let state = reducer();
expect(state.children[0].key).toBe('a');
expect(state.children[1].key).toBe('b');
expect(state.children[2].key).toBe('c');
expect(state.children.length).toBe(3);
expect(state.index).toBe(0);
state = reducer(state, JumpToAction('b'));
expect(state.children[0].key).toBe('a');
expect(state.children[1].key).toBe('b');
expect(state.children[2].key).toBe('c');
expect(state.children.length).toBe(3);
expect(state.index).toBe(1);
state = reducer(state, JumpToAction('c'));
expect(state.children[0].key).toBe('a');
expect(state.children[1].key).toBe('b');
expect(state.children[2].key).toBe('c');
expect(state.children.length).toBe(3);
expect(state.index).toBe(2);
state = reducer(state, JumpToAction('c'));
expect(state.children[0].key).toBe('a');
expect(state.children[1].key).toBe('b');
expect(state.children[2].key).toBe('c');
expect(state.children.length).toBe(3);
expect(state.index).toBe(2);
expect(state.key).toBe('myStack');
});
it('handles JumpToIndexAction', () => {
let reducer = NavigationStackReducer({
initialStates: [
{key: 'a'},
{key: 'b'},
{key: 'c'},
],
initialIndex: 2,
key: 'myStack',
matchAction: () => true,
actionStateMap: (action) => action,
});
let state = reducer();
expect(state.children.length).toBe(3);
expect(state.index).toBe(2);
state = reducer(state, JumpToIndexAction(0));
expect(state.children.length).toBe(3);
expect(state.index).toBe(0);
state = reducer(state, JumpToIndexAction(1));
expect(state.children[0].key).toBe('a');
expect(state.children[1].key).toBe('b');
expect(state.children[2].key).toBe('c');
expect(state.children.length).toBe(3);
expect(state.index).toBe(1);
expect(state.key).toBe('myStack');
});
it('handles ResetAction', () => {
let reducer = NavigationStackReducer({
initialStates: [
{key: 'a'},
{key: 'b'},
],
initialIndex: 1,
key: 'myStack',
matchAction: () => true,
actionStateMap: (action) => action,
});
let state = reducer();
expect(state.children[0].key).toBe('a');
expect(state.children[1].key).toBe('b');
expect(state.children.length).toBe(2);
expect(state.index).toBe(1);
state = reducer(state, ResetAction([{key: 'c'}, {key: 'd'}], 0));
expect(state.children[0].key).toBe('c');
expect(state.children[1].key).toBe('d');
expect(state.children.length).toBe(2);
expect(state.index).toBe(0);
const newStates = [
{key: 'e'},
{key: 'f'},
{key: 'g'},
];
state = reducer(state, ResetAction(newStates, 1));
expect(state.children[0].key).toBe('e');
expect(state.children[1].key).toBe('f');
expect(state.children[2].key).toBe('g');
expect(state.children.length).toBe(3);
expect(state.index).toBe(1);
expect(state.key).toBe('myStack');
});
});

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow-broken
*/
'use strict';
jest
.autoMockOff()
.mock('ErrorUtils');
const NavigationTabsReducer = require('NavigationTabsReducer');
const {
JumpToAction,
} = NavigationTabsReducer;
describe('NavigationTabsReducer', () => {
it('handles JumpTo with index', () => {
let reducer = NavigationTabsReducer({
tabReducers: [
(tabState, action) => tabState || 'a',
(tabState, action) => tabState || 'b',
(tabState, action) => tabState || 'c',
],
initialIndex: 1,
});
let navState = reducer();
expect(navState.children[0]).toBe('a');
expect(navState.children[1]).toBe('b');
expect(navState.children[2]).toBe('c');
expect(navState.children.length).toBe(3);
expect(navState.index).toBe(1);
navState = reducer(
navState,
JumpToAction(2)
);
expect(navState.children[0]).toEqual('a');
expect(navState.children[1]).toEqual('b');
expect(navState.children[2]).toEqual('c');
expect(navState.children.length).toBe(3);
expect(navState.index).toBe(2);
});
});

View File

@@ -0,0 +1,57 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow-broken
*/
'use strict';
jest
.autoMockOff()
.mock('ErrorUtils');
var NavigationState = require('NavigationState');
var VALID_PARENT_STATES = [
{children: ['a','b'], index: 0},
{children: [{key: 'a'},{key: 'b', foo: 123}], index: 1},
{children: [{key: 'a'},{key: 'b'}], index: 0},
{children: [{key: 'a'},{key: 'b'}], index: 2},
];
var INVALID_PARENT_STATES = [
'foo',
{},
{children: [{key: 'a'}], index: 4},
{children: [{key: 'a'}], index: -1},
{children: [{key: 'a'}]},
{children: {key: 'foo'}},
12,
null,
undefined,
[],
];
describe('NavigationState', () => {
it('identifies parents correctly with getParent', () => {
for (var i = 0; i <= VALID_PARENT_STATES.length; i++) {
var navState = VALID_PARENT_STATES[0];
expect(NavigationState.getParent(navState)).toBe(navState);
}
for (var i = 0; i <= INVALID_PARENT_STATES.length; i++) {
var navState = INVALID_PARENT_STATES[0];
expect(NavigationState.getParent(navState)).toBe(null);
}
});
it('can get children', () => {
var fooState = {key: 'foo'};
var navState = {children: [{key: 'foobar'}, fooState], index: 0};
expect(NavigationState.get(navState, 'foo')).toBe(fooState);
expect(NavigationState.get(navState, 'missing')).toBe(null);
});
});

View File

@@ -76,6 +76,7 @@ var ReactNative = {
get LayoutAnimation() { return require('LayoutAnimation'); },
get Linking() { return require('Linking'); },
get LinkingIOS() { return require('LinkingIOS'); },
get NavigationExperimental() { return require('NavigationExperimental'); },
get NetInfo() { return require('NetInfo'); },
get PanResponder() { return require('PanResponder'); },
get PixelRatio() { return require('PixelRatio'); },