关于使用react-native-router-flux构建了具有tab的主页,在使用StatusBar配置不同barStyle和backgroundColor时,状态栏展示不对的问题

整个RN工程使用了react-native-router-flux来做路由管理;
应用的主界面底部使用了tab进行分页。

					<Scene
                        key="Tabbar"
                        hideNavBar={true}
                        name="Tabbar"
                        showLabel={false}
                        lazy={false}
                        tabs={true}
                        tabBarOnPress={tabBarOnPress}
                        panHandlers={null}
                        tabBarStyle={tabBarStyle.tabbar}>

                        {/* 首页 */}
                        <Scene
                            key="Home"
                            component={Home}
                            initial={true}
                            hideNavBar={true}
                            tabBarLabel="首页"
                            icon={TabIcon}
                            iconImg={require('../assets/image/tabbar/tabbar_home_icon.png')}
                            iconActiveImg={require('../assets/image/tabbar/tabbar_home_icon_active.png')}
                        />
                        {/* 发布 */}
                        <Scene
                            key="Publish"
                            component={Publish}
                            hideNavBar={true}
                            tabBarLabel="发布"
                            icon={PlusIcon}
                            iconImg={require('../assets/image/tabbar/tabbar_plus_icon.png')}
                            iconActiveImg={require('../assets/image/tabbar/tabbar_plus_icon.png')}
                        />

                        {/* 个人中心 */}
                        <Scene
                            key="Account"
                            component={Account}
                            hideNavBar={true}
                            tabBarLabel="我的"
                            icon={TabIcon}
                            iconImg={require('../assets/image/tabbar/tabbar_account_icon.png')}
                            iconActiveImg={require('../assets/image/tabbar/tabbar_account_icon_active.png')}
                        />
                    </Scene>

然后Home页面和Account页面希望状态栏使用不同的颜色风格;
Home.js里面如下

 <Container style={[styles.container]} iosBarStyle="light-content" androidStatusBarColor="#65D3BA">
 ...
 </Container>

Account.js里面如下

<Container style={styles.container} iosBarStyle="dark-content" androidStatusBarColor="#ff0">
...
</Container>

这里的Container是简单封装了一下StatusBar 和 SafeAreaView。大致如下:


import { Container as NBContainer } from 'native-base';
...
	render() {
        const {
            androidStatusBarColor,
            iosBarStyle,
            style,
            transparent,
            translucent,
        } = this.props;

        return (
            <NBContainer /* style={style} */>
                <StatusBar
                    backgroundColor={androidStatusBarColor}
                    barStyle={iosBarStyle}
                    translucent={transparent ? true : translucent}
                />
                <SafeAreaView
                    style={[style]}
                >
                    <View ref={c => (this._root = c)} style={{ flex: 1 }} >
                        {this.props.children}
                    </View>
                </SafeAreaView>
            </NBContainer>
        );
    }

以IOS端为例,主页底部栏从左到右对应Home、Publish、Account页面,Home使用light-content的barStyle,Account使用了dark-content的barStyle。
理论上app启动后,初始化是进入Home页面,状态栏应该是light-content风格;但是设备上显示的是dark-content风格;
为了修正这个问题,尝试在Home的DidMount里面重新修改BarStyle,

	componentDidMount() {
        setTimeout(() => {
            Platform.OS === 'ios' ? StatusBar.setBarStyle('light-content') : StatusBar.setBackgroundColor('#65D3BA');
        }, 2000);
    }

这样的话,app启动后Home页面展示的确实是light-content风格。
但是如果此时在Home页面里,跳转到一个新的场景页面:

	Actions.AnotherPage()

不管AnotherPage里面如何设置barStyle,在退出这个AnotherPage()回到应用的主场景,不管是Home\Publish\Account页面,此时的barStyle只会是Account对应的dark-content风格。
这样感觉就有点奇怪了,明明我在Home的DidMount里面重新修改了barStyle为light-content,AnotherPage在退出以后,app的statusBar应该重置到我最后一次修改的light-content上。

现在只能看StatusBar的源码里到底是如何处理的了。

	react-native/Libraries/Components/StatusBar.js;
