React Native 中的狀態欄(Android沉浸式)

在實際項目中,咱們經常須要根據頁面的不一樣去修改狀態欄的表現。例如:頁面頭部圖片延伸到狀態欄下而且狀態欄透明;狀態欄的顏色和標題欄的顏色相同;狀態欄內容顏色的深淺變化。
在此以前,我寫了一篇React Navigation 構建 Android 和 iOS 統一的 UI的文章,裏面簡單的說到了 Android 狀態欄的一些設置。後來我發現並非我想的那麼簡單,所以經過這篇博客進行補充,文中會更加詳細的介紹狀態欄相關的內容以及 React Native 項目中如何去控制狀態欄,使應用在 iOS 和 Android 平臺上都具備很好的表現。java

示例代碼我上傳到了 GitHub:https://github.com/hezhii/rn_...react

預備知識

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

iOS 中的狀態欄

在 iOS 中,頁面默認全屏(狀態欄不佔空間),狀態欄內容默認是深色。由於頁面全屏,因此若是咱們不進行處理,內容會跑到狀態欄下面去。同時,因爲 iPhone X 等劉海屏手機的出現,致使狀態欄的高度發生了變化,由以前的 20 變成了 34。爲了解決此問題,咱們能夠手動給頂部組件設置 paddingTop(值根據機型判斷),或者使用 SafeAreaView 組件。ios

在 iOS 中咱們只用處理好安全區域的問題,而後根據頁面的不一樣去設置內容的顏色深淺便可。git

Android 中的狀態欄

在 Android 中,頁面默認非全屏(狀態欄佔空間),狀態欄內容默認是淺色github

Android 中對狀態欄的支持經歷了幾個版本:面試

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

其中,若是想要設置狀態欄顏色,則不能設置 FLAG_TRANSLUCENT_STATUSreact-native

在 Android 應用中,每一個 Activity 都對應一個狀態欄。這意味着,爲一個頁面設置狀態欄不會對其餘頁面的狀態欄形成影響。安全

React Native 中的狀態欄

React Native 官方提供了 StatusBar 組件用於控制狀態欄,支持設置內容深淺色,狀態欄背景(Android)等。性能優化

StatusBar 能夠同時添加多個,而屬性則會按照加載順序合併(後者覆蓋前者)。

不一樣於 Android 中的狀態欄,在 React Native 中狀態欄是公用的,任何一個地方修改狀態欄都會致使狀態欄發生變化,即便切換到了其餘未設置的頁面。所以,咱們須要在每一個頁面渲染時都設置一下相應的狀態欄,或是在離開設置了狀態欄的頁面時重置狀態欄。

實際案例

在瞭解了必要的知識後,讓咱們經過一個實際案例來看看咱們須要作什麼以及怎麼作才更好。

在這個案例中,有三個頁面:主頁,個人,登陸。其中「主頁」和「個人」是兩個標籤頁,「主頁」頭部有背景圖片,「個人」頁面頂部是藍色,「登陸」頁面頂部爲白色。頁面效果以下圖。

image

「主頁」和「個人」頁面使用自定義的 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

處理狀態欄的問題

從上圖的案例中,能夠發現如下幾點問題:

  • iOS 設備中,狀態欄內容的顏色顯示不正確,「主頁」和「個人」頁面狀態欄應該是淺色。
  • Android 設備中,「主頁」的狀態欄應該是透明的,而且圖片應該延伸到狀態欄下。
  • Android 設備中,「個人」頁面狀態欄顏色應該也是藍色。

爲了讓應用表現得更好,咱們須要根據頁面動態的調整狀態欄。React Native 爲開發者提供了 StatusBar 組件去控制狀態欄。

StatusBar 組件控制狀態欄

咱們在「主頁」中,設置狀態欄內容爲「淺色」,背景色爲透明,translucent 爲 true。而後,「主頁」和「個人」頁面的 Header 都添加 fullScreen 屬性。效果以下:

從圖中能夠看到,由於頁面路由是 js 層作的,整個應用對應一個 StatusBar,雖然「個人」和「登陸」頁面都沒有設置狀態欄,但狀態欄也是透明的。

這樣就有一個問題,「登陸」頁面其實使用默認效果便可,可是因爲其餘頁面設置了狀態欄,致使進入到「登陸」頁面時效果就不對了。因此,每一個頁面都須要設置相應的狀態欄,由於狀態欄可能被其餘頁面改變。

接下來,在「登陸」頁面設置狀態欄爲白色且內容爲深色:

<StatusBar translucent={false} backgroundColor='#fff' barStyle="dark-content" />

如今「登陸」頁面的效果就和指望的同樣了,當咱們從「登陸」頁面返回到主界面時,狀態欄會切換回以前的狀態,可是有一點延時。按照前面的經驗,當從登陸頁面回來時,狀態欄應該還是白色且內容深色。由於返回時,前面的頁面不會從新渲染,狀態欄應該會保持當前的狀態。可是狀態欄卻自動調整成了以前的狀態,雖然有一點延時。我在 react-navigation 的 GitHub issue 中發現有人提到,當離開 route時,會自動的重設狀態欄。我沒有具體研究,但我認同這一點,這必然是某處作了此類處理。

那爲何會有延時呢?我猜想這應該是自動重置狀態欄的時機致使的。我嘗試增長了一個註冊頁面(由登陸頁面點擊按鈕進入),並設置狀態欄爲紅色。而後,我在登陸頁面監聽了 willFocus 和 didFocus 事件,分別在事件的處理函數中,將狀態欄設置爲白色。結果是,在 willFocus 中處理是咱們指望的結果,而 didFocus 中處理和默認不處理時是同樣的。

到這裏,咱們基本能夠得出一個結論:若是咱們要在 app 中調整狀態欄,穩妥的作法是在每個頁面 willFocus 時設置其相應的狀態欄,除非能確保前一個頁面的狀態欄和自身相同

由於這個功能十分通用,因此咱們能夠經過一個高階組件來完成這件事:

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)。

如今 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 層去作,經過高階組件的方式也不是很麻煩。

更多資料分享歡迎Android工程師朋友們加入安卓開發技術進階互助:856328774免費提供安卓開發架構的資料(包括Fultter、高級UI、性能優化、架構師課程、 NDK、Kotlin、混合式開發(ReactNative+Weex)和一線互聯網公司關於Android面試的題目彙總。
相關文章
相關標籤/搜索