整个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 >
</>
);
}
就会按照下面的流程执行:
componentDidMount() {
console.log('StatusBar componentDidMount',this.props)
this._stackEntry = StatusBar.pushStackEntry(this.props);
}
<Status>标签内配置的props属性,会被push到StatusBar静态类下的_propsStack中,并返回引用到_stachEntry中;
static pushStackEntry(props: any): any {
const entry = createStackEntry(props);
StatusBar._propsStack.push(entry);
StatusBar._updatePropsStack();
return entry;
}
上面传进来的props会被二次封装下,配置成一个属性比较完整的entry存放在_propsStack中,
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
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._propsStack
pop掉新加入的props,重新进行进行mergedProps()
,比较StatusBar._propsStack
和StatusBar._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
。懒得看了,先这么解决了。