關於使用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。懶得看了,先這麼解決了。

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