我似乎无法在任何地方使用 React-Native 找到自定义汉堡菜单下拉菜单。我不想在我的应用程序中使用 Stack.Drawer,我只想自己控制菜单,但我希望菜单模式出现在上一个路线上,而不是上一个路线标题上。
import React from 'react';
import { Pressable } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import type { StackNavigationProp } from '@react-navigation/stack';
import { MaterialIcons as Icon } from '@expo/vector-icons';
import { navScreens } from '@app/utils';
import type { StackParamsList } from '@app/types';
interface Props {
isOpen: boolean;
}
export default function Hamburger({ isOpen }: Props): JSX.Element {
const navigation = useNavigation<StackNavigationProp<StackParamsList>>();
return isOpen ? (
<Pressable onPress={() => navigation.goBack()}>
<Icon name="close" size={24} color="#ffffff" />
</Pressable>
) : (
<Pressable onPress={() => navigation.navigate(navScreens.hamburgerMenu.route)}>
<Icon name="menu" size={24} color="#ffffff" />
</Pressable>
);
}
import React from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import type { StackNavigationProp } from '@react-navigation/stack';
import { StackParamsList } from '@app/types';
import ScreenLayout from './ScreenLayout';
import { navScreens } from '@app/utils';
export default function HamburgerMenu(): JSX.Element {
const navigation = useNavigation<StackNavigationProp<StackParamsList>>();
return (
<ScreenLayout styles={{ flex: 1 }}>
<Pressable style={styles.background} onPress={() => navigation.goBack()}></Pressable>
<View style={styles.selectionContainer}>
<Pressable style={styles.selection} onPress={() => navigation.navigate(navScreens.white.route)}>
<Text style={styles.selectionFont}>White Screen</Text>
</Pressable>
<Pressable style={styles.selection} onPress={() => navigation.navigate(navScreens.blue.route)}>
<Text style={styles.selectionFont}>Blue Screen</Text>
</Pressable>
<Pressable style={styles.selection} onPress={() => navigation.navigate(navScreens.pink.route)}>
<Text style={styles.selectionFont}>Pink Screen</Text>
</Pressable>
<Pressable style={styles.selection} onPress={() => navigation.navigate(navScreens.red.route)}>
<Text style={styles.selectionFont}>Red Screen</Text>
</Pressable>
</View>
</ScreenLayout>
);
}
const styles = StyleSheet.create({
background: {
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
zIndex: 5,
paddingTop: 20,
paddingBottom: 100,
paddingHorizontal: 20,
backgroundColor: 'rgba(22, 27, 34, 0.6)'
},
selection: {
width: '100%',
padding: 10,
borderBottomColor: '#0ff',
borderBottomWidth: 2
},
selectionContainer: {
position: 'absolute',
top: 0,
zIndex: 10,
width: '100%',
borderTopColor: '#0ff',
borderTopWidth: 2,
backgroundColor: '#030E13'
},
selectionFont: {
fontSize: 20,
color: '#ffffff'
}
});
export { default as Hamburger } from './Hamburger';
export { default as HamburgerMenu } from './HamburgerMenu';
export { default as ScreenLayout } from './ScreenLayout';
import React, { ReactNode } from 'react';
import { View } from 'react-native';
import { StatusBar } from 'expo-status-bar';
interface Props {
styles: any | null;
children: ReactNode;
}
export default function ScreenLayout({ styles, children }: Props): JSX.Element {
return (
<View style={styles}>
{children}
<StatusBar style="auto" />
</View>
);
}
ScreenLayout.defaultProps = {
styles: null,
children: null
};
export { default as NavigationConductor } from './NavigationConductor';
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { Hamburger, HamburgerMenu } from '@app/components';
import { Blue, Pink, Red, White } from '@app/screens';
import { navScreens } from '@app/utils';
import type { StackParamsList } from '@app/types';
const Stack = createStackNavigator<StackParamsList>();
export default function NavigationConductor() {
return (
<NavigationContainer>
<Stack.Navigator
initialRouteName={navScreens.white.route}
screenOptions={({ route }) => {
return {
headerTitleAlign: 'center',
headerStyle: { backgroundColor: '#043B2B' },
headerRightContainerStyle: { paddingRight: 20 },
headerLeftContainerStyle: { paddingLeft: 20 },
headerTitleStyle: { color: '#ffffff' },
headerTintColor: '#ffffff',
headerLeft: () => <Hamburger isOpen={route.name === navScreens.hamburgerMenu.route} />
};
}}
>
<Stack.Group>
<Stack.Screen name={navScreens.white.route} component={White} options={{ title: navScreens.white.title }} />
<Stack.Screen name={navScreens.blue.route} component={Blue} options={{ title: navScreens.blue.title }} />
<Stack.Screen name={navScreens.pink.route} component={Pink} options={{ title: navScreens.pink.title }} />
<Stack.Screen name={navScreens.red.route} component={Red} options={{ title: navScreens.red.title }} />
</Stack.Group>
<Stack.Group>
<Stack.Screen
name={navScreens.hamburgerMenu.route}
component={HamburgerMenu}
options={{ headerShown: false, presentation: 'transparentModal' }}
/>
</Stack.Group>
</Stack.Navigator>
</NavigationContainer>
);
}
import React from 'react';
import { StyleSheet, Text } from 'react-native';
import { ScreenLayout } from '@app/components';
export default function Blue(): JSX.Element {
return (
<ScreenLayout styles={styles.container}>
<Text style={{ fontSize: 26, color: '#ffffff' }}>Blue Screen</Text>
</ScreenLayout>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'blue'
}
});
export { default as Blue } from './Blue';
export { default as Pink } from './Pink';
export { default as Red } from './Red';
export { default as White } from './White';
import React from 'react';
import { StyleSheet, Text } from 'react-native';
import { ScreenLayout } from '@app/components';
export default function Pink(): JSX.Element {
return (
<ScreenLayout styles={styles.container}>
<Text style={{ fontSize: 26 }}>Pink Screen</Text>
</ScreenLayout>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'pink'
}
});
import React from 'react';
import { StyleSheet, Text } from 'react-native';
import { ScreenLayout } from '@app/components';
export default function Red(): JSX.Element {
return (
<ScreenLayout styles={styles.container}>
<Text style={{ fontSize: 26, color: '#ffffff' }}>Red Screen</Text>
</ScreenLayout>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'red'
}
});
import React from 'react';
import { StyleSheet, Text } from 'react-native';
import { ScreenLayout } from '@app/components';
export default function White(): JSX.Element {
return (
<ScreenLayout styles={styles.container}>
<Text style={{ fontSize: 26 }}>White Screen</Text>
</ScreenLayout>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}
});
export type { NavigationStack, StackParamsList } from './Navigation';
import type {
DefaultNavigatorOptions,
ParamListBase,
StackNavigationState,
StackRouterOptions,
TypedNavigator
} from '@react-navigation/native';
import type { StackNavigationEventMap, StackNavigationOptions } from '@react-navigation/stack';
import type { StackNavigationConfig } from '@react-navigation/stack/lib/typescript/src/types';
export type StackParamsList = { [key: string]: undefined };
type NavigationStackProps = DefaultNavigatorOptions<
ParamListBase,
StackNavigationState<ParamListBase>,
StackNavigationOptions,
StackNavigationEventMap
> &
StackRouterOptions &
StackNavigationConfig;
export type NavigationStack = TypedNavigator<
StackParamsList,
StackNavigationState<ParamListBase>,
StackNavigationOptions,
StackNavigationEventMap,
({ id, initialRouteName, children, screenListeners, screenOptions, ...rest }: NavigationStackProps) => JSX.Element
>;
export { navScreens } from './Navigation';
type NavScreen = { route: string; title: string };
type NavScreens = {
blue: NavScreen;
hamburgerMenu: NavScreen;
pink: NavScreen;
red: NavScreen;
white: NavScreen;
};
export const navScreens: NavScreens = {
blue: { route: 'blue', title: 'Blue Screen' },
hamburgerMenu: { route: 'hamburger-menu', title: '' },
pink: { route: 'pink', title: 'Pink Screen' },
red: { route: 'red', title: 'Red Screen' },
white: { route: 'white', title: 'White Screen' }
};
# Ignore linting on libraries
node_modules
# Don't run lint on dist directory
dist/
# Don't run lint under any test sub directories
test/test
test/coverage
# solve linting errors
babel.config.json
babel.config.js
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"prettier",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript"
],
"globals": {
"fetch": false
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"allowImportExportEverywhere": true,
"ecmaFeatures": { "jsx": true },
"ecmaVersion": "latest",
"sourceType": "module",
"project": ["./tsconfig.json"]
},
"plugins": ["@typescript-eslint", "import", "prettier", "react", "react-hooks"],
"rules": {
"@typescript-eslint/ban-types": [
"error",
{
"extendDefaults": true,
"types": { "{}": false }
}
],
"@typescript-eslint/explicit-module-boundary-types": "warn",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/no-non-null-assertion": "off",
"class-methods-use-this": "off",
"comma-dangle": "off",
"indent": "off",
"indent-legacy": 0,
"import/no-unresolved": 0,
"import/named": 0,
"import/namespace": 0,
"import/default": 0,
"import/no-named-as-default-member": 0,
"no-param-reassign": [2, { "props": false }],
"no-tabs": ["off", { "allowIndentationTabs": true }],
"no-use-before-define": "warn",
"no-unused-vars": "warn",
"quotes": ["error", "single", { "avoidEscape": true }],
"react-hooks/rules-of-hooks": "off",
"react-hooks/exhaustive-deps": "warn",
"react/jsx-filename-extension": "off",
"react/jsx-uses-react": "off",
"react/jsx-uses-vars": "error",
"react/prop-types": "off",
"react/react-in-jsx-scope": "off",
"react/require-default-props": "off",
"sort-imports": [
"error",
{
"ignoreCase": false,
"ignoreDeclarationSort": true,
"ignoreMemberSort": false,
"memberSyntaxSortOrder": ["none", "all", "multiple", "single"]
}
]
},
"settings": {
"react": {
"createClass": "createReactClass",
"pragma": "React",
"fragment": "Fragment",
"version": "detect"
}
}
}
{
"arrowParens": "avoid",
"bracketSpacing": true,
"endOfLine": "auto",
"eslintIntegration": true,
"printWidth": 120,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "none"
}
{
"expo": {
"name": "react-native-hamburger",
"slug": "react-native-hamburger",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.jamespageced.reactnativehamburger",
"versionCode": 1
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}
import React from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { NavigationConductor } from '@app/navigation';
export default function App() {
// render
return (
<SafeAreaProvider>
<NavigationConductor />
</SafeAreaProvider>
);
}
module.exports = function (api) {
api.cache(true);
return {
presets: ['module:metro-react-native-babel-preset', 'babel-preset-expo'],
plugins: [
[
require.resolve('babel-plugin-module-resolver'),
{
extensions: ['.js', '.jsx', '.ts', '.tsx', '.android.js', '.android.tsx', '.ios.js', '.ios.tsx'],
alias: {
'@': './',
'@app': './app'
}
}
]
]
};
};
{
"name": "react-native-hamburger",
"version": "1.0.0",
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start --port 8082",
"android": "expo run:android --port 8082",
"ios": "expo run:ios",
"web": "expo start --web"
},
"dependencies": {
"@react-navigation/native": "^6.1.9",
"@react-navigation/stack": "^6.3.20",
"expo": "~49.0.15",
"expo-status-bar": "~1.6.0",
"react": "18.2.0",
"react-native": "0.72.6",
"react-native-gesture-handler": "~2.12.0",
"react-native-safe-area-context": "4.6.3",
"react-native-screens": "~3.22.0",
"expo-splash-screen": "~0.20.5"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@types/react": "~18.2.14",
"typescript": "^5.1.3"
},
"private": true
}
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true,
"baseUrl": "./",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"jsx": "react",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "esnext",
"moduleResolution": "node",
"noEmit": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": false,
"paths": {
"@/*": ["/*/index", "/*"],
"@app/*": ["app/*/index", "app/*"]
},
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "es5",
"useUnknownInCatchVariables": true
},
"include": ["app", "App.tsx", "test", "**/*.[jt]s?(x)"],
"exclude": ["node_modules", ".expo", "yarn.lock"]
}
问题#1:正如我们所看到的,问题是实际结果显示汉堡菜单覆盖了标题。
问题#2:一个额外的错误...从白屏以外的任何其他屏幕拉出汉堡菜单时,它会首先默认并转到白屏,然后它会拉出菜单以覆盖白色屏幕。
要解决此问题,您需要将
headerShown
设置为 false
:
<Stack.Screen name={navScreens.white.route} component={White} options={{ title: navScreens.white.title }} />
<Stack.Screen name={navScreens.blue.route} component={Blue} options={{ title: navScreens.blue.title , headerShown:false}} />
<Stack.Screen name={navScreens.pink.route} component={Pink} options={{ title: navScreens.pink.title, headerShown:false }} />
<Stack.Screen name={navScreens.red.route} component={Red} options={{ title: navScreens.red.title , headerShown:false}} />
这解决了菜单覆盖标题的第一个问题。
我们可以通过访问
navigate
来显示模态并复制标题,同时保持相同的规则并显示先前的路线名称。将 title
移入 screenOptions
,然后从 title
的中移除
<Stack.Screen .../>
export default function NavigationConductor() {
return (
<NavigationContainer>
<Stack.Navigator
initialRouteName={navScreens.white.route}
screenOptions={({ route, navigation }) => {
const routes = navigation.getState().routes;
const routeName = routes.length < 2 ? route.name : routes[routes.length - 2].name;
console.log('routeName', routeName);
return {
headerTitleAlign: 'center',
headerStyle: { backgroundColor: '#043B2B' },
headerRightContainerStyle: { paddingRight: 20 },
headerLeftContainerStyle: { paddingLeft: 20 },
headerTitleStyle: { color: '#ffffff' },
headerTintColor: '#ffffff',
title: routeName,
headerLeft: () => <Hamburger isOpen={route.name === navScreens.hamburgerMenu.route} />
};
}}
>
<Stack.Group>
<Stack.Screen name={navScreens.white.route} component={White} />
<Stack.Screen name={navScreens.blue.route} component={Blue} />
<Stack.Screen name={navScreens.pink.route} component={Pink} />
<Stack.Screen name={navScreens.red.route} component={Red} />
<Stack.Screen
name={navScreens.hamburgerMenu.route}
component={HamburgerMenu}
options={{ detachPreviousScreen: false, presentation: 'transparentModal' }}
/>
</Stack.Group>
</Stack.Navigator>
</NavigationContainer>
);
}
但是,当您在白屏以外的任何屏幕上时,问题#2仍然存在,它会先导航回白屏,然后再打开汉堡菜单。