/**
   * Set the status bar style
   * @param style Status bar style to set
   * @param animated Animate the style change.
   */
  static setBarStyle(style: StatusBarStyle, animated?: boolean) {
    animated = animated || false;
    StatusBar._defaultProps.barStyle.value = style;
    if (Platform.OS === 'ios') {
      console.log('StatusBar ios setBarStyle _defaultProps', style)
      NativeStatusBarManagerIOS.setStyle(style, animated);
    } else if (Platform.OS === 'android') {
      NativeStatusBarManagerAndroid.setStyle(style);
    }
  }
/**
   * Set the background color for the status bar
   * @param color Background color.
   * @param animated Animate the style change.
   */
  static setBackgroundColor(color: string, animated?: boolean) {
    if (Platform.OS !== 'android') {
      console.warn('`setBackgroundColor` is only available on Android');
      return;
    }
    animated = animated || false;
    StatusBar._defaultProps.backgroundColor.value = color;

    const processedColor = processColor(color);
    if (processedColor == null) {
      console.warn(
        `\`StatusBar.setBackgroundColor\`: Color ${color} parsed to null or undefined`,
      );
      return;
    }
    console.log('StatusBar android setBackgroundColor _defaultProps', color)
    NativeStatusBarManagerAndroid.setColor(processedColor, animated);
  }
componentDidMount() {
  // Every time a StatusBar component is mounted, we push it's prop to a stack
  // and always update the native status bar with the props from the top of then
  // stack. This allows having multiple StatusBar components and the one that is
  // added last or is deeper in the view hierarchy will have priority.
  this._stackEntry = StatusBar.pushStackEntry(this.props);
}

componentWillUnmount() {
  // When a StatusBar is unmounted, remove itself from the stack and update
  // the native bar with the next props.
  StatusBar.popStackEntry(this._stackEntry);
}


/**
   * Push a StatusBar entry onto the stack.
   * The return value should be passed to `popStackEntry` when complete.
   *
   * @param props Object containing the StatusBar props to use in the stack entry.
   */
  static pushStackEntry(props: any): any {
    const entry = createStackEntry(props);
    StatusBar._propsStack.push(entry);
    StatusBar._updatePropsStack();
    return entry;
  }

  /**
   * Pop a StatusBar entry from the stack.
   *
   * @param entry Entry returned from `pushStackEntry`.
   */
  static popStackEntry(entry: any) {
    const index = StatusBar._propsStack.indexOf(entry);
    if (index !== -1) {
      StatusBar._propsStack.splice(index, 1);
    }
    StatusBar._updatePropsStack();
  }

/**
   * Updates the native status bar with the props from the stack.
   */
  static _updatePropsStack = () => {
  // Send the update to the native module only once at the end of the frame.
  clearImmediate(StatusBar._updateImmediate);
  StatusBar._updateImmediate = setImmediate(() => {
    console.log('_propsStack', StatusBar._propsStack.length, StatusBar._propsStack)
    console.log('_defaultProps', StatusBar._defaultProps)
    const oldProps = StatusBar._currentValues;
    const mergedProps = mergePropsStack(
      StatusBar._propsStack,
      StatusBar._defaultProps,
    );

    console.log('oldProps', StatusBar._currentValues)
    console.log('mergedProps', mergedProps)
    // Update the props that have changed using the merged values from the props stack.
    if (Platform.OS === 'ios') {
      if (
        !oldProps ||
        oldProps.barStyle.value !== mergedProps.barStyle.value
      ) {
        oldProps && console.log('oldProps.barStyle', oldProps.barStyle.value)
        console.log('mergedProps.barStyle', mergedProps.barStyle.value)
        NativeStatusBarManagerIOS.setStyle(
          mergedProps.barStyle.value,
          mergedProps.barStyle.animated || false,
        );
      }
      if (!oldProps || oldProps.hidden.value !== mergedProps.hidden.value) {
        NativeStatusBarManagerIOS.setHidden(
          mergedProps.hidden.value,
          mergedProps.hidden.animated
            ? mergedProps.hidden.transition
            : 'none',
        );
      }

      if (
        !oldProps ||
        oldProps.networkActivityIndicatorVisible !==
        mergedProps.networkActivityIndicatorVisible
      ) {
        NativeStatusBarManagerIOS.setNetworkActivityIndicatorVisible(
          mergedProps.networkActivityIndicatorVisible,
        );
      }
    } else if (Platform.OS === 'android') {
      if (
        !oldProps ||
        oldProps.barStyle.value !== mergedProps.barStyle.value
      ) {
        NativeStatusBarManagerAndroid.setStyle(mergedProps.barStyle.value);
      }
      if (
        !oldProps ||
        oldProps.backgroundColor.value !== mergedProps.backgroundColor.value
      ) {

        oldProps && console.log('oldProps.backgroundColor', oldProps.backgroundColor.value)
        console.log('mergedProps.backgroundColor', mergedProps.backgroundColor.value)
        const processedColor = processColor(
          mergedProps.backgroundColor.value,
        );
        if (processedColor == null) {
          console.warn(
            `\`StatusBar._updatePropsStack\`: Color ${
            mergedProps.backgroundColor.value
            } parsed to null or undefined`,
          );
        } else {
          NativeStatusBarManagerAndroid.setColor(
            processedColor,
            mergedProps.backgroundColor.animated,
          );
        }
      }
      if (!oldProps || oldProps.hidden.value !== mergedProps.hidden.value) {
        NativeStatusBarManagerAndroid.setHidden(mergedProps.hidden.value);
      }
      if (!oldProps || oldProps.translucent !== mergedProps.translucent) {
        NativeStatusBarManagerAndroid.setTranslucent(mergedProps.translucent);
      }
    }
    // Update the current prop values.
    StatusBar._currentValues = mergedProps;
  });
};

