前幾天寫了一個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日更新 ----------------------------
注意注意!!!
筆者在近期開通了我的公衆號,主要分享編程,讀書筆記,思考類的文章。
由於公衆號天天發佈的消息數有限制,因此到目前爲止尚未將全部過去的精選文章都發布在公衆號上,後續會逐步發佈的。
並且由於各大博客平臺的各類限制,後面還會在公衆號上發佈一些短小精幹,以小見大的乾貨文章哦~
掃下方的公衆號二維碼並點擊關注,期待與您的共同成長~