開發一個IM應用,發送emoji和表情包貼圖的是很受歡迎的feature.微信的實現有很好的用戶體驗,用戶也接受了這種表情的選擇和發送方式。那麼在React Native中如何實現呢?react
能夠看到最終咱們實現的效果是不錯的。接着分析實現。按照React的組件化的思想。從上往下,從下往上分割組件實現都是能夠的。這裏方便描述從上往下拆分組件。以下圖所示,不一樣顏色表明不一樣的組件。做爲表情選擇組件自己定義爲StcikerPciker結合多個子組件。這裏比較疑惑的是黃色和橙色矩形區域所表明的組件。由於是能夠滑動分頁切換的因此黃色表明的是可滑動組件,橙色是每一頁內容的組件。接着是綠色部分指示當前頁在分類中的位置,隨着滑動和切換分類時變化。最下面是分類選擇,能夠高亮顯示當前分類,而且可點擊選擇分類,同時滑動分頁切換分類,也跟隨切換分類。 git
按照以上的分析,除了滑動分頁組件其它子組件都須要本身實現,最終組合成StickerPicker.滑動分頁怎麼作比較好呢?查看官方文檔,發現ScrollView 是能夠作到的。pagingEnabled 置爲true 便可。github
<ScrollView
ref={v => this.scrollView = v}
style={[styles.scrollview, { height: viewHeight }]}
automaticallyAdjustContentInsets={false}
horizontal={true}
pagingEnabled={true}
showsHorizontalScrollIndicator={false}
onMomentumScrollEnd={this.onContentHorizontalScrollEnd}
scrollEventThrottle={16}
>
複製代碼
除此以外須要設置horizontal=true,水平方向佈局。也不須要顯示Indicator。能夠看到每一頁都是須要換行佈局的,這裏定義GridView 組件實現。經過傳入數據,和行數,以及renderItem 實現渲染每一頁的表情。動態換行須要計算寬高,而且動態換行添加子組件。json
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>);
}
複製代碼
也許會問,這裏爲何須要單獨實現GridView.而不使用FlatList。沒錯,FlatList 是能夠知足實現的,只是FlatList 主要是用於無限列表的加載,很重量級。在這裏使用未免大材小用,並致使過分繪製。因此這裏實現GridView主要是爲了性能。除此以外實現GridView 不是很複雜,只是須要計算相關數組操做,臨界值的處理,以及寬高的處理。 到這裏基本上能夠跑起來了,能夠實現數據的添加以及滑動。數據的添加在StcikerPicker 是很重要以及複雜的,這裏的實現主要花的時間精力在這裏。後面部分會詳細講,這裏先對組件的實現進行講解。到這裏還有兩個指示器未實現。先看到分頁指示器 SegmentControl。其實時間是很簡單的,主要經過顯示多個小圓圈,和選中的圓圈指定選中的樣式便可實現.如下是核心代碼react-native
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 實現是很類似的只是,對單個item 的實現略微複雜一些。 到這裏主要組件的實現基本上完成了。接着就須要把組件組合起來,並賦予它們事件和邏輯互相關聯起來做爲一個總體存在。數組
能夠看到滑動sticker和會觸發兩個指示器的變化。點擊分類指示器一樣也會觸發另外一個指示器和sticker頁面的變化。這就須要事件的處理了。先看到對滑動sticker事件的處理,ScrollView 滑動結束會觸發onMomentumScrollEnd回調。咱們實現便可,回調傳入的參數也很詳細。bash
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小於頁數。offsetX 對於咱們獲取到index是很是有用的。經過offset / this.state.width 。偏移量除以頁面的寬度即爲index的值,而後就能夠作下一步的操做。經過給StickerManager的checkCategoryChanged 傳入curIndex,newIndex 能夠判斷是否分類改變,若是改變觸發onCategoryChanged,而且獲取當親分類的categoryCount,傳給指示器。若是分類未改變傳給state curIndex新值。經過這裏的處理咱們已經能夠實現滑動頁面分類的變化了。還有很重要的一步,點擊分類頁面的變化。咱們在StcategoryControl 設置onSelect 屬性。當點擊分類調用,而後在StickerPicker 添加實現微信
private onCategorySelect = (category: { name: string, image: NodeRequire }) => {
// 1. category選中,點擊的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 滑動到分類的位置,經過調用scrollTo方法,x值爲,width * newIndex .到這裏已經對事件的處理有一個比較完整的實現了,能夠看到涉及到數據離不開StickerManager。接下來進行分析app
StickerManager 封裝了對sticker的處理。在整個app生命週期中,應該是隻加載一次就能夠,因此設計爲單例模式的。能夠看到數據是在一個json文件中-- sticker.ts 做爲一個json對象,經過Object的entires方法。獲取了key:value值。而後轉變了StickerCategory[]. 這裏對每一個category 沒有佔滿分類最大值的狀況,還有添加佔位的placeholder .這些處理都在loadSticker中進行了完整的實現。而後就是剩下的十來個左右的數據處理函數,這些函數都是在StickerPicker的實現中一步步添加的。固然也是有整體的設計。frontend
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 經過該方法。主要涉及到對數組相關函數的使用。若是感興趣,能夠看看源碼,因爲時間還有寫做表達的不住,可能在以上的介紹中存在一些理解誤差。結合代碼使用更好額。我是源碼
SHA-1 for file E:\private_project\great_frontend\react-native-app\RNStickerPicker\asset\stickers\Asongsongmeow-resized\A1.gif 出現這個問題,但是個大坑。能夠看到問題開頭SHA-1 for file xxxx.那麼這個SHA-1 是在那個那裏產生的呢。在編譯期metro 會進行校驗。然而不止咋的報錯了。因此暴力的處理方式經過,把報錯的代碼進行註釋便可,至於有沒有其它反作用?暫時尚未遇到。