可以在每个函数入口处加下log,看下执行流程;
可以明确如果页面的render里面主动的配置了StatusBar,比如:

render() {
       
        return (
            <>
                <StatusBar barStyle={'light-content'} />
                <SafeAreaView style={[styles.container, { backgroundColor: '#65D3BA' }]} >
                   ...
                </SafeAreaView >
            </>
        );
    }

就会按照下面的流程执行:

  1. componentDidMount()
	componentDidMount() {
  		console.log('StatusBar componentDidMount',this.props)
  		this._stackEntry = StatusBar.pushStackEntry(this.props);
	}
<Status>标签内配置的props属性,会被push到StatusBar静态类下的_propsStack中,并返回引用到_stachEntry中;
  1. StatusBar.pushStackEntry(this.props)
	static pushStackEntry(props: any): any {
    	const entry = createStackEntry(props);
    	StatusBar._propsStack.push(entry);
    	StatusBar._updatePropsStack();
    	return entry;
  	}
上面传进来的props会被二次封装下,配置成一个属性比较完整的entry存放在_propsStack中,
  1. StatusBar._updatePropsStack()

    以IOS端为例,
    
	const oldProps = StatusBar._currentValues;
    const mergedProps = mergePropsStack(
      	StatusBar._propsStack,
      	StatusBar._defaultProps,
    );
    if (Platform.OS === 'ios') {
      if (
        !oldProps ||
        oldProps.barStyle.value !== mergedProps.barStyle.value
      ) {
        oldProps && console.log('oldProps.barStyle', oldProps.barStyle.value)
        console.log('mergedProps.barStyle', mergedProps.barStyle.value)
        NativeStatusBarManagerIOS.setStyle(
          mergedProps.barStyle.value,
          mergedProps.barStyle.animated || false,
        );
      }
   }
   ... 
   StatusBar._currentValues = mergedProps;
首先oldProp就是StatusBar._currentValues,mergedProps是待更新的StatusBar的属性,更新完成后也会被赋值到StatusBar._currentValues中,
看下如何计算出mergedProps:
/**
 * Merges the prop stack with the default values.
 */
function mergePropsStack(
  propsStack: Array<Object>,
  defaultValues: Object,
): Object {
  return propsStack.reduce((prev, cur) => {
    for (const prop in cur) {
      if (cur[prop] != null) {
        prev[prop] = cur[prop];
      }
    }
    return prev;
  }, Object.assign({}, defaultValues));
}

具体操作就是默认使用StatusBar._defaultProps,如果StatusBar._propsStack有push进新的props的话,就使用stack中最上面的一个props;
然后判断oldProp和mergedProps中的barStyle是否相同,只有不相同的时候,才会调用

	NativeStatusBarManagerIOS.setStyle(
          mergedProps.barStyle.value,
          mergedProps.barStyle.animated || false,
        );

这样配置了StatusBar的新页面状态栏会更新成指定的风格;

页面退出时的执行流程

1. componentWillUnmount

componentWillUnmount() {
  // When a StatusBar is unmounted, remove itself from the stack and update
  // the native bar with the next props.
  StatusBar.popStackEntry(this._stackEntry);
}
this._stackEntry是之前pushStackEntry返回的props对象;

