React Navigation V5/V6 用法詳解

React Navigation V5、V6版本使用方式基本相同,只是改了部分屬性,參數。

安裝


核心:提供底層 API,可在此基礎上實現各種導航形式

yarn add react-native-screens react-native-safe-area-context @react-navigation/native

修改 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

安裝完之后,在 js 入口文件,如 index.js 頂部添加 import 'react-native-gesture-handler';,少了這一句,可能會導致生產環境 app 出現閃退現象。

其他:官方提供的幾種導航器,根據需要安裝,也可以參考自行建構

  • Draweryarn add @react-navigation/drawer react-native-gesture-handler react-native-reanimated
  • Bottom Tabsyarn add @react-navigation/bottom-tabs
  • Material Bottom Tabsyarn add @react-navigation/material-bottom-tabs react-native-paper react-native-vector-icons
  • Material Top Tabsyarn add @react-navigation/material-top-tabs react-native-tab-view react-native-pager-view

安裝所需導航器并安裝相應依賴,有些依賴可能有重復,安裝一次就行了,比如 DrawerStack 都依賴 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>);
  1. 導航器總是使用 <Nav.Navigator> <Nav.Screen/> </Nav.Navigator> 格式組合頁面,其中 Nav 可以使用官方提供的幾個導航器 stackdrawerbottom-tabsmaterial-bottom-tabsmaterial-top-tabs,當然也可參考自行建構。
  2. 導航器本身可以作為另外一個導航器的 Screen component,即當作一個普通頁面作為另外一個導航器的子頁面。
  3. 最終使用 NavigationContainer 包裹最頂層導航器。
    一般使用 Stack 作為頂層導航器(如上的例子),在 Tab 內打開頁面會覆蓋整個屏幕,退回后才能進行 Tab 切換。
    當然也可以使用 Tab 作為頂層導航器,切換頁面時 TabBar 不會被覆蓋,每個 Tab 都有獨立的堆棧。

React NavigationNavigationContainerNav.NavigatorNav.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 傳遞,比如上面的示例,childrenStack.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 屬性,此處設置的 screenOptionsscreen.options 屬性完全相同,作為所有 screen 的 options 默認值。

閱讀 @react-navigation/core 源碼和文檔,這個參數的值可以是 ObjectFunction({route, navigation}) => Object,且未對 Object 字段做任何限制,而只是為導航器的實現提供了一個頂層 API,比如官方的兩個實現支持不同的 options:

// 直接設置為 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 (文檔),不同導航器還有特有消息,如:

  • @react-navigation/stackevents
  • @react-navigation/bottom-tabsevents

該屬性的值與 screenOptions 有點類似,也可以指定為 ObjectFunction,如

// 直接設置為 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 中,具體支持哪些屬性需以官方文檔為準,下面介紹 stackoptions 會提到目前的變化。

6. @react-navigation/bottom-tabs

detachInactiveScreens / backBehavior (繼承自 @react-navigation/routers) / sceneContainerStyle / tabBar / lazy / tabBarOptions (V6版這兩個屬性移動到了 options 中配置) 屬性(文檔

三、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.Navigatormode 屬性轉移到了這里),該值較為重要,會在下面單獨說明。
  • detachPreviousScreen: V6 新增,是否在頁面切換后注銷上一個頁面以節省內存。默認情況下,若新頁面為 modal 模式,該值為 false,否則為 true。即下一個頁面未鋪滿全屏,當前頁面仍會顯示部分,就應該設置為 false
    注意:該值只有在 Stack.Navigator 屬性值 detachInactiveScreens=true(默認)時才生效,該值一般無需手動設置,會根據頁面 presentation 等屬性值自動設置合適的值。

與 Header 組件相關的屬性

V6 版與 V5 版相比,將 Header 相關組件從 stack 包中提取出來組合為一個 Elements 包,該包提供了 HeaderHeaderBackgroundHeaderTitleHeaderBackButtonMissingIconPlatformPressableResourceSavingView 組件和一些工具函數。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版名稱: headerPressColorAndroid): 點擊 Header 內按鈕組件的水波紋顏色,僅對 Android 5 及以上
  • +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 默認為 falseAndroidpresentation="transparentModal"false,否則為 true
  • cardOverlay: 函數,返回 cardOverlayEnabled=true 要覆蓋的組件,該組件可用于頁面切換時的效果設定,比如一個黑色的 view,切換過程中逐漸透明,甚至是毛玻璃組件,下方頁面就呈現出一種逐漸顯示的效果。

