文章示例原始碼: 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} />;
}
複製程式碼
聯絡作者
作者微信 | 知識星球 | 讚賞作者 |
---|---|---|