在 React Navigation 5 中向 createMaterialBottomTabNavigator 添加自定义 'Add' 按钮

Adding a custom 'Add' button to createMaterialBottomTabNavigator in react navigation 5

我正在制作一个项目(react native、expo、react navigation 5),我想在其中向底部选项卡添加一个自定义 'add' 按钮,但是因为...

A navigator can only contain 'Screen' components as its direct children

...我需要找到一种方法来传递我的自定义组件。

看起来很简单,我的意思是有文档:

...但是在查看这些和其他人的问题时,我要么只找到了很多复杂的示例,要么找到了如何在早期版本中实现此目的的示例。

最后我找到了一个简单的解决方案,到目前为止效果很好(非常感谢任何关于为什么这可能是一个糟糕想法的建议)。

我想 post 我的解决方案,如果有人遇到类似的问题。请参阅下面的答案。

将组件放在导航器外部,并使用 css 将其放置在选项卡上方。如示例所示,将选项卡的图标调整为左右。

就像我上面说的,热烈欢迎关于如何以不同方式实现这一目标的建议,但还没有遇到任何问题(敲木头)。

这是它的样子:

这是兔子:

import React from 'react';
import { createMaterialBottomTabNavigator } from '@react-navigation/material-bottom-tabs';
import { Ionicons, MaterialIcons, FontAwesome } from '@expo/vector-icons';

import AddButton from '../../components/UI/AddButton';
import SpotlightProductsScreen from './SpotlightProductsScreen';
import ProductsScreen from './ProductsScreen';
import UserSpotlightScreen from './../user/UserSpotlightScreen';
import UserProductsScreen from './../user/UserProductsScreen';


const ProductsOverviewScreen = props => {

  const Tab = createMaterialBottomTabNavigator();

  return (
    <>
      <AddButton
        navigation={props.navigation}
        style={{
          position: 'absolute',
          zIndex: 99,
          bottom: 5,
          alignSelf: 'center',
          shadowColor: 'black',
          shadowOpacity: 0.15,
          shadowOffset: { width: 0, height: 2 },
          shadowRadius: 8,
          elevation: 3 //Because shadow only work on iOS, elevation is same thing but for android.
        }}
      />
      <Tab.Navigator
        initialRouteName="Spotlight"
        labeled={false}
        shifting={true}
        activeColor="#f0edf6"
        inactiveColor="#3e2465"
        barStyle={{ backgroundColor: 'rgba(127,63,191,.9)' }}
      >
        <Tab.Screen
          name="Spotlight"
          component={SpotlightProductsScreen}
          options={{
            tabBarIcon: ({ color }) => (
              <Ionicons
                name={
                  Platform.OS === 'android'
                    ? 'md-notifications'
                    : 'ios-notifications'
                }
                color={color}
                size={27}
                style={{
                  marginLeft: -35
                }}
              />
            )
          }}
        />
        <Tab.Screen
          name="Förråd"
          component={ProductsScreen}
          options={{
            tabBarIcon: ({ color }) => (
              <MaterialIcons
                name={'file-download'}
                color={color}
                size={27}
                style={{
                  marginLeft: -70
                }}
              />
            )
          }}
        />
        <Tab.Screen
          name="Mitt Förråd"
          component={UserProductsScreen}
          options={{
            tabBarIcon: ({ color }) => (
              <MaterialIcons
                name={'file-upload'}
                color={color}
                size={30}
                style={{
                  marginRight: -70
                }}
              />
            )
          }}
        />
        <Tab.Screen
          name="Min Sida"
          component={UserSpotlightScreen}
          options={{
            tabBarBadge: 4,
            tabBarIcon: ({ color }) => (
              <FontAwesome
                name={'user'}
                color={color}
                size={30}
                style={{
                  marginRight: -35
                }}
              />
            )
          }}
        />
      </Tab.Navigator>
    </>
  );
};

export default ProductsOverviewScreen;

我遇到了同样的问题,我需要向选项卡导航器添加一个与屏幕无关的自定义组件,但我尝试的一切都失败了。就我而言,我正在尝试使用 createMaterialTopTabNavigator。

React Navigation 5 的文档有点粗糙,没有很多 React Navigation 5 的示例,但经过多次尝试,我可以创建一个自定义组件并自己设置样式,这样我就可以混合由选项卡导航器中嵌入的路由和自定义按钮。

