iOS 仿微信的表情键盘

最近公司项目要求重构表情键盘,之前项目中表情键盘的控件结构底层是UIScrollview,考虑到如果用户的自定义表情添加的过多,对性能会有一定的影响,最终决定将底部的Container换成UICollectionview,并添加类似微信表情键盘的长按预览、发送功能,在自定义表情区域处,如果自定义表情的页数超过2页,底部添加滑块,并且当页面显示在非自定义表情页时,滑块处于隐藏状态,类似于微信。

先对于本demo整体图层结构进行一下梳理,方便后续内容的理解,见下图!
  • 红色区域是TMSStickerView
  • 蓝色区域是UIcollectionViewCell,每个cell的model依据屏幕尺寸已经预先处理成TMSEmoji,通过该model中的emojis属性,来显示当前cell所需要展示的所有数据
  • 绿色区域是cell上的自定义view(TMSEmojiItem),长按预览的手势添加于该视图上
@interface TMSEmoji : NSObject

@property (assign, nonatomic) TMSEmojiType type;
@property (strong, nonatomic) NSString *title;
@property (strong, nonatomic) NSArray *emojis;

@end

开发中遇到的问题:

1. 系统emoji表情页与自定义表情页交替时,底部滑块的隐藏与显示
2. 手指从自定义表情页拖拽到emoji表情页,在不松手的情况下,又拖回自定义表情页时,底部滑块的隐藏与显示
处理方案:
  • 何时对滑块做显隐动画?
    既然是交替时的特殊处理,首先在- (void)scrollViewDidScroll:(UIScrollView *)scrollView方法中依据collectionview当前偏移量从dataSource中取出当前展示的模型,以及下一个模型。获得两个模型后,首先判断两个表情模型(TMSEmoji)的类型是否相同,如果不同,则需要对滑块添加显隐的动画。

  • 对滑块是做显示动画还是隐藏动画?
    当collectionview的contentOffset.x逐渐变大时,此时需要对滑块做显示动画;反之对其做隐藏动画。这里需要辨别出collectionview的滑动方向是左滑还是滑。
    并且针对于上述问题2中的情形,需要在- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView方法中对于当前显示的item根据类型判断来控制滑块动画是否执行。

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    
    //    if (self.lastContentOffsetX < scrollView.contentOffset.x) {
    //        NSLog(@"向左滚动");
    //    }else{
    //        NSLog(@"向右滚动");
    //    }
    
    CGFloat fIdx = (scrollView.contentOffset.x + SCREEN_WIDTH * 0.2) / SCREEN_WIDTH;
    NSInteger index = (NSInteger)fIdx;
    if (index >= self.dataSource.count) {
        return;
    }
    
    TMSEmoji *emojiModel = self.dataSource[index];
    [self.toolBar setCurrentSelectedTooItemWithEmojiString:emojiModel.title];
    if (index+1 >= self.dataSource.count) {
        if (emojiModel.type == TMSEmojiTypeCustom) {
            [self.emojiSlider setValue:(scrollView.contentOffset.x / SCREEN_WIDTH - self.customEmojiStartIndex) animated:YES];
        }
        return;
    }
    
    TMSEmoji *nextEmoji = self.dataSource[index+1];
    
    if (emojiModel.type == nextEmoji.type) {
        if (emojiModel.type == TMSEmojiTypeCustom) {
            [self.emojiSlider setValue:(scrollView.contentOffset.x / SCREEN_WIDTH - self.customEmojiStartIndex) animated:YES];
        }
        return;
    }
    
    if (nextEmoji.type == TMSEmojiTypeCustom) {
        
        if (self.isAnimationing) {
            return;
        }
        
        // 左滑 消失
        /*
         右滑 显示
         1.在custom上右滑
         2.从people上右滑
         */
        if (self.lastContentOffsetX > scrollView.contentOffset.x) { // 右滑
            
            if (self.emojiSlider.alpha != 0) {
                
                [self sliderAnimationWithState:NO];
            }
            
        } else {
            
            if (scrollView.contentOffset.x >= (self.customEmojiStartIndex - 0.3) * SCREEN_WIDTH) {
                
                if (self.emojiSlider.alpha != 1) {
                    
                    self.emojiSlider.alpha = 1;
                    
                    [self sliderAnimationWithState:YES];
                    
                }
            }
        }
    }
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    
    if (self.hideCustomEmoji || self.emojiSlider.hidden) {
        return;
    }
    
    NSInteger index = scrollView.contentOffset.x / SCREEN_WIDTH;
    
    TMSEmoji *emoji = self.dataSource[index];
    if (emoji.type == TMSEmojiTypeCustom) {
        
        self.customSelectedIndex = index - self.customEmojiStartIndex > 0 ? index - self.customEmojiStartIndex : 0;
        
        if (self.emojiSlider.alpha == 0) {
            
            if (self.isAnimationing) {
                return;
            }
            
            self.emojiSlider.alpha = 1;
            
            [self sliderAnimationWithState:YES];
            
        }
    } else {
        
        self.normalSelectedIndex = index;
        
        if (self.emojiSlider.alpha == 1) {
            
            [self sliderAnimationWithState:NO];
        }
    }
    
}

