React Navigation 5.x(二)嵌套路由动态配置标
接上篇React Navigation 5.x(一)常用知识点梳理
我正在开发的App,路由结构是很典型的根堆栈导航器的首屏嵌套一个tab导航器,其余不显示选项卡的Screen都放在这屏的后面。
现在我有这两个需求:1、在嵌套路由中动态配置顶部标题栏(tab导航器嵌套在根部stack导航器的首屏);2、监听tab点击事件,触发时将对应screen重置到初始状态(数据)。
我的基础路由结构如下:
1.我把路由单独拎出来放在/src/router/index.js里,再引入App.js
// App.js
import 'react-native-gesture-handler';
import React from 'react';
import Routes from '@router'; // 路径为插件定义过的别名,实际是"/src/router/index.js"
const App = () => {
return <Routes/>;
}
export default App;
2./src/router/index.js内容:
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import Tabbar from '@components/core/Tabbar'
import GuideDetail from '@components/detail/Guide'
import MuseumDetail from '@components/detail/Museum'
const RootStack = createStackNavigator();
const Routes = () => {
return (
<NavigationContainer>
<RootStack.Navigator >
<RootStack.Screen name="Home" component={Tabbar} key="Home" />
<RootStack.Screen name="GuideDetail" component={GuideDetail} key="GuideDetail" />
<RootStack.Screen name="MuseumDetail" component={MuseumDetail} key="MuseumDetail" />
</RootStack.Navigator>
</NavigationContainer>
)
}
export default Routes
3.为了代码可读性和后期维护的便利,我把tab导航器也放到另一个js里而不是直接写在index.js
里。这已经是我加完所有配置的tabNavigator组件,不是最基础的结构。关于tabNavigator样式和其他参数配置,不清楚可以参考bottom-tab-navigator
/src/components/core/Tabbar.js
import React from 'react';
import { Icon } from '@ant-design/react-native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { StyleSheet } from 'react-native';
import Guide from '@components/home/Guide'
import Museum from '@components/home/Museum'
import Community from '@components/home/Community'
import Archive from '@components/home/Archive'
import User from '@components/home/SearchGuide'
const styles = StyleSheet.create({
tabBarStyle: {
position: 'absolute'
},
tabStyle: {
paddingTop: 5,
paddingBottom:5
},
labelStyle: {
fontSize: 12
},
})
const Tab = createBottomTabNavigator();
export default function MyTabs() {
return (
<Tab.Navigator
initialRouteName="Museum"
tabBarOptions={{ // 定义标签栏的样式
activeTintColor: '#81C784',
inactiveTintColor: '#949494',
keyboardHidesTabBar: true, // 在键盘弹出的时候将标签栏隐藏
tabStyle: styles.tabStyle,
labelStyle: styles.labelStyle,
styles: styles.tabBar,
}}
screenOptions={({ route }) => ({
tabBarIcon: ({ color, size }) => { // 自定义tabbar的图标
const icons = {
Guide: 'book',
Museum: 'bank',
Community: 'comment',
Archive: 'picture',
User: 'user'
};
return (
<Icon name={icons[route.name]} color={color} size={size} />
);
},
})}
>
<Tab.Screen
name="Guide"
component={Guide}
options={{
tabBarLabel: '攻略',
}}
/>
<Tab.Screen
name="Museum"
component={Museum}
initialParams={{ title: '博物馆图鉴' }} // 传入初始参数,screen组件获取后设置标题用
options={{
tabBarLabel: '博物馆', // 标签名
}}
/>
<Tab.Screen
name="Community"
component={Community}
initialParams={{ title: '社区' }}
options={{
tabBarLabel: '社区',
tabBarBadge: 3,
}}
/>
<Tab.Screen
name="Archive"
component={Archive}
initialParams={{ title: '其他图鉴' }}
options={{
tabBarLabel: '图鉴',
}}
/>
<Tab.Screen
name="User"
component={User}
initialParams={{ title: '我的' }}
options={{
tabBarLabel: '我的',
}}
/>
</Tab.Navigator>
);
}
嵌套路由之路由分组
上一篇讲过官方建议路由嵌套尽量不要超过两层,但这样难免会导致放在同一级的screen组件过多,层次条理不够清晰。于是我们可以这样做:官方最佳方案
即是把路由组件按功能分组,再通过map
添加到Navigator里。
const homeScreen = {
Home: Tabbar,
}
const detailScreens = { // 详情页组
GuideDetail,
MuseumDetail
}
const Routes = () => {
return (
<NavigationContainer theme={MyTheme}>
<RootStack.Navigator
initialRouteName="Home"
// 动态配置屏幕选项,跟下面的setHeaderTitle方法联在一起看
screenOptions={({ route }) => (setHeaderTitle(route))}
headerMode="screen" // 如果要自定义配置header,必须设置这个为screen
>
{Object.entries({
// 首页组
...homeScreen,
// 详情页组
...detailScreens}).map(([name, component]) => (
<RootStack.Screen name={name} component={component} key={name} />
))}
</RootStack.Navigator>
</NavigationContainer>
)
}
添加动态配置顶部标题栏功能
首先看下这是我用来动态设置屏幕选项(screenOptions)的方法:
import { NavigationContainer, getFocusedRouteNameFromRoute } from '@react-navigation/native';
const setHeaderTitle = (route) => {
let title = '动森之家' // 默认标题
// getFocusedRouteNameFromRoute可以获取路由的name
const routeName = getFocusedRouteNameFromRoute(route) ?? 'Guide';
if(routeName){ // 再根据路由名来设置title
switch(routeName) {
case 'Museum':
title = '博物馆图鉴'
break
case 'Community':
title = '社区'
break
case 'Archive':
title = '图鉴'
break
case 'User':
title = '我的'
break
}
}
if(route.params && route.params.title) { // 如果路由参数里有title属性,则该页标题以此为准
title = route.params.title
}
const screenOptions={
headerTitleAlign: 'center',
headerTintColor: 'white',
headerStyle: { backgroundColor: '#81C784', height: 60 },
// header: myHeader, // (1)返回React元素的函数,内容是自定义标题栏组件
// headerTitle: (props) => <MyTitle props={{...props, title}} />, // (2)返回React元素的函数,内容是自定义的标题文字组件
headerTitle: title, // (3)直接设置字符串,最简洁的方法
}
return screenOptions
}
看到上面screenOptions配置中,我们主要通过headerTitle来实现我们的需求,有三种方法:
(1) 自定义标题栏
这是官方的例子,一般对标题栏样式和精细度要求比较高才会用到,我这边没有实际使用。注意:如果要使用这个配置必须要在Navigator
设置headerMode:'screen'
header: ({ scene, previous, navigation }) => {
const { options } = scene.descriptor;
const title =
options.headerTitle !== undefined
? options.headerTitle
: options.title !== undefined
? options.title
: scene.route.name;
return (
<MyHeader
title={title}
leftButton={
previous ? <MyBackButton onPress={navigation.goBack} /> : undefined
}
style={options.headerStyle}
/>
);
};
(2) 自定义标题文字组件,替代原来的中间文字组件。当需求不仅仅是更改标题的文本和样式-比如:渲染图像来代替标题,或将标题变成按钮。就可以这样子来使用。
function LogoTitle(props) { // 如果需要渲染图像作为标题
return (
<Image
style={{ width: 50, height: 50 }}
source={require('@expo/snack-static/react-native-logo.png')}
/>
);
}
function MyTitle(props) { // 自定义标题文字组件,替代原来的中间文字组件,这里的写法效果和设置headerTitle: title是一样一样的。
return (
<Text>{props.props.title}</Text>
);
}
// 配置中使用
<RootStack.Navigator
screenOptions={{ headerTitle: props => <LogoTitle {...props} /> }}
</RootStack.Navigator>
(3) 没有什么额外要求的话,直接设置headerTitle为方法得到的title字符串即可。headerTitle: title
。
自定义选项卡导航器(tab Navigator)的tabPress
事件:
https://reactnavigation.org/docs/bottom-tab-navigator/#tabpress
当tabbar(选项卡栏)中当前Screen的tab按钮被按下时时触发此事件。这个事件除了让对应的tab按钮获得焦点,默认还会做这几件事:
如果这个tab对应的screen渲染的是一个滚动视图,您可以使用useScrollToTop
Hook使其滚动到顶部;
如果tab对应的屏幕是stack navigator,则在堆栈上执行popToTop
操作,即导航到该navigator的初始路由屏幕。
如果我们在点击tab按钮时有做些其他事情的需求,可以阻止这些默认事件。如下:
// tabbar的其中一个screen组件
const listRef = useRef(null)
const GuideScreen = ({ navigation, route }) => {
useEffect(() => {
const unsubscribe = navigation.addListener('tabPress', (e) => {
// 阻止默认行为
e.preventDefault();
// 手动跳转到这个GuideScreen页面
navigation.jumpTo('GuideScreen',{msg:'我是攻略页'})
getBanner() // 再重新获取下焦点图数据
listRef.current.onRefresh() // 让另一个子组件list的数据刷新
// ...
});
return unsubscribe;
}, [navigation]);
return (
<>
<Banner />
<View>你好呀,{route.params.msg}</View>
<GuideList ref={listRef}/>
</>
)
}
这边省略了其他无关的代码,比如请求方法、子组件啊,这边主要讲路由就不赘述,下次有空再展开写篇FlatList列表排序筛选功能结合Hook使用的文。