一行代碼搞定 Android平臺React Native Modal 沒法延伸到狀態欄的問題

背景介紹

最近在作react-native應用Android端沉浸式狀態欄時,發現經過Statusbar.setTrranslucent(ture)設置界面拉通狀態欄以後,使用Modal 組件的地方界面沒法延伸到狀態欄,致使使用Modal實現的彈窗背景蒙層頂部會有一個白條,看起來很不爽,在通過一番搜索以後,發現react-native github 上有人提這個問題,可是沒有解決。所以就只有找其餘方案來解決。javascript

最開始的想法是自定義一個組件來代替原生的Modal組件,可是項目裏面使用Modal的地方不少,替換起來也很麻煩。比較致命的一點是Modal組件的一些屬性是很差被替代的。好比:onRequestClose,在彈出Modal時,點擊物理返回鍵,會回調這個方法,基本上全部使用Modal的地方都會用它來作關閉彈窗,新的組件須要報保留這些屬性和功能。在網上搜到一篇文章[React Native] 還我靚靚 modal 彈窗,借鑑它的思路,最後完美解決。html

解決方案和思路

Q: 爲何react native提供的Modal組件Android平臺不能延伸到狀態欄?java

A:由於ModalAndroid 原生用Dialog 實現,Dialog 自己就不能衍生到statusbarreact

所以咱們改一下Modal原生的實現就行了。android

**解決方案: 就是更改Modal組件的原生代碼實現。從新提供一個Modal(就叫:TranslucentModal)組件給react native**端。ios

注意的問題:git

一、新的Modal組件和原來的modal 組件所暴露的屬性和方法要徹底同樣,這樣替換就很方便。github

二、在react-native作統一封裝,IOS平臺繼續使用react-native 提供的Modal組件,Android平臺使用TranslucentModalnpm

最終咱們只須要在使用Modal的頁面更改一下引用的就ok,真正的只須要修改一行代碼。react-native

import { Modal } from "react-native";
複製代碼

改成:

import Modal from 'react-native-translucent-modal';

複製代碼

效果圖

對比圖 使用RN原生的Modal 使用Translucent Modal
splash
image.png
image.png
pop
image.png
image.png

具體實現

一、原生端代碼更改

Modal組件Android端的實現類爲com.facebook.react.views.modal.ReactModalHostView.java,這個類是public的,所以咱們就能夠在咱們本身的項目下建立一個新類TranslucentModalHostView繼承自 ReactModalHostView,修改部分實現就行了,以下:

/** * React Native Modal(Android) 延伸到狀態欄 * 因爲React Native 提供的 Modal 組件不能延伸到狀態欄,所以,只有對原生{@link ReactModalHostView}實現修改。 */
public class TranslucentModalHostView extends ReactModalHostView {

    public TranslucentModalHostView(Context context) {
        super(context);
    }

    @Override
    protected void setOnShowListener(DialogInterface.OnShowListener listener) {
        super.setOnShowListener(listener);
    }

    @Override
    protected void setOnRequestCloseListener(OnRequestCloseListener listener) {
        super.setOnRequestCloseListener(listener);
    }

    @Override
    protected void setTransparent(boolean transparent) {
        super.setTransparent(transparent);
    }

    @Override
    protected void setHardwareAccelerated(boolean hardwareAccelerated) {
        super.setHardwareAccelerated(hardwareAccelerated);
    }

    @Override
    protected void setAnimationType(String animationType) {
        super.setAnimationType(animationType);
    }

    @Override
    protected void showOrUpdate() {
        super.showOrUpdate();
        Dialog dialog = getDialog();
        if (dialog != null) {
            setStatusBarTranslucent(dialog.getWindow(), true);
            setStatusBarColor(dialog.getWindow(), Color.TRANSPARENT);
            setStatusBarStyle(dialog.getWindow(), isDark());
        }
    }

    @TargetApi(23)
    private boolean isDark() {
        Activity activity = ((ReactContext) getContext()).getCurrentActivity();
        // fix activity NPE
        if (activity == null) {
            return true;
        }
        return (activity.getWindow().getDecorView().getSystemUiVisibility() & View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0;
    }

    public static void setStatusBarTranslucent(Window window, boolean translucent) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            View decorView = window.getDecorView();
            if (translucent) {
                decorView.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
                    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
                    @Override
                    public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
                        WindowInsets defaultInsets = v.onApplyWindowInsets(insets);
                        return defaultInsets.replaceSystemWindowInsets(
                                defaultInsets.getSystemWindowInsetLeft(),
                                0,
                                defaultInsets.getSystemWindowInsetRight(),
                                defaultInsets.getSystemWindowInsetBottom());
                    }
                });
            } else {
                decorView.setOnApplyWindowInsetsListener(null);
            }
            ViewCompat.requestApplyInsets(decorView);
        }
    }

    public static void setStatusBarColor(final Window window, int color) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
            window.setStatusBarColor(color);
        }
    }

    public static void setStatusBarStyle(Window window, boolean dark) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            View decorView = window.getDecorView();
            decorView.setSystemUiVisibility(
                    dark ? View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR : 0);
        }
    }
}
複製代碼

