整個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()
componentDidMount() {
console.log('StatusBar componentDidMount',this.props)
this._stackEntry = StatusBar.pushStackEntry(this.props);
}
<Status>標籤內配置的props屬性,會被push到StatusBar靜態類下的_propsStack中,並返回引用到_stachEntry中;
- StatusBar.pushStackEntry(this.props)
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
。懶得看了,先這麼解決了。