mirror of
https://github.com/status-im/react-native.git
synced 2025-01-10 01:25:39 +00:00
38979f9c68
Summary: Not a API change, but this may break the layout of exisitng apps that uses NavigationHeader. For now, NavigationHeader uses absolute position, which makes it hard for NavigationCardStack to determine the height of the scenes. Theoretically, the height of the scenes would be the height of the cards stack minus the height of the header. That said, if we want to support the headers with different height (e.g. MyIOSHeader or MyAndroidHeader), we're forced to expose the height of the headers and manually compute the height of the scenes. Alternatively, if the header does not use absolute position, the height of the scenes can adjust automatically with flex box, and that's what this commit is about to do. Reviewed By: ericvicenti Differential Revision: D3671119 fbshipit-source-id: 26e48f801da3661c5d7dce4752ba927621172f4a
486 lines
12 KiB
JavaScript
486 lines
12 KiB
JavaScript
/**
|
|
* Copyright (c) 2013-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.
|
|
*
|
|
* 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 NavigationExampleRow = require('./NavigationExampleRow');
|
|
const React = require('react');
|
|
const ReactNative = require('react-native');
|
|
|
|
/**
|
|
* Basic example that shows how to use <NavigationCardStack /> to build
|
|
* an app with composite navigation system.
|
|
*/
|
|
|
|
const {
|
|
Component,
|
|
PropTypes,
|
|
} = React;
|
|
|
|
const {
|
|
NavigationExperimental,
|
|
ScrollView,
|
|
StyleSheet,
|
|
Text,
|
|
TouchableOpacity,
|
|
View,
|
|
} = ReactNative;
|
|
|
|
const {
|
|
CardStack: NavigationCardStack,
|
|
Header: NavigationHeader,
|
|
PropTypes: NavigationPropTypes,
|
|
StateUtils: NavigationStateUtils,
|
|
} = NavigationExperimental;
|
|
|
|
// First Step.
|
|
// Define what app navigation state will look like.
|
|
function createAppNavigationState(): Object {
|
|
return {
|
|
// Three tabs.
|
|
tabs: {
|
|
index: 0,
|
|
routes: [
|
|
{key: 'apple'},
|
|
{key: 'banana'},
|
|
{key: 'orange'},
|
|
],
|
|
},
|
|
// Scenes for the `apple` tab.
|
|
apple: {
|
|
index: 0,
|
|
routes: [{key: 'Apple Home'}],
|
|
},
|
|
// Scenes for the `banana` tab.
|
|
banana: {
|
|
index: 0,
|
|
routes: [{key: 'Banana Home'}],
|
|
},
|
|
// Scenes for the `orange` tab.
|
|
orange: {
|
|
index: 0,
|
|
routes: [{key: 'Orange Home'}],
|
|
},
|
|
};
|
|
}
|
|
|
|
// Next step.
|
|
// Define what app navigation state shall be updated.
|
|
function updateAppNavigationState(
|
|
state: Object,
|
|
action: Object,
|
|
): Object {
|
|
let {type} = action;
|
|
if (type === 'BackAction') {
|
|
type = 'pop';
|
|
}
|
|
|
|
switch (type) {
|
|
case 'push': {
|
|
// Push a route into the scenes stack.
|
|
const route: Object = action.route;
|
|
const {tabs} = state;
|
|
const tabKey = tabs.routes[tabs.index].key;
|
|
const scenes = state[tabKey];
|
|
const nextScenes = NavigationStateUtils.push(scenes, route);
|
|
if (scenes !== nextScenes) {
|
|
return {
|
|
...state,
|
|
[tabKey]: nextScenes,
|
|
};
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'pop': {
|
|
// Pops a route from the scenes stack.
|
|
const {tabs} = state;
|
|
const tabKey = tabs.routes[tabs.index].key;
|
|
const scenes = state[tabKey];
|
|
const nextScenes = NavigationStateUtils.pop(scenes);
|
|
if (scenes !== nextScenes) {
|
|
return {
|
|
...state,
|
|
[tabKey]: nextScenes,
|
|
};
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'selectTab': {
|
|
// Switches the tab.
|
|
const tabKey: string = action.tabKey;
|
|
const tabs = NavigationStateUtils.jumpTo(state.tabs, tabKey);
|
|
if (tabs !== state.tabs) {
|
|
return {
|
|
...state,
|
|
tabs,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
return state;
|
|
}
|
|
|
|
// Next step.
|
|
// Defines a helper function that creates a HOC (higher-order-component)
|
|
// which provides a function `navigate` through component props. The
|
|
// `navigate` function will be used to invoke navigation changes.
|
|
// This serves a convenient way for a component to navigate.
|
|
function createAppNavigationContainer(ComponentClass) {
|
|
const key = '_yourAppNavigationContainerNavigateCall';
|
|
|
|
class Container extends Component {
|
|
static contextTypes = {
|
|
[key]: PropTypes.func,
|
|
};
|
|
|
|
static childContextTypes = {
|
|
[key]: PropTypes.func.isRequired,
|
|
};
|
|
|
|
static propTypes = {
|
|
navigate: PropTypes.func,
|
|
};
|
|
|
|
getChildContext(): Object {
|
|
return {
|
|
[key]: this.context[key] || this.props.navigate,
|
|
};
|
|
}
|
|
|
|
render(): ReactElement {
|
|
const navigate = this.context[key] || this.props.navigate;
|
|
return <ComponentClass {...this.props} navigate={navigate} />;
|
|
}
|
|
}
|
|
|
|
return Container;
|
|
}
|
|
|
|
// Next step.
|
|
// Define a component for your application that owns the navigation state.
|
|
class YourApplication extends Component {
|
|
|
|
static propTypes = {
|
|
onExampleExit: PropTypes.func,
|
|
};
|
|
|
|
// This sets up the initial navigation state.
|
|
constructor(props, context) {
|
|
super(props, context);
|
|
// This sets up the initial navigation state.
|
|
this.state = createAppNavigationState();
|
|
this._navigate = this._navigate.bind(this);
|
|
}
|
|
|
|
render(): ReactElement {
|
|
// User your own navigator (see next step).
|
|
return (
|
|
<YourNavigator
|
|
appNavigationState={this.state}
|
|
navigate={this._navigate}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// This public method is optional. If exists, the UI explorer will call it
|
|
// the "back button" is pressed. Normally this is the cases for Android only.
|
|
handleBackAction(): boolean {
|
|
return this._navigate({type: 'pop'});
|
|
}
|
|
|
|
// This handles the navigation state changes. You're free and responsible
|
|
// to define the API that changes that navigation state. In this exmaple,
|
|
// we'd simply use a `updateAppNavigationState` to update the navigation
|
|
// state.
|
|
_navigate(action: Object): void {
|
|
if (action.type === 'exit') {
|
|
// Exits the example. `this.props.onExampleExit` is provided
|
|
// by the UI Explorer.
|
|
this.props.onExampleExit && this.props.onExampleExit();
|
|
return;
|
|
}
|
|
|
|
const state = updateAppNavigationState(
|
|
this.state,
|
|
action,
|
|
);
|
|
|
|
// `updateAppNavigationState` (which uses NavigationStateUtils) gives you
|
|
// back the same `state` if nothing has changed. You could use
|
|
// that to avoid redundant re-rendering.
|
|
if (this.state !== state) {
|
|
this.setState(state);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Next step.
|
|
// Define your own controlled navigator.
|
|
const YourNavigator = createAppNavigationContainer(class extends Component {
|
|
static propTypes = {
|
|
appNavigationState: PropTypes.shape({
|
|
apple: NavigationPropTypes.navigationState.isRequired,
|
|
banana: NavigationPropTypes.navigationState.isRequired,
|
|
orange: NavigationPropTypes.navigationState.isRequired,
|
|
tabs: NavigationPropTypes.navigationState.isRequired,
|
|
}),
|
|
navigate: PropTypes.func.isRequired,
|
|
};
|
|
|
|
// This sets up the methods (e.g. Pop, Push) for navigation.
|
|
constructor(props: any, context: any) {
|
|
super(props, context);
|
|
this._back = this._back.bind(this);
|
|
this._renderHeader = this._renderHeader.bind(this);
|
|
this._renderScene = this._renderScene.bind(this);
|
|
}
|
|
|
|
// Now use the `NavigationCardStack` to render the scenes.
|
|
render(): ReactElement {
|
|
const {appNavigationState} = this.props;
|
|
const {tabs} = appNavigationState;
|
|
const tabKey = tabs.routes[tabs.index].key;
|
|
const scenes = appNavigationState[tabKey];
|
|
|
|
return (
|
|
<View style={styles.navigator}>
|
|
<NavigationCardStack
|
|
key={'stack_' + tabKey}
|
|
onNavigateBack={this._back}
|
|
navigationState={scenes}
|
|
renderHeader={this._renderHeader}
|
|
renderScene={this._renderScene}
|
|
style={styles.navigatorCardStack}
|
|
/>
|
|
<YourTabs
|
|
navigationState={tabs}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
// Render the header.
|
|
// The detailed spec of `sceneProps` is defined at `NavigationTypeDefinition`
|
|
// as type `NavigationSceneRendererProps`.
|
|
_renderHeader(sceneProps: Object): ReactElement {
|
|
return (
|
|
<YourHeader
|
|
{...sceneProps}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// Render a scene for route.
|
|
// The detailed spec of `sceneProps` is defined at `NavigationTypeDefinition`
|
|
// as type `NavigationSceneRendererProps`.
|
|
_renderScene(sceneProps: Object): ReactElement {
|
|
return (
|
|
<YourScene
|
|
{...sceneProps}
|
|
/>
|
|
);
|
|
}
|
|
|
|
_back() {
|
|
this.props.navigate({type: 'pop'});
|
|
}
|
|
});
|
|
|
|
// Next step.
|
|
// Define your own header.
|
|
const YourHeader = createAppNavigationContainer(class extends Component {
|
|
static propTypes = {
|
|
...NavigationPropTypes.SceneRendererProps,
|
|
navigate: PropTypes.func.isRequired,
|
|
};
|
|
|
|
constructor(props: Object, context: any) {
|
|
super(props, context);
|
|
this._back = this._back.bind(this);
|
|
this._renderTitleComponent = this._renderTitleComponent.bind(this);
|
|
}
|
|
|
|
render(): ReactElement {
|
|
return (
|
|
<NavigationHeader
|
|
{...this.props}
|
|
renderTitleComponent={this._renderTitleComponent}
|
|
onNavigateBack={this._back}
|
|
/>
|
|
);
|
|
}
|
|
|
|
_back(): void {
|
|
this.props.navigate({type: 'pop'});
|
|
}
|
|
|
|
_renderTitleComponent(props: Object): ReactElement {
|
|
return (
|
|
<NavigationHeader.Title>
|
|
{props.scene.route.key}
|
|
</NavigationHeader.Title>
|
|
);
|
|
}
|
|
});
|
|
|
|
// Next step.
|
|
// Define your own scene.
|
|
const YourScene = createAppNavigationContainer(class extends Component {
|
|
static propTypes = {
|
|
...NavigationPropTypes.SceneRendererProps,
|
|
navigate: PropTypes.func.isRequired,
|
|
};
|
|
|
|
constructor(props: Object, context: any) {
|
|
super(props, context);
|
|
this._exit = this._exit.bind(this);
|
|
this._popRoute = this._popRoute.bind(this);
|
|
this._pushRoute = this._pushRoute.bind(this);
|
|
}
|
|
|
|
render(): ReactElement {
|
|
return (
|
|
<ScrollView>
|
|
<NavigationExampleRow
|
|
text="Push Route"
|
|
onPress={this._pushRoute}
|
|
/>
|
|
<NavigationExampleRow
|
|
text="Pop Route"
|
|
onPress={this._popRoute}
|
|
/>
|
|
<NavigationExampleRow
|
|
text="Exit Header + Scenes + Tabs Example"
|
|
onPress={this._exit}
|
|
/>
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
_pushRoute(): void {
|
|
// Just push a route with a new unique key.
|
|
const route = {key: '[' + this.props.scenes.length + ']-' + Date.now()};
|
|
this.props.navigate({type: 'push', route});
|
|
}
|
|
|
|
_popRoute(): void {
|
|
this.props.navigate({type: 'pop'});
|
|
}
|
|
|
|
_exit(): void {
|
|
this.props.navigate({type: 'exit'});
|
|
}
|
|
});
|
|
|
|
// Next step.
|
|
// Define your own tabs.
|
|
const YourTabs = createAppNavigationContainer(class extends Component {
|
|
static propTypes = {
|
|
navigationState: NavigationPropTypes.navigationState.isRequired,
|
|
navigate: PropTypes.func.isRequired,
|
|
};
|
|
|
|
constructor(props: Object, context: any) {
|
|
super(props, context);
|
|
}
|
|
|
|
render(): ReactElement {
|
|
return (
|
|
<View style={styles.tabs}>
|
|
{this.props.navigationState.routes.map(this._renderTab, this)}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
_renderTab(route: Object, index: number): ReactElement {
|
|
return (
|
|
<YourTab
|
|
key={route.key}
|
|
route={route}
|
|
selected={this.props.navigationState.index === index}
|
|
/>
|
|
);
|
|
}
|
|
});
|
|
|
|
// Next step.
|
|
// Define your own Tab
|
|
const YourTab = createAppNavigationContainer(class extends Component {
|
|
|
|
static propTypes = {
|
|
navigate: PropTypes.func.isRequired,
|
|
route: NavigationPropTypes.navigationRoute.isRequired,
|
|
selected: PropTypes.bool.isRequired,
|
|
};
|
|
|
|
constructor(props: Object, context: any) {
|
|
super(props, context);
|
|
this._onPress = this._onPress.bind(this);
|
|
}
|
|
|
|
render(): ReactElement {
|
|
const style = [styles.tabText];
|
|
if (this.props.selected) {
|
|
style.push(styles.tabSelected);
|
|
}
|
|
return (
|
|
<TouchableOpacity style={styles.tab} onPress={this._onPress}>
|
|
<Text style={style}>
|
|
{this.props.route.key}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
);
|
|
}
|
|
|
|
_onPress() {
|
|
this.props.navigate({type: 'selectTab', tabKey: this.props.route.key});
|
|
}
|
|
});
|
|
|
|
const styles = StyleSheet.create({
|
|
navigator: {
|
|
flex: 1,
|
|
},
|
|
navigatorCardStack: {
|
|
flex: 20,
|
|
},
|
|
tabs: {
|
|
flex: 1,
|
|
flexDirection: 'row',
|
|
},
|
|
tab: {
|
|
alignItems: 'center',
|
|
backgroundColor: '#fff',
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
},
|
|
tabText: {
|
|
color: '#222',
|
|
fontWeight: '500',
|
|
},
|
|
tabSelected: {
|
|
color: 'blue',
|
|
},
|
|
});
|
|
|
|
module.exports = YourApplication;
|