React Native爬坑之StatusBar(沉浸式状态栏的实现)

最近在用RN开发一款钱包应用的时候,对于如何实现沉浸式状态栏着实苦恼了一番,后来才发现实现起来还是比较简单的,只要利用好官方提供的StatusBar组件就好了。话不多说,切入正题:

一.StatusBar组件属性介绍以及实现思路

StatusBar组件是RN官方封装好的用于控制状态栏的组件。在实现沉浸式状态栏过程中,主要是四个属性,这里大致介绍一下:
(1)translucent:这个属性仅针对安卓平台有效,设置该属性为true将会使页面的渲染延伸至StatusBar的区域。
(2)barStyle:设置StatusBar的文本颜色(light-content:白字黑底;dark-content:黑字白底)
(3)backgroundColor:设置StatusBar的背景颜色;
(4)StatusBar.currentHeight:获取StatusBar的高度;

以上就是将要用到的四个属性。

具体实现思路:让页面绘制在StatusBar的区域下,同时获取StatusBar的高度,将导航栏(头部组件)刚好撑到状态栏的下方。依据实现思路,我们可以构造如下StatusBarComp组件。

代码如下:

import React, { Component } from "react";
import { View } from "react-native";
import { StatusBar, Platform } from "react-native";

export default class StatusBarComp extends Component {
    constructor(props) {
        super(props);
        this.state = {
            isLightStyle: this.props.isLightStyle ? true : false
        }
    }

    render() {
        const STATUS_BAR_HEIGHT = StatusBar.currentHeight;
        const { isLightStyle } = this.state;

        if (Platform.OS === "android") {
            return (
                <View style={{height: STATUS_BAR_HEIGHT}}>
                    <StatusBar translucent={true} backgroundColor="transparent" barStyle={isLightStyle ? "light-content" : "dark-content"} />
                </View>
            )
        } else {
            // ios情况不做处理(StatusBar)
            return null;
        }
    }
}

然后,在所有的页面组件中,引入并使用该组件即可。还可以根据需要传入isLightStyle属性,来决定状态栏具体显示何种效果的文本。(具体到应用中,最好根据实际情况,把以上组件和头部导航栏组件封装成同一个组件使用。)

二.坑

原理很简单,接下来谈一谈实际遇到的一些坑。

1.Modal组件并不会覆盖状态栏区域

不知为何,在设置好StatusBar组件之后,如果在应用的页面中存在Modal组件,当Modal组件弹出时候,状态栏并不会被覆盖,具体效果如下:

遇到问题,当然是第一时间上StackOverFlow啦,果然就找到了:How to hide the statusBar when react-native modal shown?。据说这是React Native官方的一个问题,具体可以查看此issue

废话不多说了,实际上具体的解决方法就在该issue下的某个回答中,在项目中使用一个库修复就好了。这个库就是react-native-modal-translucent。完美的解决了问题。效果如下:

2.某些安卓手机沉浸式导航栏会在用户按下物理返回按键再次进入页面时失效

在我进行真机调试(红米note5)的过程中发现了该问题,只要是应用中当前路由栈顶端的页面(即用户按下物理返回键会退出应用的页面)退出,再次进入时,设置好的StatusBar会完全失效,效果也非常难看。

后来这个问题在https://segmentfault.com/a/1190000016601700#articleHeader11找到了答案。作者fantasy525在小米6的手机上也同样出现了这个问题,经过他对RN中StatusBarModule.java代码的分析,发现在设置StatusBar时,会有一个对于activity是否存在的判断(这个很好理解,如果当前应用的activity不存在,StatusBar应该设置为系统的状态栏样式)。而RN只有在mainActivity执行到onResume生命周期时,才会在getCurrentActivity中返回activity为mainActivity(说明此前activity都是null,如果rn代码在onCreate生命周期方法中就开始执行,极有可能在调用StatusBar模块的代码时,activity实例还没有返回,导致设置的状态栏是系统样式)。

于是针对该问题,想到如下解决方法,在按下返回键即会退出应用的页面的componentDidMount生命周期方法中通过setTimeout设置一个延迟,保证在rn代码获取到activity实例后再设置StatuBar的状态。代码示例如下:

还是在之前的StatusBarComp.js文件中,导出如下方法:

export function fixStatusBar() {
    StatusBar.setTranslucent(true);
    StatusBar.setBackgroundColor("transparent");
}

接着再对应的页面的componentDidMount()中,添加:

this.timeoutResetStatus = setTimeout(() => {
	fixStatusBar();
}, 10);

最后别忘了在componentWillUnmount中清空定时器。

3.TabNavigator的Tab标签切换过程中,StatusBar不会改变

这个问题也是十分的让人恼火,大致描述就是,在使用react-navigation提供的Tab路由时,即使设置好了StatusBar的显示样式(light-content还是dark-content)。在切换的过程中,并没有用。有人也在react-navigation的官方issue中提出了该问题:StatusBar doesn’t update with TabNavigator

