一行代码搞定 Android平台React Native Mod
本文首发于公众号: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:因为Modal
Android 原生用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在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,地址为:
已经发布到npm仓库,引用到项目中直接使用就好具体请看Github 文档,最后,别忘了star 一下哟。
Android技术杂货铺.png