React Navigation V5、V6版本使用方式基本相同,只是改了部分屬性,參數。
安裝
核心:提供底層 API,可在此基礎上實現各種導航形式
yarn add react-native-screens react-native-safe-area-context @react-navigation/native
- react-native-screens: 用于原生層釋放未展示的頁面,改善 app 內存使用
- react-native-safe-area-context: 用于保證頁面顯示在安全區域(主要針對劉海屏)
-
@react-navigation/native
: 為 React Navigation 的核心
修改 MainActivity.java
添加以下代碼,否則 Android 下可能在某些情況下造成 App 崩潰,比如調整系統字體縮放/修改 APP 權限配置后再次返回 App,若沒有以下修改,App 崩潰。即使設置了,還是有問題,無法保持最后查看的頁面, App 會重新加載 js Bundle,返回到首頁(是使用 3.10.1
版本在 debug 模式下發現的該問題,其他情況還需實際測試)
....
import android.os.Bundle;
public class MainActivity extends ReactActivity {
....
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(null);
}
}
在 App.js 中添加以下代碼,激活 react-native-screens
的原生端(最新版本默認已為激活狀態,可省略該步驟)
import { enableScreens } from 'react-native-screens';
enableScreens();
Stack:相當于路由,如果不是僅需的 Tab 或 Drawer,必裝
yarn add @react-native-masked-view/masked-view react-native-gesture-handler @react-navigation/stack
- @react-native-masked-view/masked-view: 用在頭部導航欄中返回按鈕的顏色設置
- react-native-gesture-handler: 用于支持手勢切換頁面
-
@react-navigation/stack
: Stack Navigator
安裝完之后,在 js 入口文件,如 index.js 頂部添加 import 'react-native-gesture-handler';
,少了這一句,可能會導致生產環境 app 出現閃退現象。
其他:官方提供的幾種導航器,根據需要安裝,也可以參考自行建構
-
Drawer:
yarn add @react-navigation/drawer react-native-gesture-handler react-native-reanimated
-
Bottom Tabs:
yarn add @react-navigation/bottom-tabs
-
Material Bottom Tabs:
yarn add @react-navigation/material-bottom-tabs react-native-paper react-native-vector-icons
-
Material Top Tabs:
yarn add @react-navigation/material-top-tabs react-native-tab-view react-native-pager-view
安裝所需導航器并安裝相應依賴,有些依賴可能有重復,安裝一次就行了,比如 Drawer
與 Stack
都依賴 react-native-gesture-handler
,安裝一次即可。
最后
react-native-screens
需要修改 Android 平臺的 MainActivity.java
,不再需要其他操作了
// 頂部添加
import android.os.Bundle;
// 主體 class 中添加
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(null);
}
對于 iOS,需要在項目根目錄執行 npx pod-install
安裝原生組件的依賴
使用
先看以下一段偽代碼了解 React Navigation
的使用方法
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
const Stack = createStackNavigator()
const Tab = createBottomTabNavigator();
const Top = createMaterialTopTabNavigator();
const First = () => (<Top.Navigator ...props>
<Top.Screen ...props/>
<Top.Screen ...props/>
</Top.Navigator>);
const Main = () => (<Tab.Navigator ...props>
<Tab.Screen ...props component={First}/>
<Tab.Screen ...props/>
</Tab.Navigator>);
const App = () => (<NavigationContainer ...props>
<Stack.Navigator ...props>
<Stack.Screen ...props component={Main}/>
<Stack.Screen ...props/>
</Stack.Navigator>
</NavigationContainer>);
- 導航器總是使用
<Nav.Navigator> <Nav.Screen/> </Nav.Navigator>
格式組合頁面,其中Nav
可以使用官方提供的幾個導航器stack
、drawer
、bottom-tabs
、material-bottom-tabs
、material-top-tabs
,當然也可參考自行建構。 - 導航器本身可以作為另外一個導航器的
Screen component
,即當作一個普通頁面作為另外一個導航器的子頁面。 - 最終使用
NavigationContainer
包裹最頂層導航器。
一般使用Stack
作為頂層導航器(如上的例子),在 Tab 內打開頁面會覆蓋整個屏幕,退回后才能進行 Tab 切換。
當然也可以使用Tab
作為頂層導航器,切換頁面時 TabBar 不會被覆蓋,每個 Tab 都有獨立的堆棧。
React Navigation
對 NavigationContainer
、 Nav.Navigator
、 Nav.Screen
提供了豐富的配置選項,做個簡單介紹
一、 NavigationContainer
該組件在 @react-navigation/native
中定義,一般情況,一個 APP 只有一個,參考 官方文檔,該組件支持以下屬性
1. theme
主題,該屬性由 @react-navigation/native
緩存,但并未直接起作用,而是會下發到導航器,由導航器獲取并加以利用,默認提供了 淺色 和 深色 兩組屬性 屬性使用情況如下:
theme={
dark: false,
colors: {
// 文字顏色: Header 標題 / BottomTab 未激活文字
text: 'rgb(28, 28, 30)',
// 激活顏色: BottomTab 激活文字 / iOS Header 返回上一頁文字
primary: 'rgb(99, 164, 252)',
// 區塊背景色: 如 Header, TopTab, BottomTab 背景
card: 'rgb(255, 255, 255)',
// 邊框顏色: 如 Header, TopTab, BottomTab 邊框
border: 'rgb(216, 216, 216)',
// 背景色: 頁面整體背景顏色
background: 'rgb(242, 242, 242)',
// 提醒色: BottomTab 角標背景
notification: 'rgb(255, 59, 48)',
},
}
2. ref
獲取 NavigationContainer
實例,用于調用實例 api,可通過 console.log 打印可用 api
3. initialState
自定義傳入變量,多用于 deepLink,該項暫未驗證
4. onStateChange
導航狀態變化的監聽函數,可用于頁面統計或其他操作
5. onReady
V6 版本新增,容器加載并渲染完畢時的回調,僅會觸發一次。此時可安全的使用 ref
調用 API,也可在此時隱藏開屏頁
6. linking
V6 版本新增,用于 deepLink
7. children
子組件(導航器 Navigator
組件),該項一般使用 jsx 直接插入,而不是通過 props 傳遞,比如上面的示例,children
為 Stack.Navigator
二、Nav.Navigator
導航器根組件,用于包裹導航器下的頁面;通過閱讀源碼可知道所有導航器都支持4個屬性:
-
@react-navigation/routers
定義的initialRouteName
-
@react-navigation/core
定義的children
/screenOptions
/screenListeners
1. initialRouteName
導航器默認要顯示的 screen
2. children
導航器包裹的 screens,通常不會使用 Props 傳遞,而是在 jsx 中實現。
3. screenOptions
不同類型的導航器包裹的 screen 支持的屬性是一樣的,都會有一個 options
屬性,此處設置的 screenOptions
與 screen.options
屬性完全相同,作為所有 screen 的 options
默認值。
閱讀 @react-navigation/core
源碼和文檔,這個參數的值可以是 Object
或 Function({route, navigation}) => Object
,且未對 Object 字段做任何限制,而只是為導航器的實現提供了一個頂層 API,比如官方的兩個實現支持不同的 options:
-
@react-navigation/stack
->Stack.Navigator
的 screenOptions可用屬性 -
@react-navigation/bottom-tabs
->Tab.Navigator
的 screenOptions可用屬性
// 直接設置為 Object
<Nav.Navigator
screenOptions = {{
title, header, headerShown, ........
}}
>
<Nav.Screen ...props />
</Nav.Navigator>
// 或通過函數返回,比如大部分 screen 所需屬性相同,僅在函數內對特別的 screen 做處理
<Nav.Navigator
screenOptions = { ({route, navigation}) => {
return { title, header, headerShown, ........}
}}
>
<Nav.Screen ...props />
</Nav.Navigator>
在 V6 版本官方還提供了一個 Nav.Group
對具有相同屬性的 screen 進行分組批量設置
<Nav.Navigator>
<Nav.Group screenOptions={{}}>
<Nav.Screen/>
<Nav.Screen />
</Nav.Group>
<Nav.Group screenOptions={{}}>
<Nav.Screen/>
<Nav.Screen />
</Nav.Group>
</Nav.Navigator>
4. screenListeners
監聽導航器發送的事件消息,所有導航器共有的消息類型有 focus
/ blur
/ beforeRemove
/ state
(文檔),不同導航器還有特有消息,如:
該屬性的值與 screenOptions
有點類似,也可以指定為 Object
或 Function
,如
// 直接設置為 Object
<Nav.Navigator
screenListeners={{
focus: () => {},
state: (e) => { console.log('state changed', e.data);},
}}
>
<Nav.Screen ...props />
</Nav.Navigator>
// 或通過函數返回 要綁定的 監聽函數
<Nav.Navigator
listeners={({ navigation, route }) => ({
return {
focus: () => {},
state: (e) => { console.log('state changed', e.data);},
}
}}
>
<Nav.Screen ...props />
</Nav.Navigator>
以上四項為基礎屬性,適用于所有導航器,不同的導航器會在此基礎中拓展額外的屬性:
5. @react-navigation/stack
detachInactiveScreens
/ 屬性(文檔)keyboardHandlingEnabled
/ mode
/ headerMode
Stack.Navigator
的部分屬性現在已經或即將轉移到 options
中,具體支持哪些屬性需以官方文檔為準,下面介紹 stack
的 options
會提到目前的變化。
6. @react-navigation/bottom-tabs
detachInactiveScreens
/ backBehavior
(繼承自 @react-navigation/routers
) / sceneContainerStyle
/ tabBar
/ (V6版這兩個屬性移動到了 options 中配置) 屬性(文檔)lazy
/ tabBarOptions
三、Nav.Screen
導航器內的具體頁面,該組件是在 @react-navigation/core
中實現的,與導航器類型無關,所有類型的導航器 screen 都支持且僅支持以下屬性
1. name
頁面名稱,可用于導航跳轉
2. options
與 Nav.Navigator
中的 screenOptions
相同,單獨設置來覆蓋 screenOptions
的配置,僅針對當前頁面;同樣的,可以設置為 Object
或通過 Function
返回;具體結構由 Screen 所屬的 Navigator 類型決定。
3. listeners
與 Nav.Navigator
中的 screenListeners
相同,可以設置為 Object
或通過 Function
返回;僅對當前頁面進行監聽,不會覆蓋 Nav.Navigator
中的設置,即二者都會被觸發。
4. initialParams
傳遞給 Screen
組件的初始化 params,可在 screen 內獲取,從而顯示不同數據。
5. getId
屬性值為 Function(initialParams) => string
,返回一個唯一 ID,在多個頁面有相同 name
屬性時,可在使用 navigate('ScreenName', params)
時通過指定 params.userId
跳轉到預期頁面。
6. component / getComponent / children
Screen
綁定的組件, 可通過 component
指定組件對象,getComponent
回調返回組件,或直接使用 children
定義組件;三者互斥,一般使用 component
屬性來定義
<Nav.Screen component={Screen} />
<Nav.Screen getComponent={() => require('./Screen').default} />
<Nav.Screen>
{(props) => <Screen {...props} />}
</Nav.Screen>
四、頁面內
通過以上文檔可以看出,頁面的 options
/ listeners
都是由上層代碼控制,不過 React Navigation
也提供了相關接口,可直接在 Screen
組件內部維護。
// 函數式組件: react navigation 會傳遞 navigation / route 兩個參數
function Screen({ navigation, route }) {
// 在頁面顯示之前設(重)置 options 值,相當于在 componentDidMount 階段執行
// useLayoutEffect 是阻塞同步的,即執行完此處之后,才會繼續向下執行
React.useLayoutEffect(() => {
navigation.setOptions({
title:'....'
});
}, [navigation]);
// 綁定 listener, useEffect 是異步執行的,不會阻塞
React.useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
// do something
});
// 返回卸載監聽的方法,以便在當前組件注射時取消監聽
return unsubscribe;
}, [navigation]);
// 頁面內容
return <ScreenContent />;
}
// Class 組件:navigation / route 以 props 參數傳遞
class Screen extends React.Component {
componentDidMount() {
const {navigation} = this.props;
// render 后,未顯示前,設置 options
navigation.setOptions({
title:'....'
});
// 綁定監聽函數
this._unsubscribe = navigation.addListener('focus', () => {
// do something
});
}
componentWillUnmount() {
// 組件注銷時,取消監聽
this._unsubscribe();
}
render() {
return <ScreenContent />;
}
}
在 react navigation V4 之前的版本,還提供了一個 navigationOptions
靜態變量用于設置 options,V5 版本之后移除了該特性,如果要繼續使用,可使用以下方法
class Home extends React.Component {
static navigationOptions = {
title:'....'
};
}
function Screen() {
}
Screen.navigationOptions = {
title:'....'
};
// V4 版本無需額外設置了,react navigation 默認已支持,對于 V5 之后版本
<Nav.Navigator>
<Nav.Screen component={Home} options={Home.navigationOptions} />
<Nav.Screen component={Screen} options={Screen.navigationOptions} />
</Nav.Navigator>
當然,也可以使用同樣的方法自定義一個 static listener,但 react navigation 不推薦使用這種方式了,所以才會從內核中移除了該特性,因為這種方式有以下弊端(如果不在意以下弊端,仍然這么使用也是可以的):
- 如果是高階組件,靜態屬性需要額外的代碼才能工作
- 無法使用 props 和 context 的能力,靈活性變差
- 無法自動進行類型檢查,你需要手動給這個屬性添加注解
- 在 Fast Refresh 下有問題,具體來說是修改它無法觸發重新渲染
五、StackNavigator.Screen options
這是最常用的導航器,幾乎是必備的,通常情況下,使用默認的配置即可取得不錯的效果,不過 Stack
為了更加靈活,提供了很多 options
自定義屬性,這里對其做一個整理(若無特殊說明,則代表為 V5/V6 都支持的屬性)
最基本屬性
-
title
: 設置為 string, 會作為stack
導航器標題文字headerTitle
的 fallback -
keyboardHandlingEnabled
: 切換頁面時是否自動隱藏已打開的鍵盤,默認為true
(V6 新增,從 V5 版本的Stack.Navigator
屬性移動到了這里) -
presentation
: 頁面模式,支持card
(默認) /modal
/transparentModal
(V6 新增,該屬性相當于 V5 版本Stack.Navigator
的mode
屬性轉移到了這里),該值較為重要,會在下面單獨說明。 -
detachPreviousScreen
: V6 新增,是否在頁面切換后注銷上一個頁面以節省內存。默認情況下,若新頁面為 modal 模式,該值為false
,否則為true
。即下一個頁面未鋪滿全屏,當前頁面仍會顯示部分,就應該設置為false
。
注意:該值只有在Stack.Navigator
屬性值detachInactiveScreens=true
(默認)時才生效,該值一般無需手動設置,會根據頁面presentation
等屬性值自動設置合適的值。
與 Header 組件相關的屬性
V6 版與 V5 版相比,將 Header 相關組件從 stack
包中提取出來組合為一個 Elements
包,該包提供了 Header
、HeaderBackground
、HeaderTitle
、HeaderBackButton
、MissingIcon
、PlatformPressable
、ResourceSavingView
組件和一些工具函數。Header
組件并未直接支持 left / right,stack
導航器使用該 Header
擴充了左側組件,并額外支持一些其他屬性。這樣做的好處是,更利于自定義 Header,可利用 Elements
中組件自定義整個 Header,或僅自定義 Header 左(右)側組件。以下說明中:
- 無特殊標記,則代表是
Elements/Header
組件直接支持的屬性,V5/V6通用 -
+
: V6 版新增屬性(仍是Elements/Header
直接支持的屬性) -
*
: V5/V6通用(由stack
擴充而非Elements/Header
直接支持的屬性) -
*+
: V6 新增屬性(由stack
擴充而非Elements/Header
直接支持的屬性)
標題欄整體屬性
-
headerStyle
: 自定義標題欄樣式,如果要改變高度,應直接使用height:Number
設置,不要通過布局方式設置一個不確定的高度,該值對于頁面切換時的 Header 動效非常重要。 -
headerTransparent
: 標題欄是否透明,與headerStyle
中直接設置backgroundColor
的不同在于:這里設置透明,會使頁面的marginTop
為 0,此時需要定義headerBackground
組件來遮擋。 -
headerBackground
: 標題欄背景組件,配合headerTransparent
使用的,可以用來實現諸如毛玻璃 Header 的效果。 -
headerStatusBarHeight
: 手動設置 statusBar 高度,Header 組件會 paddingTop 這個值以保證在劉海屏機型也可以正常使用,默認會由系統自動獲取。 -
headerPressColor
(V5版名稱:): 點擊 Header 內按鈕組件的水波紋顏色,僅對 Android 5 及以上headerPressColorAndroid
- +
headerPressOpacity
: 點擊 Header 內按鈕組件的透明度,對 iOS 和 Android 5 以下 -
headerTintColor
: 設置標題色調(該屬性和headerPressColor
/headerPressOpacity
會傳遞給 Header 的各子組件使用,比如標題就會使用該屬性設置文字顏色,按鈕則使用到另兩個屬性) - *
header
: 自定義標題欄組件,定義為函數,返回一個 RN 組件;設置該屬性,即不再使用默認 Header。 - *
headerShown
: 是否顯示標題欄 - *+
headerMode
: 標題欄顯示模式,支持 "float"(iOS默認值)、"screen"(非 iOS 默認值)(該屬性 V5 版是在Nav.Navigator
屬性中設置,V6 轉移到了這里) -
: 安全區域設置(針對劉海屏機型),默認情況下會自動設置,但可以通過該屬性通過safeAreaInsets
{left, right, top, bottom}
手動設置,自定義設置注意考慮橫豎屏的情況。(該屬性僅 V5 支持,V6 已移除,設置安全區域可參考 官方文檔、用法說明)
標題組件
-
headerTitleAlign
: 標題對齊方式,支持left
(Android 默認) /center
(iOS 默認) -
headerTitleAllowFontScaling
: 標題文字是否隨系統文字大小縮放 -
headerTitleStyle
: 自定義標題文字的樣式 -
headerTitleContainerStyle
: 自定義標題文字所在 View 容器的樣式 -
headerTitle
: 標題,可直接設置文字,未設置則使用title
屬性;也可以設置為函數,返回一個組件,函數參數為{allowFontScaling, style, children}
,這三個參數是由上面屬性結合而來。
左側返回組件
-
headerLeft
: 自定義 Header 左側組件,props 會傳遞 options 設置 -
headerLeftContainerStyle
: 自定義包裹 Header 左側組件容器的樣式 - *
headerBackImage
: 返回鍵,設置為一個函數,返回“返回鍵”組件,函數參數為{tintColor:"標題顏色"}
- *
headerBackTitle
: 返回鍵右側的文字 - *
headerTruncatedBackTitle
: 返回鍵右側文字過長,標題欄無法顯示時的替代返回文字,默認: "Back" - *
headerBackAllowFontScaling
: 返回文字是否隨系統文字大小縮放 - *
headerBackTitleStyle
: 自定義返回文字樣式 - *
headerBackTitleVisible
: 是否顯示返回文字,Android 默認 false,iOS 默認 true - *
headerBackAccessibilityLabel
: 返回鍵的無障礙標簽
右側自定義組件
-
headerRight
: 自定義 Header 右側組件,指定為函數 或 RN組件,props 會傳遞 options 設置 -
headerRightContainerStyle
: 自定義包裹 Header 右側組件容器的樣式
與頁面組件相關的屬性
-
cardStyle
: 頁面 Card 的樣式 -
cardShadowEnabled
: 是否在切換頁面時顯示頁面邊緣的陰影,默認為false
,啟用陰影需要當前頁面背景不能為透明 +cardStyleInterpolator
屬性返回了shadowStyle
樣式,默認只有SlideFromRightIOS
動效支持且僅支持 iOS,因為陰影組件 Animated.View 的默認樣式是使用 shadowStyle 實現的,該類型 style 僅支持 iOS -
cardOverlayEnabled
: 是否在 Card 下方添加一個 overlay 組件(即在前一個 Card 的上方添加),iOS 默認為false
,Android
在presentation="transparentModal"
為false
,否則為true
-
cardOverlay
: 函數,返回cardOverlayEnabled=true
要覆蓋的組件,該組件可用于頁面切換時的效果設定,比如一個黑色的 view,切換過程中逐漸透明,甚至是毛玻璃組件,下方頁面就呈現出一種逐漸顯示的效果。
與頁面切換手勢相關的屬性
-
gestureEnabled
: 是否啟用手勢返回,iOS默認開啟(不開啟的話只能在頁面上自定義返回按鈕了),Android 默認是關閉的(Android 除了返回按鈕,還有物理/虛擬返回鍵) -
gestureDirection
: 返回的手勢滑動方向,支持以下值-
horizontal
: 從左到右 -
horizontal-inverted
: 從右到左 -
vertical
: 從上到下 -
vertical-inverted
: 從下到上
-
-
gestureResponseDistance
: 從邊緣為起點,支持手勢返回的距離,格式為{horizontal:50, vertical:135}
;比如手勢方向gestureDirection
為horizontal
,那么只有在左邊緣 50 以內的區域向右滑動才會響應。 -
gestureVelocityImpact
: 觸摸返回的手速設置,在手速低于該值時,滑動距離需大于滑動方向上尺寸的 50% 才會返回到上一頁,否則彈回;高于所設置手速,即使滑動距離未達到50%,也會返回到上一頁面;默認值為 0.3
與頁面切換效果相關的屬性
-
animationEnabled
: 是否使用頁面切換動效,在 Android 和 iOS 默認為true
,Web 為false
-
animationTypeForReplace
: 切換動畫的方式:支持 "push"(默認) 和 "pop" -
transitionSpec
: 切換頁面的動效配置 -
cardStyleInterpolator
: 切換頁面時 Screen Card 的樣式 -
headerStyleInterpolator
: 切換頁面時 Screen Header 的樣式
切換動效
由以上屬性可以看出,頁面切換效果由以下屬性共同構成:
transitionSpec
cardStyleInterpolator
headerStyleInterpolator
gestureDirection
前三個用于實現切換動效和樣式,gestureDirection
用于在 gestureEnabled=true
(iOS默認為 true) 時配合動效,比如切換為上下展開收縮,gestureDirection
則應該支持上下滑動的手勢。設置自定義動效可使用如下結構的代碼:
const transition = {
gestureDirection:"horizontal",
transitionSpec: {},
cardStyleInterpolator:() => {},
headerStyleInterpolator:() => {},
}
<Stack.Navigator
screenOptions={
cardStyle:{},
gestureEnabled:true,
...transition
}
>
<Stack.Screen />
</Stack.Navigator>
React Navigation 的設計初衷應該也在于此,所以已默認提供了幾組屬性,可以直接使用。
-
BottomSheetAndroid
: 半透明到不透明, 從底部滑入 -
FadeFromBottomAndroid
: 半透明到不透明, 從距離頂部一小段距離的位置滑至頂部 -
ModalFadeTransition
: 無運動, 僅半透明到不透明 -
ModalPresentationIOS
: 無透明度變化, 從底部滑倒接近頂部, 以卡片形式彈窗, 下方頁面會縮小 -
ModalSlideFromBottomIOS
: 無透明度變化, 從底部滑倒頂部 -
RevealFromBottomAndroid
: 無透明度變化, 新頁面從底部逐漸展開 -
ScaleFromCenterAndroid
: 透明到不透明, 從中心點爆炸式彈出 -
SlideFromRightIOS
: 無透明度變化, 從右側滑入(只有該效果實現了cardShadowEnabled
且僅支持 iOS) -
ModalTransition
: iOS 為ModalPresentationIOS
, Android 為BottomSheetAndroid
-
DefaultTransition
: iOS 為SlideFromRightIOS
, Android 為ScaleFromCenterAndroid
(API >= 29)、RevealFromBottomAndroid
(API = 28)、FadeFromBottomAndroid
(API < 28)
對于以上切換效果,有以下特點
- 對于
headerMode="float"
, 以上運動除ModalPresentationIOS
會強制修改headerMode="screen"
外,其他切換效果都是僅在頁面內容區發生,而不是整個頁面;頁面 Header 會保持獨立的運動,若前一個頁面沒有 Header,會從右側滑入,否則會漸顯式替換前一個 Header。 - iOS 默認啟用了手勢切換,所以
IOS
結尾的切換效果都沒有透明度變化,適合手勢切換,但也同樣可以用于 Android。但反過來則不行,非IOS
結尾的切換效果由于有透明度變化,不適合用于手勢切換。
使用方法:
import { TransitionPresets } from '@react-navigation/stack';
<Stack.Navigator
screenOptions={
cardStyle:{},
...TransitionPresets.SlideFromRightIOS,
}
>
<Stack.Screen />
</Stack.Navigator>
若對這些默認提供的效果都不滿意,那只能自定義了。
1、transitionSpec
transitionSpec
需要提供 open
/ close
兩個配置,每個配置需包含 animation
/ config
兩個屬性。 其中 config
根據 animation
類型進行配置。可參考 timing 、spring
const config = {
// 一般就兩種
animation: 'timing || spring',
// 根據 animation 值提供配置
config: {
// animation="timing" 支持:
duration:1000,
easing: Easing.ease,
// animation="spring" 支持:
stiffness: 1000,
damping: 500,
mass: 3,
overshootClamping: true,
restDisplacementThreshold: 0.01,
restSpeedThreshold: 0.01,
},
};
const transitionSpec = {
open: config, // 新頁面彈出時動效
close: config, // 新頁面收回時動效,一般二者為同一個
};
// React Navigation 提供了幾個默認的,可直接使用或作為參考
import { TransitionSpecs } from '@react-navigation/stack';
transitionSpec = TransitionSpecs.TransitionIOSSpec
transitionSpec = TransitionSpecs.FadeInFromBottomAndroidSpec
transitionSpec = TransitionSpecs.FadeOutToBottomAndroidSpec
transitionSpec = TransitionSpecs.RevealFromBottomAndroidSpec
2、cardStyleInterpolator
通過函數返回以下樣式
-
containerStyle
: Card 所在Animated.View
容器的樣式 -
cardStyle
: Card 組件 (Animated.View
) 樣式 -
overlayStyle
: 在cardOverlayEnabled=true
時,由cardOverlay
組件的樣式 -
shadowStyle
: 在cardShadowEnabled=true
時,Card 邊緣的Animated.View
組件樣式
cardStyleInterpolator = ({
current, //當前頁面值,如 current.progress 進度
next, //切換后的頁面值,如 next.progress 進度
index, //card 在 stack 堆棧中的序號
closing, //是關閉還是打開 1 or 0
layouts //布局尺寸 {screen}
}) => {
return {
containerStyle:{},
cardStyle:{},
overlayStyle:{},
shadowStyle:{},
}
}
// React Navigation 提供了幾個默認的,可直接使用或作為參考
import { CardStyleInterpolators } from '@react-navigation/stack';
cardStyleInterpolator = CardStyleInterpolators.forHorizontalIOS
cardStyleInterpolator = CardStyleInterpolators.forVerticalIOS
cardStyleInterpolator = CardStyleInterpolators.forModalPresentationIOS
cardStyleInterpolator = CardStyleInterpolators.forFadeFromBottomAndroid
cardStyleInterpolator = CardStyleInterpolators.forRevealFromBottomAndroid
3、HeaderStyleInterpolators
通過函數返回以下樣式
-
leftLabelStyle
: Header 返回鍵旁邊的"返回"文字所在Animated.Text
的樣式 -
leftButtonStyle
: Header 左側返回鍵外層的Animated.View
容器的樣式 -
rightButtonStyle
: Header 右側Animated.View
容器的樣式 -
titleStyle
: Header 標題所在Animated.View
容器的樣式 -
backgroundStyle
: Header 背景組件的樣式
HeaderStyleInterpolators = ({
current, //當前頁面值,如 current.progress 進度
next, //切換后的頁面值,如 next.progress 進度
layouts //布局尺寸: {screen, title, leftLabel}
}) => {
return {
leftLabelStyle:{},
leftButtonStyle:{},
rightButtonStyle:{},
titleStyle:{},
backgroundStyle:{},
}
}
// React Navigation 提供了幾個默認的,可直接使用或作為參考
import { HeaderStyleInterpolators } from '@react-navigation/stack';
HeaderStyleInterpolators = HeaderStyleInterpolators.forUIKit
HeaderStyleInterpolators = HeaderStyleInterpolators.forFade
HeaderStyleInterpolators = HeaderStyleInterpolators.forStatic
以上三個屬性可全部自定義,也可以部分自定義 + 部分使用 React Navigation 提供的預置,最后再添加一個 gestureDirection
屬性就可構成一組自定義頁面切換效果,非常的方便。
頁面模式
以上便是 Stack.Screen
的 options
屬性支持的所有配置,最后再對影響頁面效果較大的 headerMode
、presentation
配置稍作說明,headerMode
支持的兩個值:
-
float
: 此時頁面 Header 與頁面 Card 是分離的,有一個 Header 容器組件總是在頂部,所有頁面的 Header 都在這個容器里,這種模式下,在切換頁面時, Header 與 Card 可以獨立執行各自的切換動效,比如模擬 iOS 原生效果。 -
screen
: 每個頁面的 Header 都在各自的 Card 頂部,即每個頁面整體獨立。切換頁面時,是整個頁面進行動效過渡。 -
: 該模式在 V6 版已移除,使用none
headerShown=false
替代
presentation
配置更像一個快捷方式,修改該值,可能會自動設置 cardOverlayEnabled
、detachPreviousScreen
、headerMode
、gestureDirection
、transitionSpec
等屬性的默認值用以配合效果,但如果這些值手動設置了值,將不會自動配置,而是使用手動設置的值,若設置為 transparentModal
,默認 cardStyle
的背景將修改為透明。 支持以下三個值:
-
card
: 頁面切換為模擬原生的效果,iOS 為 Header 漸隱漸顯/Card左右顯示隱藏,Android 為整體由下向上顯示(默認值) -
modal
: 無論任何平臺,都設置為頁面整體由下向上滑動顯示(與card
模式下的 Android 由下向上的動效不同)-
headerMode
自動設置為screen
- 動效也會自動設置用以配合 modal 頁面切換效果
-
-
transparentModal
: 與modal
類似-
headerMode
自動設置為screen
- 屏幕背景會設置為透明,因此可以看到上一個頁面
- 自動設置
detachPreviousScreen=false
保持上一個頁面的渲染狀態 - 設置上一個/當前頁面的動效以配合效果
-
對于 card
、modal
比較好理解,很容易適配到具體使用場景,transparentModal
值則更傾向于模擬彈窗效果,比如
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeStack} />
<Stack.Screen
name="Modal"
component={ModalScreen} // ModalScreen 為彈窗組件
options={{
presentation: 'transparentModal',
headerShown: false, // 不要顯示 Header
cardOverlayEnabled: true, // 彈窗下顯示一個半透明 overlay 蒙層
}}
/>
</Stack.Navigator>
如果需要對于 ModalScreen
自定義動畫效果,可以借助 useCardAnimation
接口實現
import { Animated, View, Text, Pressable, Button, StyleSheet } from 'react-native';
import { useTheme } from '@react-navigation/native';
import { useCardAnimation } from '@react-navigation/stack';
function ModalScreen({ navigation }) {
const { colors } = useTheme();
const { current } = useCardAnimation();
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center'}}>
<Pressable
style={[StyleSheet.absoluteFill, {backgroundColor: 'rgba(0, 0, 0, 0.5)' } ]}
onPress={navigation.goBack}
/>
<Animated.View
style={{
padding: 16, width: '90%', maxWidth: 400, borderRadius: 3,
backgroundColor: colors.card,
transform: [
{
scale: current.progress.interpolate({
inputRange: [0, 1],
outputRange: [0.9, 1],
extrapolate: 'clamp',
}),
},
],
}}
>
<Text>
Mise en place is a French term that literally means “put in place.” It
also refers to a way cooks in professional kitchens and restaurants
set up their work stations—first by gathering all ingredients for a
recipes, partially preparing them (like measuring out and chopping),
and setting them all near each other. Setting up mise en place before
cooking is another top tip for home cooks, as it seriously helps with
organization. It’ll pretty much guarantee you never forget to add an
ingredient and save you time from running back and forth from the
pantry ten times.
</Text>
<Button
title="Okay" color={colors.primary} style={{ alignSelf: 'flex-end' }}
onPress={navigation.goBack}
/>
</Animated.View>
</View>
);
}
六、BottomTabsNavigator.Screen options
最基本屬性
-
title
: 設置為 string, 會作為標題文字headerTitle
的 fallback,底部 Tab 的tabBarLabel
文字的 fallback -
lazy
: 選修卡頁面是否為懶加載(即切換至頁面時才渲染),默認為true
-
unmountOnBlur
: 頁面失去焦點后是否自動卸載,若為true
,每次切換至頁面都會重新加載,默認為false
與 Header 組件相關的屬性
參考 StackNavigator options
中與 Header 組件相關的屬性,支持 Elements/Header
組件直接支持的所有屬性,參考上面 StackNavigator.Screen options
所介紹的不含 *
的 Header 相關屬性。除了這些屬性外,額外擴展并支持以下屬性
- *
header
: 自定義標題欄組件,定義為函數,返回一個 RN 組件;設置該屬性,即不再使用默認 Header。 - *
headerShown
: 是否顯示標題欄
與 TabBar 組件相關的屬性
這些屬性在 V5 版時,是設置在 Navigator
的 tabBarOptions
屬性中,V6 版移動到了 Screen
的 options
中,為了更容易理解,可結合下圖
上圖中除了 sceneContainerStyle
/ tabBar
是在 BottomTabsNavigator.Navigator
屬性中設置的,其他都是在 BottomTabsNavigator.Screen
中設置的。默認的 tabBar
組件會利用下面要介紹的 Screen options
渲染為上圖結構,如果自定義了 tabBar
組件,則可利用 Screen options
自行設計結構。既然提到了 Navigator
屬性,順帶說下另外兩個支持的屬性:
-
detachInactiveScreens
: 切換 Tab 后,是否回收未顯示 Tab 頁面內存,默認為true
-
backBehavior
: 在 Tab 頁面按下物理(虛擬)返回鍵后的行為,支持以下值-
firstRoute
: 跳轉到第一個 Tab 頁面(默認) -
initialRoute
: 跳轉到載入時的 Tab 頁面(由initialRouteName
指定的頁面) -
order
: 按照順序依次跳轉到前一個頁面 -
history
: 按照瀏覽歷史依次跳轉到上一個訪問的頁面 -
none
: 什么都不做,通常會直接返回桌面
-
說完 BottomTabsNavigator.Navigator
的屬性,下面說一下 BottomTabsNavigator.Screen
的 options
屬性中與 TabBar
相關的屬性,可結合上圖進行理解。
-
tabBarBackground
: 默認情況下,背景為tabBar
的背景色,若指定了該組件,tabBar
背景色會自動設置為透明,tabBarBackground
組件在 Z 軸上位于tabBar
的下面,可以設置一些個性的 UI 效果,比如漸變色、圖片、毛玻璃等。 -
tabBarStyle
: TabBar 整體容器的樣式 -
tabBarShowLabel
: 是否顯示 TabBar 的文字 -
tabBarLabelPosition
: TabBar 文字顯示的位置,默認會根據設備類型自動顯示-
below-icon
: 文字顯示在圖標下面(手機默認) -
beside-icon
: 文字顯示在圖標右邊(平板默認)
-
-
tabBarInactiveTintColor
: 默認狀態下文字顏色 -
tabBarActiveTintColor
: 激活狀態下文字顏色 -
tabBarInactiveBackgroundColor
: 默認狀態下背景顏色 -
tabBarActiveBackgroundColor
: 激活狀態下背景顏色 -
tabBarHideOnKeyboard
: 在鍵盤展開時隱藏 TabBar,默認false
對于 StackNavigator
,每個頁面都是獨立的,所有屬性都是對于所屬頁面而言的。而 BottomTabsNavigator
則不然,多個頁面公用同一個 TabBar,以上是共用屬性,每個處于激活的頁面設置的屬性都會影響整個 TabBar,比如下面這種效果
上面為共用屬性,而以下屬性則是每個頁面的私有屬性,即僅會影響所屬頁面的 TabItem。
-
tabBarItemStyle
: TabBar Item 容器的樣式 -
tabBarButton
: 設置tabBarLabel
,tabBarIcon
,tabBarBadge
的容器組件,通常無需設置,可參考默認的 button -
tabBarIcon
: TabBar 圖標組件(會收到{ focused: boolean, color: string, size: number }
參數) -
tabBarIconStyle
: TabBar 圖標樣式 -
tabBarBadge
: TabBar 角標,可以是String
或Number
-
tabBarBadgeStyle
: TabBar 角標樣式 -
tabBarLabel
: TabBar 要顯示的文字(不設置會使用title
屬性),可以設置為String
或返回 React 組件的函數(函數會收到{ focused: boolean, color: string }
參數) -
tabBarLabelStyle
: TabBar 文字的樣式 -
tabBarAllowFontScaling
: TabBar 文字是否隨系統字體大小縮放 -
tabBarAccessibilityLabel
: 無障礙標簽 -
tabBarTestID
: 用于本地測試的 ID
結合 【四、頁面內】 章節,可以使用 React.useLayoutEffect
在頁面內設置 options
,僅適合 TabBar 的共用屬性,而不適合 TabBar 私有屬性,畢竟不能讓用戶激活了 Tab 頁面后,才能看到諸如 tabBarBadge
/ tabBarLabel
信息,這一點需要注意。
七、接口
1. 頁面組件會收到 navigation 和 route 兩個參數。
navigation
提供相關操作API,根據 Screen 組件所在導航器的不同,API 也會有所不同。
通用API,所有類型導航器都可使用
-
navigate
: 跳轉到指定頁面 -
goBack
: 關閉當前頁面返回到上一頁 -
reset
: 重置導航器狀態 -
setParams
: 更新當前頁面的route.params
參數 -
setOptions
: 更新當前頁面的options
選項配置 -
isFocused
: 檢測當前頁面是否處于活動狀態 -
dispatch
: 發送 Action 給導航器,可參考 文檔 -
getParent
: 若當前導航器嵌套在另外一個導航器中,返回上級導航器,否則返回undefined
-
getState
: 獲取導航器當前的狀態,一般用不到,少數情況下可能用得到
stack 導航器獨有
-
replace
: 替換當前頁面為指定頁 -
push
: 添加一個新頁面到堆棧 -
pop
: 從堆棧彈出當頁面 -
popToTop
: 返回到堆棧的起始頁
tab 導航器獨有
-
jumpTo
: 跳轉到 Tab 內的指定頁面
route
屬性提供當前頁面的相關信息
-
key
: 頁面唯一值,通常為自動生成 -
name
: 所定義的頁面名稱 -
path
: 頁面路徑,通過 Link 打開的頁面才會有這個屬性 -
params
: 頁面導航時傳遞的參數
2. 非頁面組件如何使用 navigation 和 route 屬性
通常可以在頁面內調用組件時,將 navigation 和 route 以 props 的方式傳遞給子組件,但這樣對于嵌套較深的組件使用起來非常痛苦,另外子組件也要依賴父組件正確傳遞,React Navigation
提供了另外一種方法:
import * as React from 'react';
import { View, Text, Button } from 'react-native';
import { useNavigation, useRoute } from '@react-navigation/native';
function MyConmpoent() {
const navigation = useNavigation();
const route = useRoute();
return <View>
<Text>{route.params.caption}</Text>
<Button
title="Back"
onPress={() => {
navigation.goBack();
}}
/>
</View>;
}
// 對于 class 組件
class MyConmpoent extends React.Component {
render() {
const { navigation, route } = this.props;
return <View>
<Text>{route.params.caption}</Text>
<Button
title="Back"
onPress={() => {
navigation.goBack();
}}
/>
</View>;
}
}
// Wrap and export
export default function(props) {
props.navigation = useNavigation();
props.route = useRoute();
return <MyConmpoent {...props} />;
}
3. 其他可用的 Hook API
import * as React from 'react';
import { View, Text, Button } from 'react-native';
// 可用 Hook API
import {
useNavigation,
useIsFocused,
useLinkTo,
useLinkProps,
useLinkBuilder,
useScrollToTop,
useTheme
} from '@react-navigation/native';
// function 組件
function MyConmpoent() {
const theme = useTheme();
const isFocused = useIsFocused();
const state = useNavigationState(state => state);
const linkTo = useLinkTo();
const { onPress, ...props } = useLinkProps({ to, action });
const buildLink = useLinkBuilder();
const scrollRef = React.useRef(null);
useScrollToTop(scrollRef);
// code
}
// 對于 class 組件
class MyConmpoent extends React.Component {
render() {
const {theme, isFocused, state, linkTo, onPress, buildLink, scrollRef} = this.props;
// code
}
}
// Wrap and export
export default function(props) {
props.theme = useTheme();
props.isFocused = useIsFocused();
props.state = useNavigationState(state => state);
props.linkTo = useLinkTo();
props.onPress = useLinkProps({ to, action }).onPress;
props.buildLink = useLinkBuilder();
const scrollRef = React.useRef(null);
useScrollToTop(scrollRef);
return <MyConmpoent {...props} />;
}
將這些API分為三類,第一類有 useTheme
, isFocused
, useNavigationState
,這三個使用 get 型 API 是可以直接獲取的,比如 navigation.isFocused()
,但在 render() 界面時依賴相關變量的話,這些 API Hook 就比較有用了,當這些相關變量發生變化,界面會自動更新。
第二類為 useLinkTo
, useLinkProps
, useLinkBuilder
,這三個都與 Link
功能有關,以偽代碼做個說明:
import { Link, useLinkTo, useLinkProps, useLinkBuilder} from '@react-navigation/native';
// Link 組件使用 Text 模擬,類似于 Html 的 a 標簽,接受 to / action 兩個參數
// to 指定目標頁面, action 與 navigate.dispatch 接口參數同,不指定為 navigate action
function Componet() {
return (
<Link
to={{ screen: 'Profile', params: { id: 'jane' } }}
action={StackActions.replace('Profile', { id: 'jane' })}
> Go </Link>
);
}
// 可以使用 useLinkBuilder 生成 Link 組件的 to 參數
function Componet({ route }) {
const buildLink = useLinkBuilder();
return (
<Link
to={buildLink(route.name, route.params)}
action={StackActions.replace('Profile', { id: 'jane' })}
> Go </Link>
);
}
// Link 組件使用 Text 模擬,可以使用 useLinkProps 自定義其他組件模擬
function LinkButton({ route }) {
const { onPress, ...props } = useLinkProps({ to, action });
return (
<Button onPress={onPress}> Go </Button>
);
}
// 這樣就可以和使用 Link 一樣的方式,使用自己創建的 'Link' 組件了
<LinkButton to={} action={}/>
// useLinkTo 與以上不同,更類似于 navigation.navigate , 用于跳轉到指定頁面
// 但提供的參數不同,這里需要提供 Deep Link 所設置的頁面 path
function Screen() {
const linkTo = useLinkTo();
return (
<Button onPress={() => linkTo('/profile/jane')}>
Go to Jane's profile
</Button>
);
}
第三類是 useScrollToTop
Hook,該 API 的作用是為了模擬原生 Bottom Tab 的效果,如果 Tab 頁面是可滾動的(比如 ScrollView
,FlatList
),在頁面已處于激活狀態的情況下,點擊底部 Tab 圖標,頁面滾動到最頂部。
import * as React from 'react';
import { ScrollView } from 'react-native';
import { useScrollToTop } from '@react-navigation/native';
function Screen() {
const ref = React.useRef(null);
useScrollToTop(ref);
// 如果希望點擊底部 Tab 圖標不是滾動到最頂部,可以這樣來指定一個 offset 值
// useScrollToTop(React.useRef({
// scrollToTop: () => ref.current?.scrollToOffset({ offset: -100 }),
// }));
return <ScrollView ref={ref}>{/* content */}</ScrollView>;
}
最后,除了以上 Hook API,React Navigation
還提供了一個 useFocusEffect
Hook,該 API 與以上都不同,所以放到最后單獨說一下。以上 API 都是返回值式的 Hook,該 Hook 則更類似于添加一個 listener 監聽:
import { useFocusEffect } from '@react-navigation/native';
function Profile({ userId }) {
const [user, setUser] = React.useState(null);
// useFocusEffect 與 React.useEffect 類似,不同之處在于只會在頁面激活時觸發
// 可使用 React.useCallback 包裹回調,這樣回調只會在首次激活或依賴項發生變化才觸發
// 否則每次頁面激活都會被觸發
useFocusEffect(
React.useCallback(() => {
const unsubscribe = API.subscribe(userId, user => setUser(user));
return () => unsubscribe();
}, [userId])
);
// 一般遠程請求都是異步的,所以務必只請求一次
//(因為該回調不一定僅觸發一次,可能造成競爭請求)
// 如果請求 API 未提供取消機制,需自行處理,如:
useFocusEffect(
React.useCallback(() => {
let isActive = true;
const fetchUser = async () => {
try {
const user = await API.fetch({ userId });
if (isActive) {
setUser(user);
}
} catch (e) {
// Handle error
}
};
fetchUser();
return () => {
isActive = false;
};
}, [userId])
);
return <ProfileContent user={user} />;
}
// 對于 class 組件,需采用類似于 StatusBar 的方法
function FetchUserData() {
useFocusEffect(
....
);
return null;
}
class Profile extends React.Component {
_handleUpdate = user => {
// Do something with user object
};
render() {
return (
<>
<FetchUserData />
{/* 其他組件 */}
</>
);
}
}