下面grabbou的回答将该问题关联到了另一个issue:screenIsActive prop / componentDidFocus event for TabNavigator items。该issue中,主要讨论在设置好Tab路由后,Tab标签页之间切换时候,如何才能正确的获取当前tab页的路由标识(名称)。当时有人提出应当新增这几个事件:

(1)willFocus:Tab页面将获取焦点;(用户手指滑动进入该页)
(2)didFocus:页面已获取到焦点;
(3)willBlur:页面将失去焦点;用户手指滑动离开该页)
(4)didBlur:页面已失去焦点;

这些事件现如今也可以在react-navigation文档中找到(说明此时已经添加)。

说了那么多,和我们的问题有什么关系呢?实际上,这个问题的解决方式就要通过监听这些事件来完成。为了防止在Tab页切换的过程中,StatusBar的显示样式发生莫名其妙的改变。可以在页面组件的componentWillMount生命周期方法中监听willFocus事件,然后再动态的设置StatusBar的状态为你所希望的样式。

首先,依旧是StatusBarComp.js文件中,添加一个工具方法,代码如下:

export function setStatusBar(isDarkStyle = true) {
    StatusBar.setTranslucent(true);
    StatusBar.setBackgroundColor("transparent");
    if (isDarkStyle) {
        StatusBar.setBarStyle("dark-content");
    } else {
        StatusBar.setBarStyle("light-content");
    }
}

然后在Tab路由的子页面的componentWillMount()方法中设置监听器:

this.listenStatusBar = this.props.navigation.addListener("willFocus", payload => {
	setStatusBar(false);
});

之后,在componentWillUnmount中移除监听器即可:

this.listenStatusBar.remove();

4.部分页面StatusBar背景为默认颜色(浅灰色)

例如:

这个问题比较好解决,在原组件的基础上,添加一个字段setBgColor,设置状态栏背景颜色即可,以下贴出StatusBarComp.js的完整代码:

import React, { Component } from "react";
import { View } from "react-native";
import { StatusBar, Platform } from "react-native";

export const STATUS_BAR_HEIGHT = StatusBar.currentHeight;

export function fixStatusBar() {
    StatusBar.setTranslucent(true);
    StatusBar.setBackgroundColor("transparent");
}

export function setStatusBar(isDarkStyle = true) {
    StatusBar.setTranslucent(true);
    StatusBar.setBackgroundColor("transparent");
    if (isDarkStyle) {
        StatusBar.setBarStyle("dark-content");
    } else {
        StatusBar.setBarStyle("light-content");
    }
}

export default class StatusBarComp extends Component {
    constructor(props) {
        super(props);
        this.state = {
            isLightStyle: this.props.isLightStyle ? true : false,
            setBgColor: this.props.setBgColor ? this.props.setBgColor : "transparent"
        }
    }

    render() {
        const STATUS_BAR_HEIGHT = StatusBar.currentHeight;
        const { isLightStyle, setBgColor } = this.state;

        if (Platform.OS === "android") {
            return (
                <View style={{height: STATUS_BAR_HEIGHT, backgroundColor: setBgColor}}>
                    <StatusBar translucent={true} backgroundColor="transparent" barStyle={isLightStyle ? "light-content" : "dark-content"} />
                </View>
            )
        } else {
            // ios情况不做处理(StatusBar)
            return null;
        }
    }
}

设置颜色:

<StatusBarComp setBgColor="#fff"/>

5.APP启动页(Launch Screen)如何隐藏状态栏

通过前面的步骤,基本上能够解决大部分在设置沉浸式导航栏时所出现的问题。那么最后一个问题就是,我们的启动页如何设置隐藏状态栏。启动页(图片)是放到android对应资源文件夹下的,用于应用启动加载资源的过程中显示给用户,避免出现白屏。此时,我们的js部分代码都还没有或者正在执行,所以这里设置是没有用。

不过,使用RN往往会用到react-native-splash-screen来配置启动页,这样隐藏状态栏就很简单了,代码如下:

public class MainActivity extends ReactActivity {

    /**
     * Returns the name of the main component registered from JavaScript.
     * This is used to schedule rendering of the component.
     */
    @Override
    protected String getMainComponentName() {
        return "MDWallet";
    }
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        SplashScreen.show(this, true);  		//<==这里的第二个参数设置为true即可
        super.onCreate(savedInstanceState);
        JPushInterface.init(this);
        // JPushInterface.onPause(this);
    }

    @Override
    protected void onPause() {
        super.onPause();
        JPushInterface.onPause(this);
    }

    @Override
    protected void onResume() {
        super.onResume();
        JPushInterface.onResume(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
    }
}

附效果图:

done!

发表评论

电子邮件地址不会被公开。 必填项已用*标注