React Native 高仿微信表情選擇

開發(fā)一個IM應(yīng)用,發(fā)送emoji和表情包貼圖的是很受歡迎的feature.微信的實(shí)現(xiàn)有很好的用戶體驗(yàn),用戶也接受了這種表情的選擇和發(fā)送方式。那么在React Native中如何實(shí)現(xiàn)呢?

1.需要實(shí)現(xiàn)的效果

    1. 分類顯示表情
    1. 指示器顯示當(dāng)前頁在分類中的位置
    1. 滑動翻頁切換表情,并且自動切換分類
    1. 點(diǎn)擊分類跳轉(zhuǎn)到分類的表情
demo.gif

2. 如何實(shí)現(xiàn)呢?

拆分組件

可以看到最終我們實(shí)現(xiàn)的效果是不錯的。接著分析實(shí)現(xiàn)。按照React的組件化的思想。從上往下,從下往上分割組件實(shí)現(xiàn)都是可以的。這里方便描述從上往下拆分組件。如下圖所示,不同顏色代表不同的組件。作為表情選擇組件本身定義為StcikerPciker結(jié)合多個子組件。這里比較疑惑的是黃色和橙色矩形區(qū)域所代表的組件。因?yàn)槭强梢曰瑒臃猪撉袚Q的所以黃色代表的是可滑動組件,橙色是每一頁內(nèi)容的組件。接著是綠色部分指示當(dāng)前頁在分類中的位置,隨著滑動和切換分類時變化。最下面是分類選擇,可以高亮顯示當(dāng)前分類,并且可點(diǎn)擊選擇分類,同時滑動分頁切換分類,也跟隨切換分類。


組件拆分
組件的實(shí)現(xiàn)

按照以上的分析,除了滑動分頁組件其它子組件都需要自己實(shí)現(xiàn),最終組合成StickerPicker.滑動分頁怎么做比較好呢?查看官方文檔,發(fā)現(xiàn)ScrollView 是可以做到的。pagingEnabled 置為true 即可。

<ScrollView
        ref={v => this.scrollView = v}
        style={[styles.scrollview, { height: viewHeight }]}
        automaticallyAdjustContentInsets={false}
        horizontal={true}
        pagingEnabled={true}
        showsHorizontalScrollIndicator={false}
        onMomentumScrollEnd={this.onContentHorizontalScrollEnd}
        scrollEventThrottle={16}
      >

除此之外需要設(shè)置horizontal=true,水平方向布局。也不需要顯示Indicator??梢钥吹矫恳豁摱际切枰獡Q行布局的,這里定義GridView 組件實(shí)現(xiàn)。通過傳入數(shù)據(jù),和行數(shù),以及renderItem 實(shí)現(xiàn)渲染每一頁的表情。動態(tài)換行需要計(jì)算寬高,并且動態(tài)換行添加子組件。

GridView 換行添加子組件

const itemContainers: React.ReactElement[] = [];
    const maxLine = Math.ceil(data.length / numColumns);
    for (let i = 0; i < maxLine; i++) {
      const itemContainer: React.ReactElement[] = [];
      let startIndex = 0;
      for (let j = 0; j < numColumns; j++) {
        startIndex = j + i * numColumns;
        if (startIndex < data.length) {
          const child = renderItem(data[startIndex]);
          itemContainer.push(child);
        } else {
          break;
        }
      }

      itemContainers.push(<View style={styles.itemContainer} key={i}>{itemContainer}</View>);
    }

也許會問,這里為什么需要單獨(dú)實(shí)現(xiàn)GridView.而不使用FlatList。沒錯,F(xiàn)latList 是可以滿足實(shí)現(xiàn)的,只是FlatList 主要是用于無限列表的加載,很重量級。在這里使用未免大材小用,并導(dǎo)致過度繪制。所以這里實(shí)現(xiàn)GridView主要是為了性能。除此之外實(shí)現(xiàn)GridView 不是很復(fù)雜,只是需要計(jì)算相關(guān)數(shù)組操作,臨界值的處理,以及寬高的處理。
到這里基本上可以跑起來了,可以實(shí)現(xiàn)數(shù)據(jù)的添加以及滑動。數(shù)據(jù)的添加在StcikerPicker 是很重要以及復(fù)雜的,這里的實(shí)現(xiàn)主要花的時間精力在這里。后面部分會詳細(xì)講,這里先對組件的實(shí)現(xiàn)進(jìn)行講解。到這里還有兩個指示器未實(shí)現(xiàn)。先看到分頁指示器 SegmentControl。其實(shí)時間是很簡單的,主要通過顯示多個小圓圈,和選中的圓圈指定選中的樣式即可實(shí)現(xiàn).以下是核心代碼

 render() {
        const { length } = this.props;
        return (
            <View style={styles.view}>
                {new Array(length).fill(1).map(this.renderItem)}
            </View>
        );
    }

    private renderItem = (item, index) => {
        const { currentIndex, color, currentColor } = this.props;
        const bgColor = (value) => ({ backgroundColor: value });
        const style = index === currentIndex ?
            [styles.cur, bgColor(currentColor)] :
            [styles.other, bgColor(color)];
        return <View key={index} style={style} />;
    }

。接著是分類指示器 CategoryControl,與SegmentControl 實(shí)現(xiàn)是很相似的只是,對單個item 的實(shí)現(xiàn)略微復(fù)雜一些。
到這里主要組件的實(shí)現(xiàn)基本上完成了。接著就需要把組件組合起來,并賦予它們事件和邏輯互相關(guān)聯(lián)起來作為一個整體存在。

組件的整合

