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'),