文章示例源码: github.com/youngjuning…
安装依赖
$ yarn add @react-navigation/native @react-navigation/stack @react-navigation/bottom-tabs react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view
配置
为了完成 react-native-screens
的安装,添加下面两行代码到 android/app/build.gradle
文件的 dependencies
部分中:
implementation 'androidx.appcompat:appcompat:1.1.0-rc01'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha02'
为了完成 react-native-gesture-handler
的安装, 在入口文件的顶部添加下面的代码, 比如 index.js
或 App.js
:
import 'react-native-gesture-handler';
现在,我们需要把整个 App用 NavigationContainer
包裹:
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
const App = () => {
return (
<NavigationContainer>
{/* Rest of your app code */}
</NavigationContainer>
);
};
export default App;
App.js
import React from 'react';
import {
View,
Text,
StyleSheet,
SafeAreaView,
StatusBar,
BackHandler,
} from 'react-native';
import {NavigationContainer, useFocusEffect} from '@react-navigation/native';
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
import {createStackNavigator, HeaderBackButton} from '@react-navigation/stack';
import {IconOutline} from '@ant-design/icons-react-native';
import {Button} from '@ant-design/react-native';
import IconWithBadge from './IconWithBadge';
import HeaderButtons from './HeaderButtons';
import getActiveRouteName from './getActiveRouteName';
import getScreenOptions from './getScreenOptions';
import {navigationRef} from './NavigationService';
const HomeScreen = ({navigation, route}) => {
navigation.setOptions({
headerLeft: props => (
<HeaderBackButton
{...props}
onPress={() => {
console.log('不能再返回了!');
}}
/>
),
headerRight: () => (
<HeaderButtons>
{/* title、iconName、onPress、IconComponent、iconSize、color */}
<HeaderButtons.Item
title="添加"
iconName="plus"
onPress={() => console.log('点击了添加按钮')}
iconSize={24}
color="#ffffff"
/>
</HeaderButtons>
),
});
useFocusEffect(
React.useCallback(() => {
// Do something when the screen is focused
return () => {
// Do something when the screen is unfocused
// Useful for cleanup functions
};
}, []),
);
const {author} = route.params || {};
return (
<>
<StatusBar barStyle="dark-content" />
<View style={styles.container}>
<Text>Home Screen</Text>
<Text>{author}</Text>
<Button
type="warning"
// 使用 setOptions 更新标题
onPress={() => navigation.setOptions({headerTitle: 'Updated!'})}>
Update the title
</Button>
<Button
type="primary"
onPress={() =>
// 跳转到指定页面,并传递两个参数
navigation.navigate('DetailsScreen', {
otherParam: 'anything you want here',
})
}>
Go to DetailsScreen
</Button>
<Button
type="warning"
onPress={() => navigation.navigate('SafeAreaViewScreen')}>
Go SafeAreaViewScreen
</Button>
<Button
type="primary"
onPress={() =>
navigation.navigate('CustomAndroidBackButtonBehaviorScreen')
}>
Go CustomAndroidBackButtonBehavior
</Button>
</View>
</>
);
};
const DetailsScreen = ({navigation, route}) => {
// 通过 props.route.params 接收参数
const {itemId, otherParam} = route.params;
return (
<View style={styles.container}>
<Text>Details Screen</Text>
<Text>itemId: {itemId}</Text>
<Text>otherParam: {otherParam}</Text>
<Button
type="primary"
// 返回上一页
onPress={() => navigation.goBack()}>
Go back
</Button>
<Button
type="primary"
// 如果返回上一个页面需要传递参数,请使用 navigate 方法
onPress={() => navigation.navigate('HomeScreen', {author: '杨俊宁'})}>
Go back with Params
</Button>
</View>
);
};
const SettingsScreen = ({navigation, route}) => {
return (
<SafeAreaView
style={{flex: 1, justifyContent: 'space-between', alignItems: 'center'}}>
<Text>This is top text.</Text>
<Text>This is bottom text.</Text>
</SafeAreaView>
);
};
const SafeAreaViewScreen = () => {
return (
<SafeAreaView
style={{flex: 1, justifyContent: 'space-between', alignItems: 'center'}}>
<Text>This is top text.</Text>
<Text>This is bottom text.</Text>
</SafeAreaView>
);
};
const CustomAndroidBackButtonBehaviorScreen = ({navigation, route}) => {
useFocusEffect(
React.useCallback(() => {
const onBackPress = () => {
alert('物理返回键被拦截了!');
return true;
};
BackHandler.addEventListener('hardwareBackPress', onBackPress);
return () =>
BackHandler.removeEventListener('hardwareBackPress', onBackPress);
}, []),
);
return (
<View style={styles.container}>
<Text>AndroidBackHandlerScreen</Text>
</View>
);
};
const Stack = createStackNavigator();
const BottomTab = createBottomTabNavigator();
const BottomTabScreen = () => (
<BottomTab.Navigator
screenOptions={({route}) => ({
tabBarIcon: ({focused, color, size}) => {
let iconName;
if (route.name === 'HomeScreen') {
iconName = focused ? 'apple' : 'apple';
return (
<IconWithBadge badgeCount={90}>
<IconOutline name={iconName} size={size} color={color} />
</IconWithBadge>
);
} else if (route.name === 'SettingsScreen') {
iconName = focused ? 'twitter' : 'twitter';
}
return <IconOutline name={iconName} size={size} color={color} />;
},
})}
tabBarOptions={{
activeTintColor: 'tomato',
inactiveTintColor: 'gray',
}}>
<Stack.Screen
name="HomeScreen"
component={HomeScreen}
options={{tabBarLabel: '首页'}}
/>
<Stack.Screen
name="SettingsScreen"
component={SettingsScreen}
options={{tabBarLabel: '设置'}}
/>
</BottomTab.Navigator>
);
const App = () => {
const routeNameRef = React.useRef();
return (
<>
<NavigationContainer
ref={navigationRef}
onStateChange={state => {
const previousRouteName = routeNameRef.current;
const currentRouteName = getActiveRouteName(state);
if (previousRouteName !== currentRouteName) {
console.log('[onStateChange]', currentRouteName);
if (currentRouteName === 'HomeScreen') {
StatusBar.setBarStyle('dark-content'); // 修改 StatusBar
} else {
StatusBar.setBarStyle('dark-content'); // 修改 StatusBar
}
}
// Save the current route name for later comparision
routeNameRef.current = currentRouteName;
}}>
<Stack.Navigator
initialRouteName="HomeScreen"
// 页面共享的配置
screenOptions={getScreenOptions()}>
<Stack.Screen
name="BottomTabScreen"
component={BottomTabScreen}
options={{headerShown: false}}
/>
<Stack.Screen
name="DetailsScreen"
component={DetailsScreen}
options={{headerTitle: '详情'}} // headerTitle 用来设置标题栏
initialParams={{itemId: 42}} // 默认参数
/>
<Stack.Screen
name="SafeAreaViewScreen"
component={SafeAreaViewScreen}
options={{headerTitle: 'SafeAreaView'}}
/>
<Stack.Screen
name="CustomAndroidBackButtonBehaviorScreen"
component={CustomAndroidBackButtonBehaviorScreen}
options={{headerTitle: '拦截安卓物理返回键'}}
/>
</Stack.Navigator>
</NavigationContainer>
</>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});
export default App;
路由名称的大小写无关紧要 -- 你可以使用小写字母
home
或大写字母Home
,这取决于你的喜好。 我们更喜欢将路由名称大写。 我们更喜欢利用我们的路由名称。
跳转方法有
navigate
、push
、goBack
、popToTop
可以用
navigation.setParams
方法更新页面的参数
我们可以通过
options={({ route, navigation }) => ({ headerTitle: route.params.name })}
的方式在标题中使用参数
我们可以用
navigation.setOptions
更新页面配置
Stack.Navigator
initialRouteName
: 用来配置Stack.Navigator
的初始路由screenOptions
: 页面共享配置对象
Stack.Screen
name
: 页面名component
: 页面对应组件options
: 页面配置对象initialParams
: 默认参数
HeaderButtons.js
使用 react-navigation-header-buttons
组件搭配任意 Icon 组件可以自定义自己的 Header Button 组件,我这里为了演示方便,使用了 @ant-design/icons-react-native
:
import React from 'react';
import {
HeaderButtons as RNHeaderButtons,
HeaderButton as RNHeaderButton,
Item,
} from 'react-navigation-header-buttons';
import {IconOutline} from '@ant-design/icons-react-native';
const HeaderButton = props => {
return (
<RNHeaderButton
{...props}
IconComponent={IconOutline}
iconSize={props.iconSize || 23}
color={props.color || '#000000'}
/>
);
};
const HeaderButtons = props => {
return <RNHeaderButtons HeaderButtonComponent={HeaderButton} {...props} />;
};
HeaderButtons.Item = Item;
export default HeaderButtons;
IconWithBadge.js
import React from 'react';
import {View} from 'react-native';
import {Badge} from '@ant-design/react-native';
const IconWithBadge = ({children, badgeCount, ...props}) => {
return (
<View style={{width: 24, height: 24, margin: 5}}>
{children}
<Badge
{...props}
style={{position: 'absolute', right: -6, top: -3}}
text={badgeCount}
/>
</View>
);
};
export default IconWithBadge;
getActiveRouteName.js
/**
* Gets the current screen from navigation state
* @param state
*/
const getActiveRouteName = state => {
const route = state.routes[state.index];
if (route.state) {
// Dive into nested navigators
return getActiveRouteName(route.state);
}
return route.name;
};
export default getActiveRouteName;
getScreenOptions.js
import {TransitionPresets} from '@react-navigation/stack';
const getScreenOptions = () => {
return {
headerStyle: {
backgroundColor: '#ffffff',
}, // 一个应用于 header 的最外层 View 的 样式对象
headerTintColor: '#000000', // 返回按钮和标题都使用这个属性作为它们的颜色
headerTitleStyle: {
fontWeight: 'bold',
},
headerBackTitleVisible: false,
headerTitleAlign: 'center',
cardStyle: {
flex: 1,
backgroundColor: '#f5f5f9',
},
...TransitionPresets.SlideFromRightIOS,
};
};
export default getScreenOptions;
NavigationService.js
import React from 'react';
export const navigationRef = React.createRef();
const navigate = (name, params) => {
navigationRef.current && navigationRef.current.navigate(name, params);
};
const getNavigation = () => {
return navigationRef.current && navigationRef.current;
};
export default {
navigate,
getNavigation,
};
页面生命周期与React Navigation
一个包含 页面 A 和 B 的 StackNavigator ,当跳转到 A 时,componentDidMount
方法会被调用; 当跳转到 B 时,componentDidMount
方法也会被调用,但是 A 依然在堆栈中保持 被加载状态,他的 componentWillUnMount
也不会被调用。
当从 B 跳转到 A,B的 componentWillUnmount
方法会被调用,但是 A 的 componentDidMount
方法不会被调用,应为此时 A 依然是被加载状态。
React Navigation 生命周期事件
addListener
function Profile({ navigation }) {
React.useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
// Screen was focused
// Do something
});
return unsubscribe;
}, [navigation]);
return <ProfileContent />;
}
useFocusEffect
useFocusEffect(
React.useCallback(() => {
// Do something when the screen is focused
return () => {
// Do something when the screen is unfocused
// Useful for cleanup functions
};
}, []),
);
隐藏 Header/TabBar
headerMode:"none"
: hide Header forStack.Navigator
headerShown:false
: hide Header forStack.Screen
tabBar={() => null}
: hide TabBar forBottomTab.Navigator
import {NavigationContainer, useFocusEffect} from '@react-navigation/native';
import {createStackNavigator, TransitionPresets, HeaderBackButton} from '@react-navigation/stack';
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
const Stack = createStackNavigator();
const BottomTab = createBottomTabNavigator();
export default App = () => {
<NavigationContainer>
<Stack.Navigator headerMode="none">
<Stack.Screen
...
options={{ headerShown: false }}
/>
<Stack.Screen ...>
{() => (
<BottomTab.Navigator
...
tabBar={() => null}
>
...
</BottomTab.Navigator>
)}
</Stack.Screen>
</Stack.Navigator>
</NavigationContainer>
}
TabBar 的 StatusBar 不同
一般我们会对特殊的那个TabBar进行处理。
const getActiveRouteName = state => {
const route = state.routes[state.index];
if (route.state) {
// Dive into nested navigators
return getActiveRouteName(route.state);
}
return route.name;
};
const App = () => {
const ref = React.useRef(null);
return (
<>
{/* 访问 ref.current?.navigate */}
<NavigationContainer
ref={ref}
onStateChange={state => {
const previousRouteName = ref.current;
const currentRouteName = getActiveRouteName(state);
if (previousRouteName !== currentRouteName) {
console.log('[onStateChange]', currentRouteName);
if (currentRouteName === 'HomeScreen') {
StatusBar.setBarStyle('dark-content'); // 修改 StatusBar
} else {
StatusBar.setBarStyle('dark-content'); // 修改 StatusBar
}
}
}}
>
</NavigationContainer>
</>
)
}
监听安卓物理返回键
import {View, Text, BackHandler} from 'react-native';
const CustomAndroidBackButtonBehaviorScreen = ({navigation, route}) => {
useFocusEffect(
React.useCallback(() => {
const onBackPress = () => {
alert('物理返回键被拦截了!');
return true;
};
BackHandler.addEventListener('hardwareBackPress', onBackPress);
return () =>
BackHandler.removeEventListener('hardwareBackPress', onBackPress);
}, []),
);
return (
<View style={styles.container}>
<Text>AndroidBackHandlerScreen</Text>
</View>
);
};
在子组件中访问 navigation
我们可以通过 useNavigation()
hook 来访问 navigation,再也不用传递多层 navigation
import React from 'react';
import { Button } from 'react-native';
import { useNavigation } from '@react-navigation/native';
function GoToButton({ screenName }) {
const navigation = useNavigation();
return (
<Button
title={`Go to ${screenName}`}
onPress={() => navigation.navigate(screenName)}
/>
);
}
给页面传递额外的属性
<Stack.Screen
name="HomeScreen"
options={{headerTitle: '首页'}}>
{props => <HomeScreen {...props} extraData={{author: '杨俊宁'}} />}
</Stack.Screen>
获取 Header Height
import { useHeaderHeight } from '@react-navigation/stack'
const App = () => {
const HeaderHeight = useHeaderHeight() // 获取Header Height
return(...)
}
export default App
继续使用类组件
考虑到不适应 Hooks 的但是业务又很紧急的场景,我们可以再类组件之上封装一层来支持 React Navigation 的 Hooks 组件,之所以这么做,起因是因为 React Navigation 5 中我们只能通过 useHeaderHeight()
方法获取标题栏高度。
class Albums extends React.Component {
render() {
return <ScrollView ref={this.props.scrollRef}>{/* content */}</ScrollView>;
}
}
// 封装并导出
export default function(props) {
const ref = React.useRef(null);
useScrollToTop(ref);
return <Albums {...props} scrollRef={ref} />;
}