react-native

React Navigation 5.x(二)嵌套路由动态配置标

2021-01-11  本文已影响0人  AizawaSayo

接上篇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使用的文。

上一篇 下一篇

猜你喜欢

热点阅读