React Native 高仿微信表情选择

开发一个IM应用,发送emoji和表情包贴图的是很受欢迎的feature.微信的实现有很好的用户体验,用户也接受了这种表情的选择和发送方式。那么在React Native中如何实现呢?

1.需要实现的效果

    1. 分类显示表情
    1. 指示器显示当前页在分类中的位置
    1. 滑动翻页切换表情,并且自动切换分类
    1. 点击分类跳转到分类的表情
demo.gif

2. 如何实现呢?

拆分组件

可以看到最终我们实现的效果是不错的。接着分析实现。按照React的组件化的思想。从上往下,从下往上分割组件实现都是可以的。这里方便描述从上往下拆分组件。如下图所示,不同颜色代表不同的组件。作为表情选择组件本身定义为StcikerPciker结合多个子组件。这里比较疑惑的是黄色和橙色矩形区域所代表的组件。因为是可以滑动分页切换的所以黄色代表的是可滑动组件,橙色是每一页内容的组件。接着是绿色部分指示当前页在分类中的位置,随着滑动和切换分类时变化。最下面是分类选择,可以高亮显示当前分类,并且可点击选择分类,同时滑动分页切换分类,也跟随切换分类。


组件拆分
组件的实现

按照以上的分析,除了滑动分页组件其它子组件都需要自己实现,最终组合成StickerPicker.滑动分页怎么做比较好呢?查看官方文档,发现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}
      >

除此之外需要设置horizontal=true,水平方向布局。也不需要显示Indicator。可以看到每一页都是需要换行布局的,这里定义GridView 组件实现。通过传入数据,和行数,以及renderItem 实现渲染每一页的表情。动态换行需要计算宽高,并且动态换行添加子组件。

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。其实时间是很简单的,主要通过显示多个小圆圈,和选中的圆圈指定选中的样式即可实现.以下是核心代码

 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回调。我们实现即可,回调传入的参数也很详细。

 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。接下来进行分析

数据的处理

StickerManager 封装了对sticker的处理。在整个app生命周期中,应该是只加载一次就可以,所以设计为单例模式的。可以看到数据是在一个json文件中-- sticker.ts 作为一个json对象,通过Object的entires方法。获取了key:value值。然后转变了StickerCategory[]. 这里对每个category 没有占满分类最大值的情况,还有添加占位的placeholder .这些处理都在loadSticker中进行了完整的实现。然后就是剩下的十来个左右的数据处理函数,这些函数都是在StickerPicker的实现中一步步添加的。当然也是有总体的设计。

  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 会进行校验。然而不止咋的报错了。所以暴力的处理方式通过,把报错的代码进行注释即可,至于有没有其它副作用?暂时还没有遇到。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,265评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,078评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,852评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,408评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,445评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,772评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,921评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,688评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,130评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,467评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,617评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,276评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,882评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,740评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,967评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,315评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,486评论 2 348

推荐阅读更多精彩内容

  • 1.写作是学习专栏和新知识一种很好的方式 2.把一个概念通过自己的理念,再用写作的方式把这概念教给别人。这是对于一...
    水中望我阅读 196评论 0 0
  • 百日阅读第47天 分享人:55雪舞 书名:瓦尔登湖 页码:55-67 感想:食物完全可以自给自足,不需要的物品即使...
    雪山飞狐儿阅读 292评论 0 0
  • 今天的小任务本来是要用最爱的纸笔来完成的,因为我有日记本,开始是每天都写,记录下孩子们的日常和自己的所思所想,可自...
    暖吗阅读 225评论 0 0