专栏首页iOS开发干货分享iOS表情键盘的完整实现

iOS表情键盘的完整实现

最近在公司做了个表情键盘的需求,这个需求的技术难度不会很大,比较偏向业务。但是要把用户体验做的好也是不容易的,其中有几个点需要特别注意。话不多说,下面开始正文(注:本文对应的Demo放在Github上:github.com/VernonVan/P…)。

市面上的表情键盘的分析

首先来看一下市面上主要的几个APP上的表情键盘,平时使用的时候不会去关注细节,这次特意去使用了表情键盘,发现各个APP的体验还是有优有劣的。

首先是QQ和微信,这两者差不多,切换到表情键盘的时候都是没有光标的,这样的用户体验是非常不好的,没有办法在输入表情的时候框选区域,也不能拖动光标进行特定位置的复制黏贴删除等操作,微信甚至在输入框里显示的都不是点击的表情图片,而是文字描述。

接下来看一下微博国际版,国际版调起表情键盘时是有光标的,是一个"真正的"键盘,但是想要拖拽光标的时候,很大概率上会触发到保存图片的行为(如下图所示),导致根本没办法拖动光标。

同时微博国际版输入框表情黏贴后的光标定位是错误的,如下图,开始时光标是在第4个表情后面,然后复制狗头+害羞两个表情黏贴到光标后,光标还是在第4个表情后,同时黏贴的表情前后都莫名多了空格。

最后是微博,微博客户端的表情键盘的体验是非常好的,上面说到的问题都不存在,而且表情键盘的删除按钮还能长按删除输入框的内容。

表情键盘的实现

实现效果

主要实现了以下几个功能

  • 能输入表情,有光标,支持复制黏贴删除表情等
  • 长按预览表情
  • 删除表情、长按连续删除表情
  • 适配 iPhone X

基本思路

首先,表情包的图片是用bundle的形式组织的,用PPSticker类表征一套表情包,用PPEmoji类表征某一个表情,用一个plist作为配置文件,存储表情包的信息。

PPStickerDataManager类主要负责数据部分,用单例的形式,这样可以在初始化的时候只会读取一次plist文件中的所有表情信息;同时我们把输入框内容发到服务端以及从服务端请求到的都是纯文本的,比如会把 "笑死了?" 转成 "笑死了[笑哭]" 这样的纯文本,而不是直接把表情图片直接发到服务端,也就是说项目中有大量的地方会有把文本->表情的操作,所以PPStickerDataManager类也提供匹配某段纯文本中的表情,并把文本替换为图片的功能,PPStickerDataManager类的头文件如下:

@interface PPStickerDataManager : NSObject

+ (instancetype)sharedInstance;

/// 所有的表情包
@property (nonatomic, strong, readonly) NSArray<PPSticker *> *allStickers;

/* 匹配给定attributedString中的所有emoji,如果匹配到的emoji有本地图片的话会直接换成本地的图片
 *
 * @param attributedString 可能包含表情包的attributedString
 * @param font 表情图片的对齐字体大小
 */
- (void)replaceEmojiForAttributedString:(NSMutableAttributedString *)attributedString font:(UIFont *)font;

@end

“真正的”键盘

真正的键盘也就是说调起表情键盘时输入框是有光标的,能进行拖拽光标、选中区域等的操作,这样的体验才是与系统键盘一致的。其实系统已经提供好了接口给我们直接使用,UITextViewUITextField都有的inputViewinputAccessoryView就是用来实现自定义键盘的,这两个属性的定义如下:

// Presented when object becomes first responder.  If set to nil, reverts to following responder chain.  If
// set while first responder, will not take effect until reloadInputViews is called.
@property (nullable, readwrite, strong) UIView *inputView;             
@property (nullable, readwrite, strong) UIView *inputAccessoryView;

同时系统键盘在 设置->声音->按键音 选项打开且手机非静音状态下输入是有按键的声音的,这个按键音也是可以支持的,只要自定义键盘类遵循UIInputViewAudioFeedback协议,同时实现 enableInputClicksWhenVisible方法并返回YES,这样就可以在点击表情的时候调用[[UIDevice currentDevice] playInputClick]方法发出按键音了,详情请查看苹果的官方文档。

下面是Demo中键盘切换方法的实现:

- (void)changeKeyboardTo:(PPKeyboardType)toType
{
    switch (toType) {
        case PPKeyboardTypeSystem:
            self.textView.inputView = nil;    // 切换到系统键盘
            [self.textView reloadInputViews]; // 调用reloadInputViews方法会立刻进行键盘的切换
            break;
        case PPKeyboardTypeSticker:            
            self.textView.inputView = self.stickerKeyboard; // 切换到自定义的表情键盘
            [self.textView reloadInputViews];
            break;
        default:
            break;
    }
}

去除表情的拖拽交互

在iOS11上,UITextView上的NSTextAttachment(表情)默认可以进行拖拽交互,但是却导致拖动光标时很容易触发这个交互(图示可以查看上面说到的微博国际版中的误触)。一番查找之后才找到一个比较隐蔽的属性:textDragInteraction,直接设置为NO就能禁止掉NSTextAttachment的拖拽交互。

if (@available(iOS 11.0, *)) {  // 只在iOS11及以上才有这个属性
     _textView.textDragInteraction.enabled = NO;
}

与服务的的交互

我们在输入框中输入的内容与服务端进行交互的时候都是用纯文本的,比如会把 "笑死了?" 转成 "笑死了[笑哭]" 这样的纯文本发到服务端,而不是直接发表情图片,向服务端请求内容的时候也是传回 "笑死了[笑哭]",然后客户端再根据正则匹配找出表情替换成对应的表情图片,然后显示到页面上。具体过程可以看下图:

