React Native 中的狀態列

hezhii發表於2019-01-24

在實際專案中,我們常常需要根據頁面的不同去修改狀態列的表現。例如:頁面頭部圖片延伸到狀態列下並且狀態列透明;狀態列的顏色和標題欄的顏色相同;狀態列內容顏色的深淺變化。 在此之前,我寫了一篇React Navigation 構建 Android 和 iOS 統一的 UI的文章,裡面簡單的說到了 Android 狀態列的一些設定。後來我發現並不是我想的那麼簡單,因此通過這篇部落格進行補充,文中會更加詳細的介紹狀態列相關的內容以及 React Native 專案中如何去控制狀態列,使應用在 iOS 和 Android 平臺上都具有很好的表現。

示例程式碼我上傳到了 GitHub

預備知識

在正式開始之前,我先介紹一些我目前瞭解下來的相關知識,為後面的內容進行一些鋪墊。

iOS 中的狀態列

在 iOS 中,頁面預設全屏(狀態列不佔空間),狀態列內容預設是深色。因為頁面全屏,所以如果我們不進行處理,內容會跑到狀態列下面去。同時,由於 iPhone X 等劉海屏手機的出現,導致狀態列的高度發生了變化,由之前的 20 變成了 34。為了解決此問題,我們可以手動給頂部元件設定 paddingTop(值根據機型判斷),或者使用 SafeAreaView 元件。

在 iOS 中我們只用處理好安全區域的問題,然後根據頁面的不同去設定內容的顏色深淺即可。

Android 中的狀態列

在 Android 中,頁面預設非全屏(狀態列佔空間),狀態列內容預設是淺色

Android 中對狀態列的支援經歷了幾個版本:

  • Android4.4(API 19) ~ Android 5.0(API 21):通過 FLAG_TRANSLUCENT_STATUS 設定頁面為全屏且狀態列半透明(淺灰色)。
  • Android 5.0(API 21):提供了 droid:statusBarColor 屬性和 setStatusBarColor 方法用來設定狀態列的顏色。
  • Android 6.0(API 23):通過 SYSTEM_UI_FLAG_LIGHT_STATUS_BAR 支援設定狀態列內容為深色

其中,如果想要設定狀態列顏色,則不能設定 FLAG_TRANSLUCENT_STATUS

在 Android 應用中,每個 Activity 都對應一個狀態列。這意味著,為一個頁面設定狀態列不會對其他頁面的狀態列造成影響。

React Native 中的狀態列

React Native 官方提供了 StatusBar 元件用於控制狀態列,支援設定內容深淺色,狀態列背景(Android)等。

StatusBar 可以同時新增多個,而屬性則會按照載入順序合併(後者覆蓋前者)。

不同於 Android 中的狀態列,在 React Native 中狀態列是公用的,任何一個地方修改狀態列都會導致狀態列發生變化,即使切換到了其他未設定的頁面。因此,我們需要在每個頁面渲染時都設定一下相應的狀態列,或是在離開設定了狀態列的頁面時重置狀態列。

實際案例

在瞭解了必要的知識後,讓我們通過一個實際案例來看看我們需要做什麼以及怎麼做才更好。

在這個案例中,有三個頁面:主頁,我的,登入。其中“主頁”和“我的”是兩個標籤頁,“主頁”頭部有背景圖片,“我的”頁面頂部是藍色,“登入”頁面頂部為白色。頁面效果如下圖。

React Native 中的狀態列

“主頁”和“我的”頁面使用自定義的 Header,該元件會根據當前裝置,獲取狀態列的高度:

const STATUS_BAR_HEIGHT = isiOS() ? (isiPhoneX() ? 34 : 20) : StatusBar.currentHeight
複製程式碼

其中判斷裝置使用的下面的方法:

// iPhone X、iPhone XS
const X_WIDTH = 375;
const X_HEIGHT = 812;

// iPhone XR、iPhone XS Max
const XSMAX_WIDTH = 414;
const XSMAX_HEIGHT = 896;

const DEVICE_SIZE = Dimensions.get('window');
const { height: D_HEIGHT, width: D_WIDTH } = DEVICE_SIZE;

export const isiOS = () => Platform.OS === 'ios'

export const isiPhoneX = () => {
  return (
    isiOS() &&
    ((D_HEIGHT === X_HEIGHT && D_WIDTH === X_WIDTH) ||
      (D_HEIGHT === X_WIDTH && D_WIDTH === X_HEIGHT)) ||
    ((D_HEIGHT === XSMAX_HEIGHT && D_WIDTH === XSMAX_WIDTH) ||
      (D_HEIGHT === XSMAX_WIDTH && D_WIDTH === XSMAX_HEIGHT))
  );
};
複製程式碼

獲取到狀態列的高度之後,根據當前是不是全屏(fullSreen 屬性為 true 或者是 iOS 裝置)來設定自身高度和 paddingTop,標題欄高度統一設定為 44

const headerStyle = [
  styles.header,
  (fullScreen || isiOS()) && {
    height: STATUS_BAR_HEIGHT + HEADER_HEIGHT,
    paddingTop: STATUS_BAR_HEIGHT
  }
]
複製程式碼

“登入”頁面的 Header 則是 react-navigation 預設的 Header 元件,在 Android 中標題欄高度被設定為 56

處理狀態列的問題

從上圖的案例中,可以發現以下幾點問題:

  1. iOS 裝置中,狀態列內容的顏色顯示不正確,“主頁”和“我的”頁面狀態列應該是淺色
  2. Android 裝置中,“主頁”的狀態列應該是透明的,並且圖片應該延伸到狀態列下。
  3. Android 裝置中,“我的”頁面狀態列顏色應該也是藍色。

