結合一個開源的底部彈出選單元件來講一下如何封裝一個React Native元件

J_Knight_發表於1970-01-01

簡介

前幾天寫了一個React Native元件:一個可定製性比較高的底部彈出選單(ActionSheet)。該元件符合React Native的特性:同時支援iOS和Android雙平臺,一份相同的程式碼會在兩個平臺上展示幾乎完全相同的樣式。

先看一下效果(上排為iOS模擬器,下排為Android模擬器):

結合一個開源的底部彈出選單元件來講一下如何封裝一個React Native元件

上圖展示的是該元件的預設樣式。由於該元件具有較高的定製性,所以只需要通過設定一些屬性就可以得到更多不同的樣式。

開源專案地址:GitHub:react-naive-highly-customizable-action-sheet

定製性介紹

在該元件裡:最頂部的標題,中間的選擇項,最底部的取消項都是可有可無的,而且每一部分的字型,顏色,高度,距離,分割線顏色,圓角等也都是可以定製的。

先來看幾個預設的樣式:

預設的樣式:

預設的樣式是指使用者在不設定樣式相關屬性,只設定資料(文字)相關屬性時展現的樣式。該樣式是微信,微博裡使用的樣式,也是我個人非常喜歡的樣式。

結合一個開源的底部彈出選單元件來講一下如何封裝一個React Native元件

類似iOS原生 ActionSheet的樣式

使用者可以通過設定某些屬性可以實現iOS預設的ActionSheet的樣式:

結合一個開源的底部彈出選單元件來講一下如何封裝一個React Native元件

除此之外,使用者還可以通過設定某些屬性來實現各種其他的樣式:

結合一個開源的底部彈出選單元件來講一下如何封裝一個React Native元件

下面結合使用方法來看一下如何通過程式碼來定製這些樣式:

使用方法

安裝:

npm install react-naive-highly-customizable-action-sheet

引用元件:

import ActionSheet from 'react-naive-highly-customizable-action-sheet'

然後給該元件傳入標題,選項文字陣列,回撥方法陣列等實現一個ActionSheet的元件。

下面結合一下程式碼和demo截圖講解一下:

一個預設樣式的例子:

結合一個開源的底部彈出選單元件來講一下如何封裝一個React Native元件

該樣式的實現程式碼:

<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樣式的例子:

結合一個開源的底部彈出選單元件來講一下如何封裝一個React Native元件

該樣式的實現程式碼:

 <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

大致介紹完這個元件的功能和使用方法,下面來看一下該元件是如何封裝的。

React Native元件的封裝

封裝些什麼

對於GUI程式設計裡檢視元件來說,無外乎是以下三個內容:

  1. 資料
  2. 樣式
  3. 互動

而對於檢視元件的封裝,我個人的理解是:封裝接收資料的形式,資料與樣式之間的轉化規則以及互動的邏輯。而這些都是從資料的接收開始的。沒有資料的接收就沒有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屬性,在上面將它設定為必須傳入的屬性。所以如果在這種情況下沒有傳入該屬性,就會出現警告。

上面的只是我舉的例子,在我封裝的這個元件裡沒有任何屬性是必須傳入的。因為要提高定製性,所以所有屬性都是可傳可不傳。

現在我們知道了如何將資料傳入到元件裡。但是這僅僅是第一步。因為元件所需要的資料可能不僅僅包括使用者傳入的這些資料,還包括一些通過使用者傳入的這些資料計算後得到的另一些資料,比如彈窗的總高度。不難理解,彈窗的總高度取決於標題的高度,選項的高度和選項的個數,以及取消項的高度總和。而這個資料顯然是通過傳入的標題,選項等資料後經過計算得到的。

而且,對於一些可以不一定需要使用者傳入的資料,可能元件自己也許要提供一下對應屬性的預設值。

綜上所述,對於資料處理部分,可以分為兩類的處理:

  1. 計算額外的資料。
  2. 提供對應屬性的預設值。

分別舉兩個在該元件中的程式碼(之間省略了部分內容)講解一下。

資料處理

1. 額外需要計算的資料

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()方法裡面。