2. StatusBar.popStackEntry(this._stackEntry)

static popStackEntry(entry: any) {
    const index = StatusBar._propsStack.indexOf(entry);
    if (index !== -1) {
      StatusBar._propsStack.splice(index, 1);
    }
    StatusBar._updatePropsStack();
  }
此处就是在当前的StatusBar._propsStack中找到entry对应的索引,并移除,然后更新props
  1. StatusBar._updatePropsStack();

    已经介绍过了 ,不再赘述。
    

至此,通过页面内配置<StatusBar>的的方式设置barStyle的流程梳理完了。

回到Home的DidMount中,我调用了StatusBar.setBarStyle('light-content')来修正;
看下StatusBar.setBarStyle具体做了什么

  /**
   * Set the status bar style
   * @param style Status bar style to set
   * @param animated Animate the style change.
   */
  static setBarStyle(style: StatusBarStyle, animated?: boolean) {
    animated = animated || false;
    StatusBar._defaultProps.barStyle.value = style;
    if (Platform.OS === 'ios') {
      // console.log('StatusBar ios setBarStyle _defaultProps', style)
      NativeStatusBarManagerIOS.setStyle(style, animated);
    } else if (Platform.OS === 'android') {
      NativeStatusBarManagerAndroid.setStyle(style);
    }
  }

这里比较简单,只是修正了一下StatusBar._defaultProps.barStyle的值
然后就直接调用native接口生效了。

	NativeStatusBarManagerIOS.setStyle(style, animated);

对比一下两种方式,就可以发现其中的差异;

通过js接口修改barStyle,值会保存在StatusBar._defaultProps中;
通过<StatusBar>标签属性配置barStyle,值会push到StatusBar._propsStack堆栈的栈顶,然后通过mergePropsStack来取值
mergePropsStack的规则是如果_propsStack中没有值就取_defaultProps,如果_propsStack有值,就取_propsStack栈顶的props

通过log,将启动时候的几个关键方法中的参数打印出来:

[Wed Jun 24 2020 19:40:29.771]  LOG      Running "BaseApp" with {"rootTag":41,"initialProps":{}}
[Wed Jun 24 2020 19:40:29.771]  LOG      Home componentWillMount ios
[Wed Jun 24 2020 19:40:29.772]  LOG      Account render account {}
[Wed Jun 24 2020 19:40:29.773]  LOG      StatusBar componentDidMount {"animated": false, "backgroundColor": "#65D3BA", "barStyle": "light-content", "showHideTransition": "fade", "translucent": undefined}
[Wed Jun 24 2020 19:40:29.773]  LOG      StatusBar componentDidMount {"animated": false, "backgroundColor": "#ff0", "barStyle": "dark-content", "showHideTransition": "fade", "translucent": undefined}
[Wed Jun 24 2020 19:40:29.773]  LOG      Account componentDidMount
[Wed Jun 24 2020 19:40:29.774]  LOG      _propsStack 2 [{"backgroundColor": {"animated": false, "value": "#65D3BA"}, "barStyle": {"animated": false, "value": "light-content"}, "hidden": null, "networkActivityIndicatorVisible": undefined, "translucent": undefined}, {"backgroundColor": {"animated": false, "value": "#ff0"}, "barStyle": {"animated": false, "value": "dark-content"}, "hidden": null, "networkActivityIndicatorVisible": undefined, "translucent": undefined}]
[Wed Jun 24 2020 19:40:29.774]  LOG      _defaultProps {"backgroundColor": {"animated": false, "value": "black"}, "barStyle": {"animated": false, "value": "default"}, "hidden": {"animated": false, "transition": "fade", "value": false}, "networkActivityIndicatorVisible": false, "translucent": false}
[Wed Jun 24 2020 19:40:29.774]  LOG      oldProps null
[Wed Jun 24 2020 19:40:29.775]  LOG      mergedProps.barStyle dark-content
[Wed Jun 24 2020 19:40:31.628]  LOG      StatusBar ios setBarStyle _defaultProps light-content

这里遇到的一个比较奇怪的问题就是启动时,同时加载了Home和Account页面,StatusBar里面的_propsStack中push进了两个页面中配置的props;

可以看到stack数组中第一个是light-content,第二个是dark-content,
oldProps是StatusBar._currentValues,初始的时候是null,
StatusBar._defaultProps初始的时候是配置了默认值的。为default;
这样计算出来的mergedProps.barStyle 为dark-content,即stack的栈顶props;