可以看到滑動sticker和會觸發(fā)兩個指示器的變化。點(diǎn)擊分類指示器同樣也會觸發(fā)另一個指示器和sticker頁面的變化。這就需要事件的處理了。先看到對滑動sticker事件的處理,ScrollView 滑動結(jié)束會觸發(fā)onMomentumScrollEnd回調(diào)。我們實(shí)現(xiàn)即可,回調(diào)傳入的參數(shù)也很詳細(xì)。

 private onContentHorizontalScrollEnd = (event) => {
    const offsetX = event.nativeEvent.contentOffset.x;
    const newIndex = Math.round(offsetX / this.state.width);
    if (newIndex !== this.state.curIndex) {
      if (StickerManager.getInstance().checkCategoryChanged(this.state.curIndex, newIndex)) {
        this.onCategoryChanged();
        this.setState({
          curIndex: newIndex,
          categoryCount: StickerManager.getInstance().getCagegorySizeByIndex(newIndex)
        });
      } else {
        this.setState({
          curIndex: newIndex,
        });
      }
    }
  }

滑動到下一頁或者上一頁,我們需要獲取到滑動到的頁面的index,這個index的取值是大于0小于頁數(shù)。offsetX 對于我們獲取到index是非常有用的。通過offset / this.state.width 。偏移量除以頁面的寬度即為index的值,然后就可以做下一步的操作。通過給StickerManager的checkCategoryChanged 傳入curIndex,newIndex 可以判斷是否分類改變,如果改變觸發(fā)onCategoryChanged,并且獲取當(dāng)親分類的categoryCount,傳給指示器。如果分類未改變傳給state curIndex新值。通過這里的處理我們已經(jīng)可以實(shí)現(xiàn)滑動頁面分類的變化了。還有很重要的一步,點(diǎn)擊分類頁面的變化。我們在StcategoryControl 設(shè)置onSelect 屬性。當(dāng)點(diǎn)擊分類調(diào)用,然后在StickerPicker 添加實(shí)現(xiàn)

private onCategorySelect = (category: { name: string, image: NodeRequire }) => {
    // 1. category選中,點(diǎn)擊的item 2. 滑動到選中category 的分類
    const newIndex = StickerManager.getInstance().getIndexByCategory(category.name);
    this.setState({
      curIndex: newIndex,
      categoryCount: StickerManager.getInstance().getCagegorySizeByIndex(newIndex)
    });
    this.scrollView && this.scrollView.scrollTo({
      y: 0,
      x: this.state.width * newIndex,
      animated: false
    });
  }

通過返回的category 并傳遞個StickerManager的getIndexByCategory 獲取選中分類的newIndex.然后setState 新分類的categoryCount,同樣是通過StickerManager獲取。
然后讓scrollView 滑動到分類的位置,通過調(diào)用scrollTo方法,x值為,width * newIndex .到這里已經(jīng)對事件的處理有一個比較完整的實(shí)現(xiàn)了,可以看到涉及到數(shù)據(jù)離不開StickerManager。接下來進(jìn)行分析

數(shù)據(jù)的處理

StickerManager 封裝了對sticker的處理。在整個app生命周期中,應(yīng)該是只加載一次就可以,所以設(shè)計(jì)為單例模式的。可以看到數(shù)據(jù)是在一個json文件中-- sticker.ts 作為一個json對象,通過Object的entires方法。獲取了key:value值。然后轉(zhuǎn)變了StickerCategory[]. 這里對每個category 沒有占滿分類最大值的情況,還有添加占位的placeholder .這些處理都在loadSticker中進(jìn)行了完整的實(shí)現(xiàn)。然后就是剩下的十來個左右的數(shù)據(jù)處理函數(shù),這些函數(shù)都是在StickerPicker的實(shí)現(xiàn)中一步步添加的。當(dāng)然也是有總體的設(shè)計(jì)。

  public getAllStickers(): StickerItem[] {
    if (this.stickerCategories.length === 0) {
      return [];
    }
    return this.stickerCategories.map(cagegory => cagegory.getStickers()).reduce((pre, cur) => {
      return pre.concat(cur);
    });
  }

以getAllStickers 為例,獲取所有sticker包括placeholder 通過該方法。主要涉及到對數(shù)組相關(guān)函數(shù)的使用。如果感興趣,可以看看源碼,由于時間還有寫作表達(dá)的不住,可能在以上的介紹中存在一些理解偏差。結(jié)合代碼使用更好額。我是源碼

可能出現(xiàn)的問題:

SHA-1 for file E:\private_project\great_frontend\react-native-app\RNStickerPicker\asset\stickers\Asongsongmeow-resized\A1.gif
出現(xiàn)這個問題,可是個大坑??梢钥吹絾栴}開頭SHA-1 for file xxxx.那么這個SHA-1 是在那個那里產(chǎn)生的呢。在編譯期metro 會進(jìn)行校驗(yàn)。然而不止咋的報錯了。所以暴力的處理方式通過,把報錯的代碼進(jìn)行注釋即可,至于有沒有其它副作用?暫時還沒有遇到。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1.寫作是學(xué)習(xí)專欄和新知識一種很好的方式 2.把一個概念通過自己的理念,再用寫作的方式把這概念教給別人。這是對于一...
    水中望我閱讀 241評論 0 0
  • 百日閱讀第47天 分享人:55雪舞 書名:瓦爾登湖 頁碼:55-67 感想:食物完全可以自給自足,不需要的物品即使...
    雪山飛狐兒閱讀 330評論 0 0
  • 今天的小任務(wù)本來是要用最愛的紙筆來完成的,因?yàn)槲矣腥沼洷?,開始是每天都寫,記錄下孩子們的日常和自己的所思所想,可自...
    暖嗎閱讀 279評論 0 0

友情鏈接更多精彩內(nèi)容