Navigation Back support and examples for Android

Summary:
public
- Intro new back action
- Add support in the two main reducers
- Use it in examples to support Android back button
- Disable NavigationCard gestures on Android

Reviewed By: hedgerwang

Differential Revision: D2914154

fb-gh-sync-id: d4dce6538e19613a2ffca21e2e3b2ecaded3d5dc
shipit-source-id: d4dce6538e19613a2ffca21e2e3b2ecaded3d5dc
This commit is contained in:
Eric Vicenti 2016-02-08 20:02:45 -08:00 committed by facebook-github-bot-5
parent 7b57b5c84a
commit 7b2b0c3c1c
13 changed files with 157 additions and 54 deletions

View File

@ -44,11 +44,18 @@ class NavigationAnimatedExample extends React.Component {
return (
<NavigationRootContainer
reducer={NavigationBasicReducer}
ref={navRootContainer => { this.navRootContainer = navRootContainer; }}
persistenceKey="NavigationAnimatedExampleState"
renderNavigation={this._renderNavigated}
/>
);
}
handleBackAction() {
return (
this.navRootContainer &&
this.navRootContainer.handleNavigation(NavigationRootContainer.getBackAction())
);
}
_renderNavigated(navigationState, onNavigate) {
if (!navigationState) {
return null;

View File

@ -40,6 +40,7 @@ const NavigationBasicExample = React.createClass({
<NavigationRootContainer
reducer={NavigationBasicReducer}
persistenceKey="NavigationBasicExampleState"
ref={navRootContainer => { this.navRootContainer = navRootContainer; }}
renderNavigation={(navState, onNavigate) => {
if (!navState) { return null; }
return (
@ -69,6 +70,14 @@ const NavigationBasicExample = React.createClass({
/>
);
},
handleBackAction: function() {
return (
this.navRootContainer &&
this.navRootContainer.handleNavigation(NavigationRootContainer.getBackAction())
);
},
});
const styles = StyleSheet.create({

View File

@ -135,18 +135,18 @@ function stateTypeTitleMap(pageState) {
}
}
let ExampleTabScreen = React.createClass({
render: function() {
class ExampleTabScreen extends React.Component {
render() {
return (
<NavigationAnimatedView
style={styles.tabContent}
navigationState={this.props.navigationState}
renderOverlay={this._renderHeader}
renderScene={this._renderScene}
renderOverlay={this._renderHeader.bind(this)}
renderScene={this._renderScene.bind(this)}
/>
);
},
_renderHeader: function(position, layout) {
}
_renderHeader(position, layout) {
return (
<NavigationHeader
navigationState={this.props.navigationState}
@ -155,8 +155,8 @@ let ExampleTabScreen = React.createClass({
getTitle={state => stateTypeTitleMap(state)}
/>
);
},
_renderScene: function(child, index, position, layout) {
}
_renderScene(child, index, position, layout) {
return (
<NavigationCard
key={child.key}
@ -187,21 +187,28 @@ let ExampleTabScreen = React.createClass({
</ScrollView>
</NavigationCard>
);
},
});
}
}
ExampleTabScreen = NavigationContainer.create(ExampleTabScreen);
const NavigationCompositionExample = React.createClass({
render: function() {
class NavigationCompositionExample extends React.Component {
render() {
return (
<NavigationRootContainer
reducer={ExampleAppReducer}
persistenceKey="NavigationCompositionExampleState"
renderNavigation={this.renderApp}
ref={navRootContainer => { this.navRootContainer = navRootContainer; }}
renderNavigation={this.renderApp.bind(this)}
/>
);
},
renderApp: function(navigationState, onNavigate) {
}
handleBackAction() {
return (
this.navRootContainer &&
this.navRootContainer.handleNavigation(NavigationRootContainer.getBackAction())
);
}
renderApp(navigationState, onNavigate) {
if (!navigationState) {
return null;
}
@ -217,11 +224,11 @@ const NavigationCompositionExample = React.createClass({
/>
</View>
);
},
});
}
}
let ExampleMainView = React.createClass({
render: function() {
class ExampleMainView extends React.Component {
render() {
return (
<NavigationView
navigationState={this.props.navigationState}
@ -230,20 +237,20 @@ let ExampleMainView = React.createClass({
<ExampleTabScreen
key={tabState.key}
navigationState={tabState}
onNavigate={this._handleNavigation.bind(null, tabState.key)}
onNavigate={this._handleNavigation.bind(this, tabState.key)}
/>
)}
/>
);
},
_handleNavigation: function(tabKey, action) {
}
_handleNavigation(tabKey, action) {
if (ExampleExitAction.match(action)) {
this.props.onExampleExit();
return;
}
this.props.onNavigate(action);
},
});
}
}
ExampleMainView = NavigationContainer.create(ExampleMainView);
const styles = StyleSheet.create({

View File

@ -25,7 +25,7 @@ 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:
* To learn how to use the Navigation API, take a look at the following example files:
*/
var EXAMPLES = {
'Tabs': require('./NavigationTabsExample'),
@ -106,13 +106,34 @@ var NavigationExperimentalExample = React.createClass({
this.setExample('menu');
},
handleBackAction: function() {
const wasHandledByExample = (
this.exampleRef &&
this.exampleRef.handleBackAction &&
this.exampleRef.handleBackAction()
);
if (wasHandledByExample) {
return true;
}
if (this.state.example && this.state.example !== 'menu') {
this._exitInnerExample();
return true;
}
return false;
},
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 (
<Component
onExampleExit={this._exitInnerExample}
ref={exampleRef => { this.exampleRef = exampleRef; }}
/>
);
}
return null;
},

View File

@ -64,12 +64,13 @@ const ExampleTabsReducer = NavigationReducer.TabsReducer({
],
});
const NavigationTabsExample = React.createClass({
render: function() {
class NavigationTabsExample extends React.Component {
render() {
return (
<NavigationRootContainer
reducer={ExampleTabsReducer}
persistenceKey="NAV_EXAMPLE_STATE_TABS"
ref={navRootContainer => { this.navRootContainer = navRootContainer; }}
renderNavigation={(navigationState) => {
if (!navigationState) { return null; }
return (
@ -88,8 +89,14 @@ const NavigationTabsExample = React.createClass({
}}
/>
);
},
});
}
handleBackAction() {
return (
this.navRootContainer &&
this.navRootContainer.handleNavigation(NavigationRootContainer.getBackAction())
);
}
}
const styles = StyleSheet.create({
topView: {

View File

@ -109,12 +109,21 @@ var UIExplorerApp = React.createClass({
style={styles.toolbar}
title={this.state.example.title}
/>
<Component />
<Component
ref={(example) => { this._exampleRef = example; }}
/>
</View>
);
},
_handleBackButtonPress: function() {
if (
this._exampleRef &&
this._exampleRef.handleBackAction &&
this._exampleRef.handleBackAction()
) {
return true;
}
if (this.state.example.title !== this._getUIExplorerHome().title) {
this.onSelectExample(this._getUIExplorerHome());
return true;

View File

@ -54,6 +54,7 @@ var APIS = [
require('./IntentAndroidExample.android'),
require('./LayoutEventsExample'),
require('./LayoutExample'),
require('./NavigationExperimental/NavigationExperimentalExample'),
require('./NetInfoExample'),
require('./PanResponderExample'),
require('./PointerEventsExample'),

View File

@ -31,10 +31,13 @@ const Animated = require('Animated');
const NavigationReducer = require('NavigationReducer');
const NavigationContainer = require('NavigationContainer');
const PanResponder = require('PanResponder');
const Platform = require('Platform');
const React = require('React');
const StyleSheet = require('StyleSheet');
const View = require('View');
const ENABLE_GESTURES = Platform.OS !== 'android';
import type {
NavigationParentState
} from 'NavigationState';
@ -62,7 +65,12 @@ class NavigationCard extends React.Component {
_widthListener: string;
_heightListener: string;
props: Props;
componentWillMount(props) {
componentWillMount() {
if (ENABLE_GESTURES) {
this._enableGestures();
}
}
_enableGestures() {
this._responder = PanResponder.create({
onMoveShouldSetPanResponder: (e, {dx, dy, moveX, moveY, x0, y0}) => {
if (this.props.navigationState.index === 0) {
@ -119,9 +127,10 @@ class NavigationCard extends React.Component {
render() {
const cardPosition = Animated.add(this.props.position, new Animated.Value(-this.props.index));
const gestureValue = Animated.multiply(cardPosition, this.props.layout.width);
const touchResponderHandlers = this._responder ? this._responder.panHandlers : null;
return (
<Animated.View
{...this._responder.panHandlers}
{...touchResponderHandlers}
style={[
styles.card,
{

View File

@ -11,19 +11,29 @@
*/
'use strict';
var AsyncStorage = require('AsyncStorage');
var React = require('React');
const AsyncStorage = require('AsyncStorage');
const React = require('React');
const BackAndroid = require('BackAndroid');
const Platform = require('Platform');
import type {
NavigationState,
NavigationReducer
} from 'NavigationState';
type NavigationRenderer = (
export type NavigationRenderer = (
navigationState: NavigationState,
onNavigate: Function
) => ReactElement;
export type BackAction = {
type: 'BackAction';
};
function getBackAction(): BackAction {
return { type: 'BackAction' };
}
type Props = {
renderNavigation: NavigationRenderer;
reducer: NavigationReducer;
@ -61,17 +71,21 @@ class NavigationRootContainer extends React.Component {
onNavigate: this.handleNavigation,
};
}
handleNavigation(action: Object) {
handleNavigation(action: Object): boolean {
const navState = this.props.reducer(this.state.navState, action);
if (navState === this.state.navState) {
return false;
}
this.setState({
navState,
});
if (this.props.persistenceKey) {
AsyncStorage.setItem(this.props.persistenceKey, JSON.stringify(navState));
}
return true;
}
render(): ReactElement {
var navigation = this.props.renderNavigation(
const navigation = this.props.renderNavigation(
this.state.navState,
this.handleNavigation
);
@ -83,4 +97,6 @@ NavigationRootContainer.childContextTypes = {
onNavigate: React.PropTypes.func,
};
NavigationRootContainer.getBackAction = getBackAction;
module.exports = NavigationRootContainer;

View File

@ -23,9 +23,13 @@ export type NavigationParentState = {
children: Array<NavigationState>;
};
export type NavigationAction = {
type: string;
};
export type NavigationReducer = (
state: ?NavigationState,
action: ?any
action: ?NavigationAction
) => ?NavigationState;
export type NavigationReducerWithDefault = (
@ -142,6 +146,9 @@ export function set(state: ?NavigationState, key: string, nextChildren: Array<Na
export function jumpToIndex(state: NavigationState, index: number): NavigationState {
const parentState = getParent(state);
if (parentState && parentState.index === index) {
return parentState;
}
return {
...parentState,
index,

View File

@ -22,7 +22,7 @@ import type {
NavigationReducer
} from 'NavigationState';
function NavigationFindReducer(reducers: Array<NavigationReducer>): ?NavigationReducer {
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];

View File

@ -18,7 +18,11 @@ import type {
NavigationReducer,
} from 'NavigationState';
export type NavigationStackReducerAction = {
import type {
BackAction,
} from 'NavigationRootContainer';
export type NavigationStackReducerAction = BackAction | {
type: string,
};
@ -101,6 +105,7 @@ function NavigationStackReducer({initialStates, initialIndex, key, matchAction,
action.state
);
case ActionTypes.POP:
case 'BackAction':
if (lastParentState.index === 0 || lastParentState.children.length === 1) {
return lastParentState;
}

View File

@ -17,7 +17,8 @@ const NavigationStateUtils = require('NavigationState');
import type {
NavigationReducer,
NavigationReducerWithDefault,
NavigationState
NavigationState,
NavigationParentState
} from 'NavigationState';
const ActionTypes = {
@ -106,17 +107,15 @@ function NavigationTabsReducer({key, initialIndex, tabReducers}: TabsReducerConf
}
}
const subReducers = tabReducers.map((tabReducer, tabIndex) => {
return function reduceTab(lastTabState: ?NavigationState, tabAction: ?any): ?NavigationState {
if (!lastTabState) {
return tabReducer(lastTabState, tabAction);
return function reduceTab(lastNavState: ?NavigationState, tabAction: ?any): ?NavigationState {
if (!tabReducer || !lastNavState) {
return lastNavState;
}
if (!lastParentNavState) {
return lastTabState;
}
const lastSubTabState = lastParentNavState.children[tabIndex];
const lastParentNavState = NavigationStateUtils.getParent(lastNavState);
const lastSubTabState = lastParentNavState && lastParentNavState.children[tabIndex];
const nextSubTabState = tabReducer(lastSubTabState, tabAction);
if (nextSubTabState && lastSubTabState !== nextSubTabState) {
const tabs = lastParentNavState.children;
const tabs = lastParentNavState && lastParentNavState.children || [];
tabs[tabIndex] = nextSubTabState;
return {
...lastParentNavState,
@ -129,11 +128,17 @@ function NavigationTabsReducer({key, initialIndex, tabReducers}: TabsReducerConf
});
let selectedTabReducer = subReducers.splice(lastParentNavState.index, 1)[0];
subReducers.unshift(selectedTabReducer);
subReducers.push((lastParentNavState: ?NavigationState, action: ?any) => {
if (lastParentNavState && action && action.type === 'BackAction') {
return NavigationStateUtils.jumpToIndex(
lastParentNavState,
0
);
}
return lastParentNavState;
});
const findReducer = NavigationFindReducer(subReducers);
if (findReducer) {
return findReducer(lastParentNavState, action);
}
return lastParentNavState;
return findReducer(lastParentNavState, action);
};
}