也就是说这种情况下,app虽然展示的是Home页面,但是StatusBar的风格是最后一个加载的Account页面中配置的dark-content风格;
然后Home的DidMount中主动设置了StatusBar.setBarStyle('light-content')

log中对应的修改了_defaultProps为 light-content,可以看下时间戳。

这样app启动后,展示的即是Home页面,且StatusBar是对应的light-content风格。

1、然后我们发现在切换tab,在Home和Account页面来回切换,没有任何log输出了。也就是说通过react-native-router-flux配置的带tab的主场景</Scene>,在来回切换的时候,不会触发StatusBar的componentDidMount,也就不会更新StatusBar._propsStack
2、那么此时如果打开一个新的使用<StatusBar>标签的AnotherPage页面,StatusBar._propsStack栈顶会加入一个新的props,然后通过mergedProps(),会取值到栈顶的props,使新加入的props配置生效;
3、AnotherPage退出的时候,StatusBar._propsStackpop掉新加入的props,重新进行进行mergedProps(),比较StatusBar._propsStackStatusBar._defaultProps会取值到stack栈顶props,也就是Account对应的配置,使之生效。

这样就解释了Home启动后,状态栏修正为light风格,但是打开一个新的页面并退出,虽然页面是Home但是状态栏变成了dark风格的原因。

个人感觉原因就是通过接口形式StatusBar.setBarStyle()配置StatusBar,要比通过标签形式配置,权重要低。另外就是react-native-router-flux中页面切换不修改stack导致的。

解决方法:

对于这种方式配置的主页面,
1、使用StatusBar.pushStackEntry来替代StatusBar.setBarStyle,这样的话,StatusBar的属性控制完全都在StatusBar._propsStack中了,
2、在Home和Account页面来回切换的时候,主动修改StatusBar._propsStack,放弃默认加载生成的StatusBar._propsStack

修改后的代码如下
Home.js

componentWillMount() {
        console.log('Home componentWillMount', Platform.OS);

        this.didFocusListener = this.props.navigation.addListener(
            'didFocus',
            (obj) => {
                console.log('Home focus');
                setTimeout(() => {
                    let statusParams = {
                        barStyle: 'light-content',
                        backgroundColor: '#65D3BA',
                        animated: false,        // 必须指定这个参数,否则android端会报错
                        showHideTransition: 'fade', // 非必须指定这个参数,但是StatusBar里面默认是fade,最好也带上。
                    };
                    this.statusStyle = StatusBar.pushStackEntry(statusParams);
                }, 100);	//加了延迟,主要是didBlur要比didFocus早触发;
            }
        );
        this.didBlurListener = this.props.navigation.addListener(
            'didBlur',
            (obj) => {
                console.log('Home blur');
                if (this.statusStyle) {
                    StatusBar.popStackEntry(this.statusStyle);
                }
            }
        );
    }
    componentWillUnmount() {
        this.didFocusListener.remove();
        this.didBlurListener.remove();
    }

Account.js

componentWillMount() {
        this.didFocusListener = this.props.navigation.addListener(
            'didFocus',
            (obj) => {
                console.log('Account focus');
                setTimeout(() => {
                    let statusParams = {
                        barStyle: 'dark-content',
                        backgroundColor: '#ff0',
                        animated: false,        // 必须指定这个参数,否则android端会报错
                        showHideTransition: 'fade', // 非必须指定这个参数,但是StatusBar里面默认是fade,最好也带上。
                    };
                    this.statusStyle = StatusBar.pushStackEntry(statusParams);
                }, 100);
            }
        );
        this.didBlurListener = this.props.navigation.addListener(
            'didBlur',
            (obj) => {
                console.log('Account blur');
                if (this.statusStyle) {
                    StatusBar.popStackEntry(this.statusStyle);
                }
            }
        );
    }

    componentWillUnmount() {
        console.log('Account componentWillUnmount');
        this.didFocusListener.remove();
        this.didBlurListener.remove();
    }

至于不在主场景<Scene key="Tabbar">下的其他的<Scene>就不需要使用这种方式了,正常配置<StatusBar>标签即可正常切换状态栏

其实最正确的解决方式应该还是在react-native-router-flux中解决为啥tab形式的Scene页面切换的时候不修正StatusBar._propsStack。懒得看了,先这么解决了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章