就這樣,功能就實現了,如今咱們須要把它以組件的形式提供給react native端,能夠看一下com.facebook.react.views.modal用到了以下幾個類:

image.png

它們的可見性都是包內訪問的,所以在咱們本身的包下訪問不了,所以,須要把這幾個類拷貝一份出來:

image.png

TranslucentReactModalHostManager 中換一下對應的名字就ok 了。

二、react native 端統一封裝

由於咱們提供的屬性要和原來的Modal組件保持一致,所以,咱們把原來的Modal.js文件拷貝一份出來改一下,把 ios 端的屬性和相關方法剔除掉,剩下Android 平臺的屬性相關就行了。最終以下,取名爲MFTranslucentModal.android.js:

const AppContainer = require('AppContainer');
const I18nManager = require('I18nManager');
const Platform = require('Platform');
const React = require('React');
const PropTypes = require('prop-types');
const StyleSheet = require('StyleSheet');
const View = require('View');

const requireNativeComponent = require('requireNativeComponent');

const RCTModalHostView = requireNativeComponent('RCTTranslucentModalHostView', null);


/** * The Modal component is a simple way to present content above an enclosing view. * * See https://facebook.github.io/react-native/docs/modal.html */

class Modal extends React.Component {
  static propTypes = {
    /** * The `animationType` prop controls how the modal animates. * * See https://facebook.github.io/react-native/docs/modal.html#animationtype */
    animationType: PropTypes.oneOf(['none', 'slide', 'fade']),
    /** * The `transparent` prop determines whether your modal will fill the * entire view. * * See https://facebook.github.io/react-native/docs/modal.html#transparent */
    transparent: PropTypes.bool,
    /** * The `hardwareAccelerated` prop controls whether to force hardware * acceleration for the underlying window. * * See https://facebook.github.io/react-native/docs/modal.html#hardwareaccelerated */
    hardwareAccelerated: PropTypes.bool,
    /** * The `visible` prop determines whether your modal is visible. * * See https://facebook.github.io/react-native/docs/modal.html#visible */
    visible: PropTypes.bool,
    /** * The `onRequestClose` callback is called when the user taps the hardware * back button on Android or the menu button on Apple TV. * * See https://facebook.github.io/react-native/docs/modal.html#onrequestclose */
    onRequestClose: (Platform.isTVOS || Platform.OS === 'android') ? PropTypes.func.isRequired : PropTypes.func,
    /** * The `onShow` prop allows passing a function that will be called once the * modal has been shown. * * See https://facebook.github.io/react-native/docs/modal.html#onshow */
    onShow: PropTypes.func,
  };

  static defaultProps = {
    visible: true,
    hardwareAccelerated: false,
  };

  static contextTypes = {
    rootTag: PropTypes.number,
  };


  render() {
    if (this.props.visible === false) {
      return null;
    }

    const containerStyles = {
      backgroundColor: this.props.transparent ? 'transparent' : 'white',
    };

    let animationType = this.props.animationType;
    if (!animationType) {
      // manually setting default prop here to keep support for the deprecated 'animated' prop
      animationType = 'none';
    }

    const innerChildren = __DEV__ ?
      (<AppContainer rootTag={this.context.rootTag}> {this.props.children} </AppContainer>) :
      this.props.children;

    return (
      <RCTModalHostView animationType={animationType} transparent={this.props.transparent} hardwareAccelerated={this.props.hardwareAccelerated} onRequestClose={this.props.onRequestClose} onShow={this.props.onShow} style={styles.modal} onStartShouldSetResponder={this._shouldSetResponder} > <View style={[styles.container, containerStyles]}> {innerChildren} </View> </RCTModalHostView>
    );
  }

  // We don't want any responder events bubbling out of the modal.
  _shouldSetResponder = () => true
}

const side = I18nManager.isRTL ? 'right' : 'left';
const styles = StyleSheet.create({
  modal: {
    position: 'absolute',
  },
  container: {
    position: 'absolute',
    [side]: 0,
    top: 0,
  },
});

module.exports = Modal;
複製代碼

ios 使用原來的Modal組件,添加一個MFTranslucentModal.ios.js 文件,實現很簡單,引用 react native 的Modal就ok , 以下:

import { Modal } from 'react-native';

export default Modal;
複製代碼

最後,經過,index.js 文件統一導出:

import MFTranslucentModal from './MFTranslucentModal';

export default MFTranslucentModal;
複製代碼

好了,整個封裝過程就完成了。

新的Modal和原來的Modal使用徹底同樣,只須要更改一行代碼那就是import的地方

import { Modal } from "react-native";
複製代碼

改成

import Modal from '@components/Modal';
複製代碼

爲了方便使用,我已經這個組件開源的Github,地址爲:

github.com/23mf/react-…

已經發布到npm倉庫,引用到項目中直接使用就好具體請看Github 文檔,最後,別忘了star 一下喲。

相關文章
相關標籤/搜索