與頁面切換手勢相關的屬性

  • gestureEnabled: 是否啟用手勢返回,iOS默認開啟(不開啟的話只能在頁面上自定義返回按鈕了),Android 默認是關閉的(Android 除了返回按鈕,還有物理/虛擬返回鍵)
  • gestureDirection: 返回的手勢滑動方向,支持以下值
    • horizontal: 從左到右
    • horizontal-inverted: 從右到左
    • vertical: 從上到下
    • vertical-inverted: 從下到上
  • gestureResponseDistance: 從邊緣為起點,支持手勢返回的距離,格式為 {horizontal:50, vertical:135};比如手勢方向 gestureDirectionhorizontal,那么只有在左邊緣 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 類型進行配置。可參考 timingspring

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.Screenoptions 屬性支持的所有配置,最后再對影響頁面效果較大的 headerModepresentation 配置稍作說明,headerMode 支持的兩個值:

  • float: 此時頁面 Header 與頁面 Card 是分離的,有一個 Header 容器組件總是在頂部,所有頁面的 Header 都在這個容器里,這種模式下,在切換頁面時, Header 與 Card 可以獨立執行各自的切換動效,比如模擬 iOS 原生效果。
  • screen: 每個頁面的 Header 都在各自的 Card 頂部,即每個頁面整體獨立。切換頁面時,是整個頁面進行動效過渡。
  • none: 該模式在 V6 版已移除,使用 headerShown=false 替代

presentation 配置更像一個快捷方式,修改該值,可能會自動設置 cardOverlayEnableddetachPreviousScreenheaderModegestureDirectiontransitionSpec 等屬性的默認值用以配合效果,但如果這些值手動設置了值,將不會自動配置,而是使用手動設置的值,若設置為 transparentModal,默認 cardStyle 的背景將修改為透明。 支持以下三個值:

  • card: 頁面切換為模擬原生的效果,iOS 為 Header 漸隱漸顯/Card左右顯示隱藏,Android 為整體由下向上顯示(默認值)
  • modal: 無論任何平臺,都設置為頁面整體由下向上滑動顯示(與 card 模式下的 Android 由下向上的動效不同)
    • headerMode 自動設置為 screen
    • 動效也會自動設置用以配合 modal 頁面切換效果
  • transparentModal: 與 modal 類似
    • headerMode 自動設置為 screen
    • 屏幕背景會設置為透明,因此可以看到上一個頁面
    • 自動設置 detachPreviousScreen=false 保持上一個頁面的渲染狀態
    • 設置上一個/當前頁面的動效以配合效果

對于 cardmodal 比較好理解,很容易適配到具體使用場景,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 版時,是設置在 NavigatortabBarOptions 屬性中,V6 版移動到了 Screenoptions 中,為了更容易理解,可結合下圖

BottonTab(紅色為支持屬性)

上圖中除了 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.Screenoptions 屬性中與 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,比如下面這種效果

TabBar 效果

上面為共用屬性,而以下屬性則是每個頁面的私有屬性,即僅會影響所屬頁面的 TabItem。

  • tabBarItemStyle: TabBar Item 容器的樣式
  • tabBarButton: 設置 tabBarLabel,tabBarIcon,tabBarBadge 的容器組件,通常無需設置,可參考默認的 button
  • tabBarIcon: TabBar 圖標組件(會收到 { focused: boolean, color: string, size: number } 參數)
  • tabBarIconStyle: TabBar 圖標樣式
  • tabBarBadge: TabBar 角標,可以是 StringNumber
  • 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. 頁面組件會收到 navigationroute 兩個參數。

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 頁面是可滾動的(比如 ScrollViewFlatList),在頁面已處于激活狀態的情況下,點擊底部 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 />
        {/* 其他組件 */}
      </>
    );
  }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。