美文网首页Android开发经验谈Android开发
一行代码搞定 Android平台React Native Mod

一行代码搞定 Android平台React Native Mod

作者: 依然范特稀西 | 来源:发表于2019-06-26 08:55 被阅读33次

    本文首发于公众号:Android技术杂货铺

    背景介绍

    最近在做react-native应用Android端沉浸式状态栏时,发现通过Statusbar.setTrranslucent(ture)设置界面拉通状态栏之后,使用Modal 组件的地方界面无法延伸到状态栏,导致使用Modal实现的弹窗背景蒙层顶部会有一个白条,看起来很不爽,在经过一番搜索之后,发现react-native github 上有人提这个问题,但是没有解决。因此就只有找其他方案来解决。

    最开始的想法是自定义一个组件来代替原生的Modal组件,但是项目里面使用Modal的地方很多,替换起来也很麻烦。比较致命的一点是Modal组件的一些属性是不好被替代的。比如:onRequestClose,在弹出Modal时,点击物理返回键,会回调这个方法,基本上所有使用Modal的地方都会用它来做关闭弹窗,新的组件需要报保留这些属性和功能。在网上搜到一篇文章[React Native] 还我靓靓 modal 弹窗,借鉴它的思路,最后完美解决。

    解决方案和思路

    Q: 为什么react native提供的Modal组件Android平台不能延伸到状态栏?

    A:因为ModalAndroid 原生用Dialog 实现,Dialog 本身就不能衍生到statusbar

    因此我们改一下Modal原生的实现就好了。

    解决方案: 就是更改Modal组件的原生代码实现。重新提供一个Modal(就叫:TranslucentModal)组件给react native端。

    注意的问题:

    1、新的Modal组件和原来的modal 组件所暴露的属性和方法要完全一样,这样替换就很方便。

    2、在react-native做统一封装,IOS平台继续使用react-native 提供的Modal组件,Android平台使用TranslucentModal

    最终我们只需要在使用Modal的页面更改一下引用的就ok,真正的只需要修改一行代码。

    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

    具体实现

    1、原生端代码更改

    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 了。

    2、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,地址为:

    https://github.com/23mf/react-native-translucent-modal

    已经发布到npm仓库,引用到项目中直接使用就好具体请看Github 文档,最后,别忘了star 一下哟。

    Android技术杂货铺.png

    相关文章

      网友评论

        本文标题:一行代码搞定 Android平台React Native Mod

        本文链接:https://www.haomeiwen.com/subject/zlfvqctx.html