React-Native StackNavigator和TabNavigator路由嵌套设计
嵌套模型
Screen没有画全,但大概是这样的模型。
代码写在app.js中统一管理:
const LoginStack = createStackNavigator({
Login: Login,
Register: Register,
}, {
initialRouteName: 'Login',
});
const ShopStack = createStackNavigator({
Shop: Shop,
ShoppingLog: ShoppingLog
}, {
initialRouteName: 'Shop',
});
const AssetsStack = createStackNavigator({
Assets: Assets,
TransferInLog: TransferInLog,
TransferOutLog: TransferOutLog,
}, {
initialRouteName: 'Assets',
});
const MineStack = createStackNavigator({
Mine: Mine,
MyPhone: MyPhone,
PasswordSetting: PasswordSetting,
}, {
initialRouteName: 'Mine',
});
const MainTab = createBottomTabNavigator({
ShopStack: {
screen: ShopStack,
navigationOptions: {
tabBarLabel: '商城',
}
},
AssetsStack: {
screen: AssetsStack,
navigationOptions: {
tabBarLabel: '资产',
}
},
MineStack: {
screen: MineStack,
navigationOptions: {
tabBarLabel: '我的',
}
}
});
const AppNavigator = createStackNavigator({
Loading: {
screen: Loading
},
LoginStack: {
screen: LoginStack
},
MainTab: {
screen: MainTab
}
}, {
initialRouteName: 'Loading',
});
const AppContainer = createAppContainer(AppNavigator);
export default class App extends React.Component {
render() {
<AppContainer/>
}
}
react的路由其实是把我们写的页面(screen)放进StackNavigator
或是TabNavigator
(下面简称Stack
和Tab
)这种容器中,切换页面时告诉容器,然后容器显示对应的页面,跟传统的网页跳转不大一样,但是跟单页面应用有点相似。
createStackNavigator
和createBottomTabNavigator
两个函数的第一个参数都是声明包含的页面(screen),第二个参数为该容器的配置。
上面的代码只是实现了模型图中的路由,后面还有一大堆需要配置的东西。
头部栏的配置
上图所示的为路由中的头部栏。进入Stack
非首页后会出现头部栏并带有返回按钮。
隐藏头部栏
每个容器都拥有一个头部栏配置。而我们的容器又是嵌套的,参照上面的图,当我们打开ShopScreen
时,我们可能同时看见两个头部栏,分别是ShopStack
和AppStack
的头部栏(Tab
没有头部栏,所以不会显示MainTab
的头部栏,但是Tab
会有底部栏,在下面介绍),所以我们必须在一定场合隐藏一些头部栏。隐藏的方式是在配置中的defaultNavigationOptions
加入header: null
。这里以AppStack
为例,他作为最外层的容器,不需要任何头部栏。
const AppNavigator = createStackNavigator({
Loading: {
screen: Loading
},
LoginStack: {
screen: LoginStack
},
MainTab: {
screen: MainTab
}
}, {
initialRouteName: 'Loading',
defaultNavigationOptions: {
header: null
}
});
头部栏样式
事实上我们大多数时候都是需要头部栏,我们可以在对应的Stack的defaultNavigationOptions设置对应的样式,然后在对应的页面添加静态方法navigationOptions
,用于设置头部栏的标题和左右两边的按钮等。因为我这个项目多个Stack
的头部栏样式相同,所以我把样式配置提了出来。
但在配置时要记住当前页面是属于哪一个Stack
,使用的哪一个Stack
的头部栏。
app.js
const defaultNavigationOptions = () => ({
headerStyle: {
backgroundColor: '#1c2d34',
height: 48,
shadowOpacity: 0,
shadowOffset: {
height: 0,
},
shadowRadius: 0,
elevation: 0,
},
headerTintColor: '#00b17b',
headerTitleStyle: {
fontSize: 16,
textAlign: 'center',
color: '#6792a3',
fontWeight: 'normal',
flex: 1,
textAlign:'center'
},
headerRight: <View></View>
});
const MineStack = createStackNavigator({
Mine: Mine,
MyPhone: MyPhone,
PasswordSetting: PasswordSetting,
}, {
initialRouteName: 'Mine',
defaultNavigationOptions: defaultNavigationOptions//头部栏样式
});
mine.js
export default class Mine extends React.Component {
...
static navigationOptions = ({navigation}) => ({
headerTitle: '个人信息',
headerLeft: <View></View>,
headerRight: <Text style={{
marginRight: 12,
textAlign: 'right',
fontSize: 16,
color: '#e60012'
}}>登出</Text>
});
...
}
效果如下
标题居中
在上面的mine.js
中,我在配置中加了一个headerLeft: <View></View>
。因为头部栏是由三部分组成的,headerLeft
,headerTitle
,headerRight
。在之前我们也说过,在非首页的页面,头部栏的左边才会出现一个返回按钮,如果是在Stack
的首页,那么headerLeft
将不存在,导致headerTitle
向左偏移,无论如何都难以居中,所以我们在这里添加一个空的View
,这样就能正常居中了。
自定义左右按钮
在上面,我在headerRight
中写了一个登出的按钮,同样的,我也能在headerLeft
中写按钮。如果headerLeft
被编辑,那么当前页面默认的返回按钮将不会出现,会被编辑的内容替代。你可以自定义样式,自定义跳转等。
下面的例子中我点击关闭将直接回到登录界面。
...
static navigationOptions = ({navigation}) => ({
headerTitle: '激活',
headerStyle: {
backgroundColor: '#1C2D34',
height: 48,
shadowOpacity: 0,
shadowOffset: {
height: 0,
},
shadowRadius: 0,
elevation: 0,
},
headerLeft: <Icon name="times" size={24} color="#00b17b" style={{
marginLeft: 20
}} onPress={() => {
const { navigate } = navigation;
navigate('Login');
}}></Icon>
});
...
底部栏的配置
底部栏也是一个app应该必备的功能,通过点击底部栏图标快速切换对应的页面。
底部栏图标
最开始的配置中我们已经编辑了底部栏每个按钮的标题tabBarLabel
,当然光有文字是不够的,我们还需要图标,就如上图所示。
const MainTab = createBottomTabNavigator({
MiningStack: {
screen: MiningStack,
navigationOptions: {
tabBarLabel: '主页',
}
},
ShopStack: {
screen: ShopStack,
navigationOptions: {
tabBarLabel: '商城',
}
},
AssetsStack: {
screen: AssetsStack,
navigationOptions: {
tabBarLabel: '资产',
}
},
MineStack: {
screen: MineStack,
navigationOptions: {
tabBarLabel: '我的',
}
}
}, {
defaultNavigationOptions: ({ navigation }) => ({
tabBarIcon: ({ focused, horizontal, tintColor }) => {
const { routeName } = navigation.state;
let img;
let imgArr = {
"Mining": require('./static/image/mining.png'),
"Mining_hover": require('./static/image/mining_hover.png'),
"Shop": require('./static/image/shop.png'),
"Shop_hover": require('./static/image/shop_hover.png'),
"Assets": require('./static/image/assets.png'),
"Assets_hover": require('./static/image/assets_hover.png'),
"Mine": require('./static/image/mine.png'),
"Mine_hover": require('./static/image/mine_hover.png'),
};
if (routeName === 'MiningStack') {
img = focused ? 'Mining_hover' : 'Mining';
} else if (routeName === 'ShopStack') {
img = focused ? 'Shop_hover' : 'Shop';
} else if (routeName === 'AssetsStack') {
img = focused ? 'Assets_hover' : 'Assets';
} else if (routeName === 'MineStack') {
img = focused ? 'Mine_hover' : 'Mine';
}
return <Image style={{
width: 22,
height: 22,
}} source={imgArr[img]} />;
},
//隐藏底部栏
tabBarVisible: navigation.state.index > 0 ? false : true,
}),
//底部栏样式
tabBarOptions: {
activeTintColor: '#00b07a',
inactiveTintColor: '#6792a3',
style: {
backgroundColor: '#1c2d34',
height: 48,
paddingTop: 8,
paddingBottom: 4
},
},
});
这里,我们把四个按钮的图标统一在app.js
中处理了,当然也可以在对应页面的navigationOptions
里配置tabBarIcon
。
底部栏样式
上面代码也注释出来了,tabBarOptions
即为Tab
的样式配置。
隐藏底部栏
底部栏通常只在底部栏上对应的页面中显示,如果是在其他页面,例如PasswordSettingScreen
,如果不加任何配置,那么底部栏会一直存在。所以我们要让底部栏在不需要的时候不显示出来。
上面代码同样注释出来了。在defaultNavigationOptions
里配置tabBarVisible
。
tabBarVisible: navigation.state.index > 0 ? false : true,
为什么要这样设置呢?
还记得我们的MainTab
包含有多个Stack
,而MainTab
里显示的页面其实是Stack
里的页面,所以这里的navigation
其实是对应的Stack
的。navigation.state.index > 0
代表着当前Stack
显示的是非首页,就达到了我们想要的目的。
返回键的处理
以下内容仅针对android,因为ios没有返回键233
默认的情况下,在非Stack
首页按下返回键会自动回到上一页(等同头部栏的返回键),在Stack
首页按下返回键会退出app。在Tab
里,按下返回键会返回到第一个页面(index=0),因为我们的MainTab
嵌套在AppStack
里,所以在MainTab
的首页里再按下返回键就会跳转到LoadingScreen
。
当然上面的情况是我们不想要的,所以我们需要加以处理。另外我们在这里在这里再添加一个'再按一次退出应用'功能。
先上代码。
import {
ToastAndroid,
BackHandler,
} from 'react-native';
let isExit = true;
let lastBackPressed = null;
export default class App extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.onBackAndroid);
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.onBackAndroid);
lastBackPressed = null;
}
onBackAndroid() {
if (isExit) { // 根界面
if (lastBackPressed && lastBackPressed + 2000 >= Date.now()) {
BackHandler.exitApp();
return true;
}
lastBackPressed = Date.now();
ToastAndroid.show('再按一次退出应用', ToastAndroid.SHORT);
return true;
}
return false
}
render() {
return <AppContainer onNavigationStateChange={(prevNav, nav, action) => {
routes = nav.routes;
curretRoutes = routes[routes.length - 1];
if (curretRoutes.routeName == 'MainTab') {
if (curretRoutes.routes[curretRoutes.index].index == undefined || curretRoutes.routes[curretRoutes.index].index == 0) {
isExit = true;
} else {
isExit = false;
}
} else {
isExit = false;
}
if (curretRoutes.routeName == 'LoginStack') {
if (curretRoutes.index == 0){
isExit = true;
} else {
isExit = false;
}
}
}}/>
}
}
我们在componentDidMount
和componentWillUnmount
里分别添加和移除hardwareBackPress
的事件,即监听返回键,按下返回键时触发onBackAndroid()
。我们又在AppContainer
里修改onNavigationStateChange
方法,即路由发生改变时执行的方法。
这里要注意的是,当我们按下返回键时,路由即将改变,所以这里会先触发onNavigationStateChange
,然后再触发onBackAndroid
。所以我们用isExit
变量控制onBackAndroid
之后的操作。监听函数返回false
,则会执行默认行为,返回上一页或者退出应用,返回true
则不会执行这些行为。
LoginStack
很好理解,curretRoutes.index == 0
即在LoginStack
首页时执行'再按一次退出应用'的功能,不在首页执行默认功能。
对于MainTab
,curretRoutes.routes[curretRoutes.index]
代表着Tab
当前选中页,因为MainTab
里嵌套的是Stack
,所以这里就是对应Stack
。curretRoutes.routes[curretRoutes.index].index == 0
即对应Stack
在首页,curretRoutes.routes[curretRoutes.index].index == undefined
即选中的不是Stack
而是普通的Screen
。
在任意js文件里跳转路由
这个项目中,接口都是分离到api文件夹下的,单独的js文件,如果这时需要跳转路由当然也是可以做到的。
首先需要一个创建一个NavigationService.js
文件
// NavigationService.js
import { NavigationActions } from 'react-navigation';
let _navigator;
function setTopLevelNavigator(navigatorRef) {
_navigator = navigatorRef;
}
function navigate(routeName, params) {
_navigator.dispatch(
NavigationActions.navigate({
routeName,
params,
})
);
}
// add other navigation functions that you need and export them
export default {
navigate,
setTopLevelNavigator,
};
在app.js里引入NavigationService.js
,并在AppContainer
上加上ref参数
import NavigationService from './utils/NavigationService';
export default class App extends React.Component {
...
render() {
return <AppContainer onNavigationStateChange={(prevNav, nav, action) => {
...
}} ref={navigatorRef => {
NavigationService.setTopLevelNavigator(navigatorRef);
}}/>
}
...
}
在需要的页面引入NavigationService.js
,调用navigate
即可
import NavigationService from './NavigationService';
...
NavigationService.navigate('Login');
...
切换动画
跟样式一样,切换动画对于各个Stack
都通用,所以我也把其提了出来。
import {
Easing,
Animated
} from 'react-native';
const defaultTransitionConfig = () => ({
transitionSpec: {
duration: 500,
easing: Easing.out(Easing.poly(4)),
timing: Animated.timing,
},
screenInterpolator: sceneProps => {
const { layout, position, scene } = sceneProps;
const { index } = scene;
const height = layout.initHeight;
const translateY = position.interpolate({
inputRange: [index - 1, index, index + 1],
outputRange: [height, 0, 0],
});
const opacity = position.interpolate({
inputRange: [index - 1, index - 0.99, index],
outputRange: [0, 1, 1],
});
return { opacity, transform: [{ translateY }] };
},
});
const MineStack = createStackNavigator({
Mine: Mine,
MyPhone: MyPhone,
PasswordSetting: PasswordSetting,
}, {
initialRouteName: 'Mine',
defaultNavigationOptions: defaultNavigationOptions,
transitionConfig: defaultTransitionConfig,//切换动画配置
});
Or you can contact me by Email