2. 提供對應屬性的預設值

如果使用者沒有傳入標題文字的顏色,則提供一個預設的標題顏色:

 constructor(props) {
	super(props);
    this.state = {
      ...
      mainTitleColor:this.props.mainTitleColor?this.props.mainTitleColor:'gray',//主標題顏色
      cancelTitle:this.props.cancelTitle?this.props.cancelTitle:'Cancel',//取消的文字
      ...
    }
 }
複製程式碼

我們可以看到,如果使用者沒有設定mainTitleColorcancelTitle這兩個屬性值,元件內部會提供相應的預設值。

資料展示

在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. 傳入回撥函式
  2. 攔截使用者操作
  3. 呼叫回撥函式

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();
    }
}
複製程式碼

在這裡除了使選單回落以外,再點選取消的時候還給了使用者反饋:點選時背景色的透明度改變。實現方法是利用的TouchableOpacityactiveOpacity = {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,就決定挑戰一下,看定製效能提高到什麼程度。

如上文所說,在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)

不難看出,該元件的三個部分(標題,選項,取消)裡,每個部分都有各自對應的屬性可以設定。因為在設計這個元件的時候就將這三個部分高度解耦了:每個部分都互不影響,有各自的資料(除了少數可以共同使用的資料),並分別進行繪製。

比如,我們可以設定:

每個部分文字內容,字型大小,高度

結合一個開源的底部彈出選單元件來講一下如何封裝一個React Native元件

背景顏色(可以統一設定,也可以單獨設定)

結合一個開源的底部彈出選單元件來講一下如何封裝一個React Native元件

分割線高度,距離底部的高度,距離螢幕側邊的距離

結合一個開源的底部彈出選單元件來講一下如何封裝一個React Native元件

分割線的顏色

結合一個開源的底部彈出選單元件來講一下如何封裝一個React Native元件

上面這些圖片的效果對應的程式碼在demo中都有提供(具體檢視Example資料夾)。

另外該元件也支援一些比較極端的情況,雖然可能需求上極少遇到,但還是提供了支援。

極端情況:

結合一個開源的底部彈出選單元件來講一下如何封裝一個React Native元件

高度解耦的程度可以通過這最後一張圖看出來:主標題,選擇項,取消項都可以根據傳入屬性的情況來展示,互不影響。而且在都不設定的情況下,只展示了灰色的底部mask。

最後的話

寫這個元件一共花了3天的時間,其實第一天就已經完成了預設樣式的開發。而後2天主要做的是提高定製性的工作。因為定製性的工作是與資料處理和應用分不開的,而自己對JavaScript語法瞭解得不是很好,所以期間寫了不少的bug。值得慶幸的是,由於React Native本身搭建UI的能力很強,效率很高,所以資料處理好了之後工作量就不大了。

畢竟是自己封裝的第一個React Native元件,我相信它還是有很多提升空間的,比如資料處理這一塊可能有不妥的地方,還需要各位能給出寶貴的意見和建議。


本篇已同步到個人部落格:J_Knight_:結合一個開源的底部選單元件來講一下如何封裝一個React Native元件

---------------------------- 2018年7月17日更新 ----------------------------

注意注意!!!

筆者在近期開通了個人公眾號,主要分享程式設計,讀書筆記,思考類的文章。

  • 程式設計類文章:包括筆者以前釋出的精選技術文章,以及後續釋出的技術文章(以原創為主),並且逐漸脫離 iOS 的內容,將側重點會轉移到提高程式設計能力的方向上。
  • 讀書筆記類文章:分享程式設計類思考類心理類職場類書籍的讀書筆記。
  • 思考類文章:分享筆者平時在技術上生活上的思考。

因為公眾號每天釋出的訊息數有限制,所以到目前為止還沒有將所有過去的精選文章都發布在公眾號上,後續會逐步釋出的。

而且因為各大部落格平臺的各種限制,後面還會在公眾號上釋出一些短小精幹,以小見大的乾貨文章哦~

掃下方的公眾號二維碼並點選關注,期待與您的共同成長~

公眾號:程式設計師維他命

相關文章