也就是说,我们设置到输入框的NSAttributedString中的每一个NSTextAttachment都有一个"隐藏的"属性—表情的文本描述,这里对NSAttributedString进行拓展就能实现。pp_setTextBackedString可以对NSAttributedString的指定range设置一个PPTextBackedString类型的属性,而pp_plainTextForRange能拿到NSAttributedString指定range的纯文本。具体实现如下:

@implementation NSAttributedString (PPAddition)

- (NSString *)pp_plainTextForRange:(NSRange)range
{
    if (range.location == NSNotFound || range.length == NSNotFound) {
        return nil;
    }

    NSMutableString *result = [[NSMutableString alloc] init];
    if (range.length == 0) {
        return result;
    }

    NSString *string = self.string;
    [self enumerateAttribute:PPTextBackedStringAttributeName inRange:range options:kNilOptions usingBlock:^(id value, NSRange range, BOOL *stop) {
        PPTextBackedString *backed = value;
        if (backed && backed.string) {
            [result appendString:backed.string];
        } else {
            [result appendString:[string substringWithRange:range]];
        }
    }];
    return result;
}

@end

@implementation NSMutableAttributedString (PPAddition)

- (void)pp_setTextBackedString:(PPTextBackedString *)textBackedString range:(NSRange)range
{
    if (textBackedString && ![NSNull isEqual:textBackedString]) {
        [self addAttribute:PPTextBackedStringAttributeName value:textBackedString range:range];
    } else {
        [self removeAttribute:PPTextBackedStringAttributeName range:range];
    }
}

@end

灵活的光标

表情功能,UITextView都是用NSAttributedString进行赋值的,并且我们底层其实还是用上面说到的纯文本进行实现的,那么把 [笑死] 转成 ? 就会从4个字符变成1个字符,这里是有差值的,如果不处理的话就会出现上面提到的微博国际版中复制黏贴输入框的表情会导致光标位置不对,甚至莫名其妙多出前后空格的问题。为了精准的定位光标,我们需要自行处理好这些问题。

这里自己继承并实现了UITextView的子类PPStickerTextView,在这个类中重载复制、黏贴、剪切等操作,分别对应的方法如下:

- (void)cut:(id)sender;     // 剪切

- (void)copy:(id)sender;    // 复制

- (void)paste:(id)sender;   // 黏贴

下面以剪切方法举例,看看怎么处理光标的问题,需要注意的地方请看对应的注释:

- (void)cut:(id)sender
{
    // 1.从textView中拿到对应的纯文本,比如:笑死了[笑死]
    NSString *string = [self.attributedText pp_plainTextForRange:self.selectedRange];
    if (string.length) {
        // 2. 将纯文本写入到剪贴板中
        [UIPasteboard generalPasteboard].string = string;

        // 3. 记住当前的光标位置
        NSRange selectedRange = self.selectedRange;
        NSMutableAttributedString *attributeContent = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedText];
        // 4. 将检测到是表情的文本替换成对应的图片
        [attributeContent replaceCharactersInRange:self.selectedRange withString:@""];
        self.attributedText = attributeContent;
      
        // 5. 重新设置光标
        self.selectedRange = NSMakeRange(selectedRange.location, 0);
    }
}

本文分享自微信公众号 - iOSSir(iOSSir),作者:VernonVan

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-04-26

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • iOS内购(IAP)自动续订订阅类型总结

    经过完成这次的项目,我觉得其中最麻烦的就是自动续期订阅类型。因为其他几类都是一次性的内购类型,而只有自定续期订阅类是有连续性的,其中还有免费试用期、促销期的概念...

    iOSSir
  • 探究ReactiveCocoa 底层KVO封装流程

    但是RACKVO只是用了非常简单的一段代码就实现了以上的这三个步骤,去掉了胶水代码,真正的做到了面向业务开发,那它是怎么实现的呢,接下来我们来一层层分析

    iOSSir
  • 探究ReactiveCocoa 底层KVO封装流程

    原文作者:溪浣双鲤 https://www.jianshu.com/p/51758229b4a5

    iOSSir
  • 开发一定要看的Ajax编程

    Ajax编程入门 1.web交互的2种模式对比 ①:2种交互模式的流程 ? ②:2种交互模式用户体验 同步交互模式:客户端提交请求,等待,在响应回到客户端前,客...

    奋斗蒙
  • 云网融合-助力企业业务直连 | 互联港湾

    港湾人儿
  • 图解“管道过滤器模式”应用实例:SOD框架的命令执行管道

    管道和过滤器 管道和过滤器是八种体系结构模式之一,这八种体系结构模式是:层、管道和过滤器、黑板、代理者、模型-视图-控制器(MVC) 表示-抽象-控制(PAC)...

    用户1177503
  • SAP UI5日期字段的显示逻辑和用法

    每一条Notes Item上面都可以绑定一个Date Formatter,对应的是Formatter.js文件下的notesDateFormatter方法:

    Jerry Wang
  • 论文 | 谷歌OpenAI联合发布AI安全五大准则,预防AI暴走(下)

    今天我们带来了谷歌安全五大定律的下篇,经过详细的论证,谷歌给出了一个可执行的AI的安全问题设计框架。让对于AI的限制的问题不再仅限于假设和推测,对今后深度学习系...

    AI科技评论
  • keras 学习笔记:从头开始构建网络处理 mnist

    用户2434869
  • hbase源码系列(五)Trie单词查找树

      在上一章中提到了编码压缩,讲了一个简单的DataBlockEncoding.PREFIX算法,它用的是前序编码压缩的算法,它搜索到时候,是全扫描的方式搜索的...

    岑玉海

扫码关注云+社区

领取腾讯云代金券

玩转腾讯云 有奖征文活动