react-native-selfadapt-modal
在实际开发过程中,下拉选择应该是比较常用的功能。我在项目开发中先后使用了以下两种第三方的下拉选择框插件:
react-native-popup-menu
react-native-modal-dropdown
但是这两款插件对我来说都不是特别理想。
首先react-native-popup-menu
自定义样式是比较自由的,但是编码相当繁复,而且代码复用率太低了,尤其是使用该组件需要在最外层使用MenuProvider
替换View
。
其次react-native-modal-dropdown
自定义样式不是很友好,举个栗子?:因为该组件的背景色是透明的,在需要设置背景色为半透明的情况下,令人抓狂?!此外,如果想令弹框自适应高度也不太好使。
所以,在下希冀通过封装react native自有的Modal
组件,实现复用率较高且能自定义样式的可定位下拉框组件。
先看看效果:
获取定位参数
先说下基本思路吧:要使Modal
在特定的位置展开,就必须获取到一些定位参数,在Modal
中使用绝对布局,并按照一定的规则传入这些参数,才能正确的定位弹窗位置,除此之外还需要判断Modal
到底应该是向上展开还是向下展开。
示意图:
根据上述思路可知,必要参数是Y轴坐标(y)、Modal
控制按钮的高度(height)和屏幕高度。弹框的起点位置
首先,我想的是如何获取到Modal
的展开位置,并需要根据这个位置判断应该向下展开还是向上展开。
一开始我准备使用onLayout
方法,但是发现了几个问题,现总结如下:
1.onLayout
是 y 轴起点是根据父布局来计算的。也就是说如果不以根布局为父布局则无法获取到绝对y轴坐标。
2. 如果onLayout
用于第二级元素,Android 和 iOS 的 y 轴的起点位置是不同的,Android是从状态栏以下开始计算的,而iOS是从屏幕顶端开始计算的。
关于第二点,有示例图如下:
左边为Android,右边为iOS。
经过一番查阅,发现使用react native提供API —— NativeMethodsMixin 中的 measure
方法可以获取到某组件在屏幕中的绝对位置。该方法通过ref调用。具体是使用示例如下:
this.listRow&&this.listRow.measure((x, y, width, height, left, top) => {
console.log('===x===',x); //组件相对父布局的X坐标??
console.log('===y===',y); //组件相对父布局的Y坐标??
console.log('===width===',width); //组件的宽度
console.log('===height===',height); //组件的高度
console.log('===left===',left); //组件在屏幕中的X坐标
console.log('===top===',top); //组件在屏幕中的Y坐标
});
根据查阅API,我们知道该方法有6个回调参数,在这里我们需要使用的就是 top
和 height
。至于前两个参数,我不太确定是啥意思,有点小尴尬。欢迎有了解的童鞋进行补充。
关键代码
接下来就是实际的编码了。核心技术点如下:
第一步,通过measure
获取到Modal
的y轴坐标和点击组件(可点击的组件较多,例如Button、TouchableOpacity等,这里和下文都将此类组件笼统的成为“点击组件”)的高度,需要注意的measure是一个异步方法,因此在这里需要传入一个回调参数,示例如下:
getPosition = (callback) => {
this.listRow&&this.listRow.measure((x, y, width, height, left, top) => {
this.yaxis = top;
this.itemHeight = height;
callback&&callback();
});
}
第二步,判断Modal
是向上展开还是向下展开,当y轴坐标大于屏幕高度的一半时,向上展开,反之向下展开。
/** 获取默认的弹出位置,通过样式的形式返回 */
getPositionStyle = () => {
let positionStyle = {};
if (parseInt(this.state.yNumber) > parseInt(screenHeight / 2)) {
positionStyle = {bottom: screenHeight - (this.state.yNumber + AndroidStatusBar) };
} else {
positionStyle = {top: this.state.yNumber + this.state.itemHeight};
}
return positionStyle;
}
在这一步中,需要注意的是,因为Android的Y轴坐标是从状态栏以下开始计算的,而屏幕高度是包含了状态栏在内的,因此如果遇到向上展开的情况,就需要减去相应的高度。
Android可以通过 StatusBar
的 currentHeight
方法获取到状态栏高度。示例:
const AndroidStatusBar = Platform.OS == 'android'? StatusBar.currentHeight: 0;
最终效果如下所示:
扩展点
上文提到如何获取到Android端状态栏的高度,但是这一方法在iOS端却是无效的。经过一番查阅,我找到了以下方法:
import { NativeModules } from 'react-native';
const { StatusBarManager } = NativeModules;
componentWillMount = () => {
if (Platform.OS == 'ios') {
StatusBarManager.getHeight((statusBarHeight) => {
this.iosHeight = statusBarHeight && statusBarHeight.height;
});
}
}
getHeight
方法是只对iOS端有效,调用时请务必加上平台判断,不然会报错。
其它问题
在开发过程可能会遇到measure
的回调参数全部为undefined
的情况,这时你可能需要在创建ref的元素上添加collapsable={false}
<View
collapsable={false}
ref={(o) => this.listRow = o}>
……
</View>
然后,经过试验,发现使用measureInWindow
比起measure
更适合本地开发的需求,使用示例:
this.listRow&&this.listRow.measureInWindow((x, y, width, height) => {
console.log('===x===',x);
console.log('===y===',y);
console.log('===width===',width);
console.log('===height===',height);
});
总结
在本次开发中学习和使用了一些平时没有使用过的API和方法,例如measure
、measureInWindow
、collapsable
、StatusBarManager
等。
发现了onLayout
方法的使用局限以及其获取到的参数的意义。
熟悉了对于异步函数(例如measure
)的处理方法。
备注
我已经将该组件发布到npm,欢迎有需要的童鞋引用。
npm install react-native-selfadapt-modal --save
同时代码库也同步到github,希望各位不吝Star