import * as React from 'react';
import { View } from 'react-native'
import {
  NavigationHelpersContext,
  useNavigationBuilder,
  TabRouter,
  TabActions,
  createNavigatorFactory,
} from '@react-navigation/native';
import styled from 'styled-components'
import Colors from '../constants/Colors';

const customTabNavigator = ({ 
  initialRouteName, 
  children, 
  screenOptions, 
  tabContainerStyle, 
  contentStyle, 
  leftIcon,
  rightIcon 
}) => {
  const { state, navigation, descriptors } = useNavigationBuilder(TabRouter, {
    children,
    screenOptions,
    initialRouteName,
  });

  return (
    <NavigationHelpersContext.Provider value={navigation}>
      <OuterWrapper style={tabContainerStyle}>
        { leftIcon }
        <TabWrapper>
          {state.routes.map((route, i) => {
            return (
              <Tab
                key={route.key}
                onPress={() => {
                  const event = navigation.emit({
                    type: 'tabPress',
                    target: route.key,
                    canPreventDefault: true,
                  });

                  if (!event.defaultPrevented) {
                    navigation.dispatch({
                      ...TabActions.jumpTo(route.name),
                      target: state.key,
                    });
                  }
                }}
                style={descriptors[route.key].options.tabStyle}
              >
                { descriptors[route.key].options.label ?? <Label active={state.index === i}>{descriptors[route.key].options.title || route.name}</Label> }
              </Tab>
            )
          })}
        </TabWrapper>
        { rightIcon }
      </OuterWrapper>
      <View style={[{ flex: 1 }, contentStyle]}>
        {descriptors[state.routes[state.index].key].render()}
      </View>
    </NavigationHelpersContext.Provider>
  );
}

const OuterWrapper = styled.View`
  height: 55px;
  flex-direction: row;
  justify-content: space-between;
  background-color: ${Colors.grey1};
`
const TabWrapper = styled.View`
  flex: 1;
  flex-direction: row;
  justify-content: space-evenly;
`
const Tab = styled.TouchableOpacity`
  padding: 0 24px;
  justify-content: center;
  height: 100%;
`
const Label = styled.Text`
  font-family: Futura-Medium;
  font-size: 26px;
  color: ${({ active }) => active ? Colors.grey6 : Colors.grey3};
`

export default createNavigatorFactory(customTabNavigator)
import customTabNavigator from './customTabNavigator'
import * as React from 'react';
import { View, Image } from 'react-native'

import {
  ProjectsScreen,
  RenderScreen,
  EventsScreen,
  CameraScreen
} from '../screens';

import Colors from '../constants/Colors'

import logo from '../assets/images/icon.png'
import { Ionicons } from '@expo/vector-icons';
import { TouchableOpacity } from 'react-native-gesture-handler';

const TopTab = customTabNavigator();
const INITIAL_ROUTE_NAME = 'Home';

export default function MainNavigator({ navigation, route }) {

  navigation.setOptions({ headerTitle: getHeaderTitle(route) });
  return (
      <TopTab.Navigator
        initialRouteName={INITIAL_ROUTE_NAME}
        leftIcon={(
          <TouchableOpacity style={{ height: "100%", justifyContent: "center" }} onPress={() => alert("Whatever")}>
            <Image source={logo} style={{ resizeMode: "center", width: 70, height: 40 }} />
          </TouchableOpacity>
        )}
      >
        <TopTab.Screen
          name="Home"
          component={ProjectsScreen}
          options={{
            title: 'Proyectos',
          }}
        />
        <TopTab.Screen
          name="Preview"
          component={EventsScreen}
          options={{
            title: 'Eventos',
          }}
        />
        <TopTab.Screen
          name="Render"
          component={RenderScreen}
          options={{
            title: 'Mi cuenta',
          }}
        />
        <TopTab.Screen
          name="Camera"
          component={CameraScreen}
          options={{
            title: "Camera",
            label: (
              <View style={{ width: 36, height: 32, backgroundColor: Colors.grey3, borderRadius: 3, alignItems: "center", justifyContent: "center" }}>
                <Ionicons name="md-camera" style={{ color: Colors.grey5 }} size={25} />
              </View>
            ),
            tabStyle: { flexDirection: "row", alignItems: "center", justifyContent: "flex-end", flex: 1  }
          }}
        />
      </TopTab.Navigator>
  );
}