3. 按分类浏览过的表情,当点击底部分类按钮时,跳转到之前浏览过的页面(仅支持键盘类型为包含自定义表情的类型)
处理方案:

声明两个属性:customSelectedIndex(自定义表情用户最后停留的index)和normalSelectedIndex(系统emoji表情用户最后停留的index)。在- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView方法中记录这两个值,当点击底部分类进行跳转时,遍历整个collectionview的dataSource,依据用户当前所点击的分类找到属于该分类的第一个model所处的index,在index基础上加上customSelectedIndex或者normalSelectedIndex,使用- (void)collectionView的scrollToItemAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UICollectionViewScrollPosition)scrollPosition animated:(BOOL)animated方法进行跳转

[weakSelf.dataSource enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            
   TMSEmoji *emoji = weakSelf.dataSource[idx];
   if (emoji.type == emojiType) { // 找到属于该分类表情的第一页所属的index
                
       if (emojiType == TMSEmojiTypeCustom) {
          idx = idx + weakSelf.customSelectedIndex;
       } else {
          if (!weakSelf.hideCustomEmoji) {
             idx = weakSelf.normalSelectedIndex;
          }
       }
                
    [weakSelf.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:idx inSection:0] atScrollPosition:UICollectionViewScrollPositionNone animated:NO];
    weakSelf.emojiSlider.alpha = emojiType == TMSEmojiTypeCustom ? 1 : 0;
    [weakSelf.emojiSlider setValue:weakSelf.customSelectedIndex animated:YES];
     *stop = YES;
    }
}];

4. 当调出表情键盘时,单击输入框重新调出系统键盘。
处理方案:

当展示出表情键盘时,对textView添加tap手势,在tap手势的action中重新调出系统键盘,并且移除tap手势

if (sender.selected) { // 显示表情键盘
    self.stickerView.sendActionBlock = ^(id emoji) {
        NSLog(@"发送emoji表情");
    };
    [self.stickerView setTextView:self.textView];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self.textView addGestureRecognizer:self.textViewTap];
     });
} else {
     self.textView.inputView = nil;
     [self.textView removeGestureRecognizer:self.textViewTap];
}

5. 表情的长按预览功能
处理方案:

参考于PPStickerKeyboard的实现思路,在TMSEmojItem上添加UILongPressGestureRecognizer,依据手势recognizer.state的三种状态(UIGestureRecognizerStateBegan, UIGestureRecognizerStateChanged, UIGestureRecognizerStateEnded)来控制标签预览层的显示与隐藏
自定义表情预览层的边框通过drawRect方法实现,考虑到drawRect对性能的影响,也可以使用CAShapeLayer来替换drawRect方法。

Demo下载:https://github.com/TMMMMMS/TMSStickerView.git

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

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,024评论 4 62
  • 我就是。人际关系中的强者。做事的强者。哲学的强者。我爱大神。
    老郑_e744阅读 235评论 1 0
  • 工作生活中,耳边时常传来一组混乱的对话。双方都想让对方明白自己的意思,可表述有问题,怎么解释都说不通。最后可能还因...
    苏菲的世界D阅读 1,050评论 0 1
  • 坐动车到武汉,又飞了7个小时终于到达了梦想之地。本来早就想写一篇游记,却迟迟未下笔。可能是因为有太多想说的,却不知...
    迷人小妖精的精阅读 111评论 0 1
  • 01 「请教」 为理解而阅读,很容易遇到读不懂的情况。这时你可能会选择「请教」。但凭借外力帮助的阅读,都不算是「真...
    何阿予阅读 475评论 0 1