為了讓應用表現得更好,我們需要根據頁面動態的調整狀態列。React Native 為開發者提供了 StatusBar 元件去控制狀態列。

StatusBar 元件控制狀態列

我們在“主頁”中,設定狀態列內容為“淺色”,背景色為透明,translucenttrue。然後,“主頁”和“我的”頁面的 Header 都新增 fullScreen 屬性。效果如下:

React Native 中的狀態列

從圖中可以看到,因為頁面路由是 js 層做的,整個應用對應一個 StatusBar,雖然“我的”和“登入”頁面都沒有設定狀態列,但狀態列也是透明的。

這樣就有一個問題,“登入”頁面其實使用預設效果即可,但是由於其他頁面設定了狀態列,導致進入到“登入”頁面時效果就不對了。所以,每個頁面都需要設定相應的狀態列,因為狀態列可能被其他頁面改變。

接下來,在“登入”頁面設定狀態列為白色且內容為深色:

<StatusBar translucent={false} backgroundColor='#fff' barStyle="dark-content" />
複製程式碼

React Native 中的狀態列

現在“登入”頁面的效果就和期望的一樣了,當我們從“登入”頁面返回到主介面時,狀態列會切換回之前的狀態,但是有一點延時。按照前面的經驗,當從登入頁面回來時,狀態列應該仍是白色且內容深色。因為返回時,前面的頁面不會重新渲染,狀態列應該會保持當前的狀態。但是狀態列卻自動調整成了之前的狀態,雖然有一點延時。我在 react-navigationGitHub issue 中發現有人提到,當離開 route 時,會自動的重設狀態列。我沒有具體研究,但我認同這一點,這必然是某處做了此類處理。

那為什麼會有延時呢?我猜測這應該是自動重置狀態列的時機導致的。我嘗試增加了一個註冊頁面(由登入頁面點選按鈕進入),並設定狀態列為紅色。然後,我在登入頁面監聽了 willFocusdidFocus 事件,分別在事件的處理函式中,將狀態列設定為白色。結果是,在 willFocus 中處理是我們期望的結果,而 didFocus 中處理和預設不處理時是一樣的。

React Native 中的狀態列

到這裡,我們基本可以得出一個結論:如果我們要在 app 中調整狀態列,穩妥的做法是在每一個頁面 willFocus 時設定其相應的狀態列,除非能確保前一個頁面的狀態列和自身相同。

因為這個功能十分通用,所以我們可以通過一個高階元件來完成這件事:

import React from 'react'
import hoistNonReactStatics from 'hoist-non-react-statics'
import { StatusBar } from 'react-native'

import { isAndroid } from '../../utils/device'

export const setStatusBar = (statusbarProps = {}) => WrappedComponent => {
  class Component extends React.PureComponent {
    constructor(props) {
      super(props)
      this._navListener = props.navigation.addListener('willFocus', this._setStatusBar)
    }

    componentWillUnmount() {
      this._navListener.remove();
    }

    _setStatusBar = () => {
      const {
        barStyle = "dark-content",
        backgroundColor = '#fff',
        translucent = false
      } = statusbarProps
      StatusBar.setBarStyle(barStyle)
      if (isAndroid()) {
        StatusBar.setTranslucent(translucent)
        StatusBar.setBackgroundColor(backgroundColor);
      }
    }

    render() {
      return <WrappedComponent {...this.props} />
    }
  }

  return hoistNonReactStatics(Component, WrappedComponent);
}
複製程式碼

通過裝飾器的方式使用也十分簡單:

@setStatusBar({
  barStyle: 'light-content',
  translucent: true,
  backgroundColor: 'transparent'
})
export default class Home extends React.PureComponent {
 ... 
}
複製程式碼

設定 Android 全屏且狀態列透明

除了在 js 層通過 StatusBar 元件設定狀態列的顏色、半透明等,我們也可以先將 Android 的狀態列設定為全屏且狀態列透明,這樣 Android 的表現就和 iOS 一樣,可以統一的去處理。

MainActivity.java 中新增下面的程式碼,可以設定全屏且狀態列透明:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    View decorView = getWindow().getDecorView();
    decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
    if (Build.VERSION.SDK_INT >= 21) {
        getWindow().setStatusBarColor(Color.TRANSPARENT);
    }
}
複製程式碼

設定完成後的效果如下圖(沒有處理 paddingTop)。

React Native 中的狀態列

現在 Android 狀態列的表現就和 iOS 一樣了,處理的時候統一按照 iOS 的處理邏輯即可,只是在 Header 的高度以及 paddingTop 的計算上不同。

此外,還需要注意 react-native 的 Header 沒有處理 Android 全屏的情況,因此我們需要在 Android 平臺下修改 headerStyle:

defaultNavigationOptions: {
  headerStyle: {
    ...Platform.OS === 'android' && {
      height: StatusBar.currentHeight + 44,
      paddingTop: StatusBar.currentHeight
    }
  }
}
複製程式碼

總結

React Native 中想要讓狀態列表現得更好還是需要做一些工作的。現在看來其實使用 StatusBar 元件更加的容易一點,因為即使在 Android 原生層面設定了全屏和透明狀態列,最後還是需要根據頁面去設定狀態列內容的顏色,所以還不許統一的在 js 層去做,通過高階元件的方式也不是很麻煩。

相關文章