function getHeaderTitle(route) {
  const routeName = route.state?.routes[route.state.index]?.name ?? INITIAL_ROUTE_NAME;

  switch (routeName) {
    case 'Home':
      return 'Montar vídeo';
    case 'Preview':
      return 'Previsualizar vídeo';
    case 'Render':
      return 'Renderizar';
    case 'Gallery':
      return 'Galería'
    case 'Camera':
      return 'Camera'
  }
}

对于 https://reactnavigation.org/docs/custom-navigators 的示例,我添加了不同的样式和两个新道具,leftIcon 和 rightIcon。该道具接收一个组件,用于将其渲染到选项卡包装器的相应侧。并且此组件可以是 TouchableWhatevers,带有与屏幕无关的自定义 onPress :P

我希望它能帮到你,我几乎把自己扔进了 window 直到我成功了,哈哈

你可以试试这个:

<Tab.Screen 
        name = "Button" 
        component={ScanStack} 
        options={{
        tabBarButton:()=>
        <View style={{position:'relative',bottom:35,alignItems:'center', justifyContent:'space-around',height:85}}>
          <Icon 
            name="barcode-scan"
            type = "material-community" 
            reverse
            color={'yellow'}
            reverseColor='black'
            containerStyle={{padding:0,margin:0,elevation:5}}
            onPress={()=>console.log('Hi')}
            size={30}/>
          <Text>Scan</Text>
        </View>
        }}/>

在组件中需要使用有效的反应组件,我尝试使用component={()=>null} 但控制台出现警告

Result

step-1:- 在 bottomTabNav.js 中给 component-prop 提供一个 returns 什么都没有的屏幕

import React from 'react'
const AddMoreScreen = () => {
  return  null
}
export default AddMoreScreen
//I created AddMoreScreen.js component 

第 2 步:- 如第 1 步中所述,我正在提供一个不向组件道具呈现任何内容的屏幕,稍后在选项道具中我将点击 tabBarButton 对象和 returns 一个自定义按钮

  ...
 <Tab.Screen
    name={"Add"}
    component={AddMoreScreen}
    options={{
      tabBarButton: ()=> <AddMoreModal />
    }}
  />
  ...

第 3 步:- 最后是我们的中心按钮代码,在我的例子中,如果我按下按钮,模式必须从底部出现,只需根据您的要求更改以下代码。 AddMoreModal.js

export default function AddMoreModal() {
const [modalVisible, setModalVisible] = useState(false);

return (
<View style={{ marginTop: 15, marginLeft: 10, marginRight: 10, }}>
  <TouchableOpacity
    onPress={() => {
      setModalVisible(true);
    }}
  >
    <View style={styles.buttonStyle}>
      <Image
        source={icons.quickAddOutlined}
        style={styles.bottomTabImage}
      />
      <Text style={styles.bottomTabText}>Add</Text>
    </View>
  </TouchableOpacity>

  <View style={styles.container}>
    <Modal
      backdropOpacity={0.3}
      isVisible={modalVisible}
      onBackdropPress={() => setModalVisible(false)}
      style={styles.contentView}
    >
      <View style={styles.content}>
        <Text style={styles.contentTitle}>Hi !</Text>
        <Text>Welcome to CRAZY MIDDLE BUTTON! forgotten by react-native navigation</Text>
      </View>
    </Modal>
  </View>
</View>
);
}


const styles = StyleSheet.create({
content: {
backgroundColor: "white",
padding: 22,
justifyContent: "center",
alignItems: "center",
borderTopRightRadius: 17,
borderTopLeftRadius: 17,
},
contentTitle: {
fontSize: 20,
marginBottom: 12,
},
contentView: {
justifyContent: "flex-end",
margin: 0,
},
buttonStyle: {
marginBottom: 0,
alignItems:'center',
justifyContent:'center'
},
bottomTabImage: {
width: 25,
height: 25,
},
bottomTabText: {
marginTop: 4,
fontSize: FONTS.body3.fontSize,
fontFamily: FONTS.body3.fontFamily,
color: COLORS.fontGray90,
},
});

第四步:-