前几天写了一个React Native组件:一个可定制性比较高的底部弹出菜单(ActionSheet)。该组件符合React Native的特性:同时支持iOS和Android双平台,一份相同的代码会在两个平台上展现几乎彻底相同的样式。javascript
先看一下效果(上排为iOS模拟器,下排为Android模拟器):html
上图展现的是该组件的默认样式。因为该组件具备较高的定制性,因此只须要经过设置一些属性就能够获得更多不一样的样式。java
开源项目地址:GitHub:react-naive-highly-customizable-action-sheetreact
在该组件里:最顶部的标题,中间的选择项,最底部的取消项都是无关紧要的,并且每一部分的字体,颜色,高度,距离,分割线颜色,圆角等也都是能够定制的。git
先来看几个默认的样式:程序员
默认的样式是指使用者在不设置样式相关属性,只设置数据(文字)相关属性时展示的样式。该样式是微信,微博里使用的样式,也是我我的很是喜欢的样式。github
用户能够经过设置某些属性能够实现iOS默认的ActionSheet的样式:npm
除此以外,用户还能够经过设置某些属性来实现各类其余的样式:编程
下面结合使用方法来看一下如何经过代码来定制这些样式:数组
npm install react-naive-highly-customizable-action-sheet
引用组件:
import ActionSheet from 'react-naive-highly-customizable-action-sheet'
而后给该组件传入标题,选项文字数组,回调方法数组等实现一个ActionSheet的组件。
下面结合一下代码和demo截图讲解一下:
一个默认样式的例子:
该样式的实现代码:
<ActionSheet
mainTitle="There are three ways to contact. Please choose one to contact."
itemTitles = {["By phone","By message","By email"]}
selectionCallbacks = {[this.clickedByPhone,this.clickedByMessage,this.clickedByEmail]}
mainTitleTextAlign = 'center'
ref={(actionsheet)=>{this.actionsheet = actionsheet}}
/>
//弹出底部菜单
showActionSheet(){
this.actionsheet.show();
}
//回调函数
clickedByPhone(){
alert('By Phone');
}
//回调函数
clickedByMessage(){
alert('By Message');
}
//回调函数
clickedByEmail(){
alert('By Email');
}
复制代码
在这里,
mainTitle
:是最上方的标题。itemTitles
:选项文字的数组。selectionCallbacks
:点击选项后的回调函数数组。须要注意的是,选项文字的数组和回调函数数组里的元素应该是一一对应的。不过即便回调函数数组里的元素个数少于选项文字数组里的元素个数也不会引发崩溃。
一个iOS ActionSheet样式的例子:
该样式的实现代码:
<ActionSheet
mainTitle="There are three ways to contact. Please choose one to contact."
itemTitles = {["By phone","By message","By email"]}
selectionCallbacks = {[this.clickedByPhone,this.clickedByMessage,this.clickedByEmail]}
mainTitleTextAlign = 'center'
contentBackgroundColor = '#EFF0F1'
bottomSpace = {10}
cancelVerticalSpace = {10}
borderRadius = {5}
sideSpace = {6}
itemTitleColor = '#006FFF'
cancelTitleColor = '#006FFF'
ref={(actionsheet)=>{this.actionsheet = actionsheet}}
/>
//弹出底部菜单
showActionSheet(){
this.actionsheet.show();
}
//回调函数
clickedByPhone(){
alert('By Phone');
}
//回调函数
clickedByMessage(){
alert('By Message');
}
//回调函数
clickedByEmail(){
alert('By Email');
}
复制代码
更多其余的样式设定能够参考demo里的Example
。
大体介绍完这个组件的功能和使用方法,下面来看一下该组件是如何封装的。
对于GUI编程里视图组件来讲,无外乎是如下三个内容:
而对于视图组件的封装,我我的的理解是:封装接收数据的形式,数据与样式之间的转化规则以及交互的逻辑。而这些都是从数据的接收开始的。没有数据的接收就没有UI的展现,更谈不上交互了。
因此在最开始从React Native视图组件的数据接收来讲起是比较稳当的。
在iOS开发中,给view提供数据的方式是经过设置属性或者实现数据源方法来作的。可是在React Native开发中,一般只能经过设置属性来传入该组件为了实现某个样式所须要的一些数据。好比在上面的两个例子里,标题,以及选项文字都是经过设置特定的属性来传入的。
并且,为了保证设置属性的类型正确,最好对属性作一个类型检查:
import React, {Component, PropTypes} from 'react';
static propTypes = {
mainTitle:PropTypes.string.isRequired,//类型为字符串,且必须传入
mainTitleFont:PropTypes.number,//类型为数字
mainTitleColor:PropTypes.string,//类型为字符串
mainTitleTextAlign:PropTypes.oneOf(['center', 'left']),//两者选其一
hideCancel:PropTypes.bool,//类型为布尔值
...
}
复制代码
注意一下第一行的mainTitle
属性,在上面将它设置为必须传入的属性。因此若是在这种状况下没有传入该属性,就会出现警告。
上面的只是我举的例子,在我封装的这个组件里没有任何属性是必须传入的。由于要提升定制性,因此全部属性都是可传可不传。
如今咱们知道了如何将数据传入到组件里。可是这仅仅是第一步。由于组件所须要的数据可能不只仅包括用户传入的这些数据,还包括一些经过用户传入的这些数据计算后获得的另外一些数据,好比弹窗的总高度。不难理解,弹窗的总高度取决于标题的高度,选项的高度和选项的个数,以及取消项的高度总和。而这个数据显然是经过传入的标题,选项等数据后通过计算获得的。
并且,对于一些能够不必定须要用户传入的数据,可能组件本身也许要提供一下对应属性的默认值。
综上所述,对于数据处理部分,能够分为两类的处理:
分别举两个在该组件中的代码(之间省略了部份内容)讲解一下。
componentWillMount(){
...
//Calculate Title Height
if (!this.props.mainTitle){
this.real_titleHeight = 0
}else {
this.real_titleHeight = this.state.mainTitleHeight;
}
//Calculate Items height
if (!this.props.itemTitles){
this.real_itemsPartHeight = 0;
}else {
this.real_itemsPartHeight = (this.state.itemHeight + this.state.itemVerticalSpace) * this.props.itemTitles.length;
}
//Calculate Cancel part height
if (this.props.hideCancel){
this.real_cancelPartHeight = 0;
}else {
this.real_cancelPartHeight = this.state.cancelVerticalSpace + this.state.cancelHeight;
}
// total content height
this.totalHeight = this.real_titleHeight + this.real_itemsPartHeight + this.real_cancelPartHeight + this.state.bottomSpace;
...
}
复制代码
在这里,this.real_titleHeight
,this.real_itemsPartHeight
,this.real_cancelPartHeigh
,this.totalHeight
都是在拿到属性之后,须要额外计算的数据。我把这些工做放在了componentWillMount()
方法里面。
若是用户没有传入标题文字的颜色,则提供一个默认的标题颜色:
constructor(props) {
super(props);
this.state = {
...
mainTitleColor:this.props.mainTitleColor?this.props.mainTitleColor:'gray',//主标题颜色
cancelTitle:this.props.cancelTitle?this.props.cancelTitle:'Cancel',//取消的文字
...
}
}
复制代码
咱们能够看到,若是用户没有设置mainTitleColor
和cancelTitle
这两个属性值,组件内部会提供相应的默认值。
在React Native里,组件的render()
函数负责渲染组件。所以这个函数里会使用以前计算好的数据来渲染组件:
render() {
retrun(
<View> {this._renderTitleItem()} {this._renderItemsPart()} {this._renderCancelItem()} </View>)
}
//render title part
_renderTitleItem(){
if(!this.props.mainTitle){
return null;
}else {
return (
<TouchableWithoutFeedback> <View style={[styles.contentViewStyle]}> <Text>{this.props.mainTitle}</Text> </View> </TouchableWithoutFeedback>
)
}
}
//render selection items part
_renderItemsPart(){
var itemsArr = new Array();
let title = this.state.itemTitles[i];
let itemView =
<View key={i}> {/* Seperate Line */} {this._renderItemSeperateLine(showItemSeperateLine)} {/* item for selection*/} <TouchableOpacity onPress={this._didSelect.bind(this, i)}> <View style={[styles.contentViewStyle]} key={i}> <Text style={[styles.textStyle]}>{title}</Text> </View> </TouchableOpacity> </View>
itemsArr.push(itemView);
return itemsArr;
}
//render cancel part
_renderCancelItem(){
return (
<View style={{width:this.contentWidth,height: this.real_cancelPartHeight}}> {/* Seperate Line */} {this._renderCancelSeperateLine(showCancelSeperateLine)} {/* Cancel Item */} <TouchableOpacity onPress={this._dismiss.bind(this)}> <View style={[styles.contentViewStyle]}> <Text style={[styles.textStyle]}>{this.state.cancelTitle}</Text> </View> </TouchableOpacity> </View>
);
}
复制代码
组件的交互能够分为两种:有外部回调的交互以及没有外部回调的交互。这个外部回调是指在组件外部所须要执行的函数。好比底部菜单组件:若是用户点击了某一项,菜单会回落,并调用该组件外部的函数(例如退出登陆,清除缓存等等)。类比在iOS开发中,可使用代理或者block的方式进行回调,而在React Native中实现回调的方式与iOS中block的方式相似。
在React Native中,若是须要调用外部的函数,就须要在一开始的时候将该函数做为属性传入组件中。而后拦截用户的点击,调用相应的回调函数。这里面分为三个步骤:
1. 传入回调函数:
static propTypes = {
//selection items callback
selectionCallbacks:PropTypes.array,
}
复制代码
在这里,
selectionCallbacks
是对应选择项的回调函数数组属性。这里由于选择项数量不肯定,因此用数组来保存回调函数。
2. 拦截用户操做(点击):
<TouchableOpacity onPress={this._didSelect.bind(this, i)} activeOpacity = {0.9}>
<View style={styles.contentViewStyle} key={i}>
<Text style={styles.textStyle}>{title}</Text>
</View>
</TouchableOpacity>
复制代码
在这里,使用了
TouchableOpacity
组件让View
组件得到能够被点击的能力,而且绑定了函数_select(index)
。
3. 调用回调函数:
//取出相应的回调函数并调用
_select(i) {
let callback = this.state.selectionCallbacks[i];
if(callback){
{callback()}
}
}
复制代码
在这里,_didSelect(index)函数是某个选项被点击后调用的函数。该函数拿到传入的index值,从callback数组里面获取对应index的回调函数并调用。并且为了不崩溃,还判断了callback是否为空。
若是这个交互没有回调就比较简单了,在组件内部作就能够了。好比点击取消后的回落事件:
<TouchableOpacity onPress={this._dismiss.bind(this)} activeOpacity = {0.9}>
<View style={styles.contentViewStyle}>
<Text style={styles.textStyle}>{this.state.cancelTitle}</Text>
</View>
</TouchableOpacity>
//dismiss ActionSheet
_dismiss() {
if (!this.state.hide) {
this._fade();
}
}
复制代码
在这里除了使菜单回落之外,再点击取消的时候还给了用户反馈:点击时背景色的透明度改变。实现方法是利用的TouchableOpacity
的activeOpacity = {0.9}
OK,如今讲完了数据和交互,再来看一下React Native是如何支持动画效果的(由于用到了因此就顺带讲一下了)。
通常来讲,底部菜单在弹出和回落的时候是有动画效果的,React Native的动画效果能够用其内置的Animated
库来实现。
结合菜单弹出的例子来讲明一下:
//animation of showing
_appear() {
Animated.parallel([
Animated.timing(
this.state.opacity, //动画改编的变量
{
easing: Easing.linear,
duration: 200, //动画时长,单位是毫秒
toValue: 0.7, //终点值
}
),
Animated.timing(
this.state.offset,
{
easing: Easing.linear,
duration: 200,
toValue: 1,
}
)
]).start();
}
复制代码
在这里,
Animated.parallel
函数负责执行同时执行的组合动画。既然是组合动画,那么传入的就应该是一个动画的数组。仔细看一下就会发现这里有两个Animated.timing
函数。
Animated.timing
函数负责执行以时间为单位的动画。从注释上不难看出,在这里同时执行的两个动画是:
this.state.opacity
值在200毫秒内,从0到0.7渐变的动画。this.state.offset
值在200毫秒内,从0到1渐变的动画。最底部的start()
函数触发了这个组合动画。
这里没有提供起点值,由于在这里直接获取的是传入变量的当前值。
相对底部菜单的弹出动画,来看一下底部菜单的回落动画:
//animation of fading
_fade() {
Animated.parallel([
Animated.timing(
this.state.opacity,
{
easing: Easing.linear,
duration: 200,
toValue: 0,
}
),
Animated.timing(
this.state.offset,
{
easing: Easing.linear,
duration: 200,
toValue: 0,
}
)
]).start((finished) => this.setState({hide: true}));
}
复制代码
有关动画的知识能够查看官方文档React Native :动画
其实到这里,对于组件的封装就基本讲完了,讲解的内容仍是集中在数据这一块,组件是怎么画出来的就不讲解了。由于毕竟每一个组件将数据转化为样式的代码是不同的,学会一个弹出菜单的画法对于画其余的组件没有太大的借鉴意义。可是对于一个通用组件来讲,其定制性必须达到必定标准才能够。因此相对于讲解“组件是如何画出来的”,我认为讲一下“提升组件定制性”应该更实际一些。
最开始作这个控件也仅仅只能设置标题,选项以及回调函数,样式也只有这一种:
可是为了提升定制性,支持更多的样式,也为了本身能更好地了解React Native,就决定挑战一下,看定制性能提升到什么程度。
如上文所说,在React Native里,组件的数据传递是经过设置其属性来实现的。因此若是想要提升组件的定制性就须要增长该组件的属性。
看一下该组件的全部属性:
itemTitles
(Array):选择项的标题数组
selectionCallbacks
(Array):点击选项的回调数组
mainTitle
(String):标题文字
mainTitleFont
(Number):标题字体
mainTitleColor
(String):标题颜色
mainTitleHeight
(Number):标题栏高度
mainTitleTextAlign
(String):标题对齐方式
mainTitlePadding
(Number):标题内边距
itemTitleFont
(Number):选择项字体
itemTitleColor
(String):选择项颜色
itemHeight
(Number):选择栏高度
cancelTitle
(String):取消项标题,默认为'Cancel'
cancelTitleFont
(Number):取消标题字体
cancelTitleColor
(String):取消标题颜色
cancelHeight
(Number):取消栏高度
hideCancel
(Bool):是否隐藏取消项(默认不隐藏)
fontWeight
(String):全部文字的字体粗细(同时设置标题,选择项,取消项的字体粗细)
titleFontWeight
(String):标题的字体粗细,默认为'normal'
itemFontWeight
(String):选择项的字体粗细,默认为'normal'
cancelFontWeight
(String):取消项的字体粗细,默认为'bold'
contentBackgroundColor
(String):全部项目的背景色(同时设置标题,选择项,取消项的背景色)
titleBackgroundColor
(String):标题的背景色(默认是白色)
itemBackgroundColor
(String):选择项的背景色(默认是白色)
cancelBackgroundColor
(String):取消项的背景色(默认是白色)
itemSpaceColor
(String):选择项之间的分割线颜色(默认是浅灰色)
cancelSpaceColor
(String):取消项和最后一个选择项之间的分割线颜色(默认是浅灰色)
itemVerticalSpace
(Number):选择项之间分割线的高度
cancelVerticalSpace
(Number):取消项和最后一个选择项之间的分割线的高度
bottomSpace
(Number):屏幕底部距离取消项底部的距离
sideSpace
(Number):弹出框左右侧边距离屏幕左右侧边的距离
borderRadius
(Number):弹出框的圆角
maskOpacity
(Number):mask的透明度(默认为0.3)
不难看出,该组件的三个部分(标题,选项,取消)里,每一个部分都有各自对应的属性能够设置。由于在设计这个组件的时候就将这三个部分高度解耦了:每一个部分都互不影响,有各自的数据(除了少数能够共同使用的数据),并分别进行绘制。
好比,咱们能够设置:
上面这些图片的效果对应的代码在demo中都有提供(具体查看Example文件夹)。
另外该组件也支持一些比较极端的状况,虽然可能需求上极少遇到,但仍是提供了支持。
高度解耦的程度能够经过这最后一张图看出来:主标题,选择项,取消项均可以根据传入属性的状况来展现,互不影响。并且在都不设置的状况下,只展现了灰色的底部mask。
写这个组件一共花了3天的时间,其实第一天就已经完成了默认样式的开发。然后2天主要作的是提升定制性的工做。由于定制性的工做是与数据处理和应用分不开的,而本身对JavaScript语法了解得不是很好,因此期间写了很多的bug。值得庆幸的是,因为React Native自己搭建UI的能力很强,效率很高,因此数据处理好了以后工做量就不大了。
毕竟是本身封装的第一个React Native组件,我相信它仍是有不少提高空间的,好比数据处理这一块可能有不妥的地方,还须要各位能给出宝贵的意见和建议。
本篇已同步到我的博客:J_Knight_:结合一个开源的底部菜单组件来说一下如何封装一个React Native组件
---------------------------- 2018年7月17日更新 ----------------------------
注意注意!!!
笔者在近期开通了我的公众号,主要分享编程,读书笔记,思考类的文章。
由于公众号天天发布的消息数有限制,因此到目前为止尚未将全部过去的精选文章都发布在公众号上,后续会逐步发布的。
并且由于各大博客平台的各类限制,后面还会在公众号上发布一些短小精干,以小见大的干货文章哦~
扫下方的公众号二维码并点击关注,期待与您的共同成长~