iOS中表情键盘的完整实现方法详解

前言

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

市面上的表情键盘的分析

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

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

微信QQ表情键盘.JPG

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


微博国际版误触.JPG

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


微博国际版黏贴.JPG

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


微博表情键盘.jpg

表情键盘的实现

实现效果

主要实现了以下几个功能

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


演示.GIF

基本思路

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

表情的组织.jpg

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

"真正的"键盘

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

// 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;
}

与服务端的交互

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


与服务端的交互.png

也就是说,我们设置到输入框的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);
 }
}

技术点的分析就是以上这些,详细的代码可以到Github上clone下来查看:https://github.com/VernonVan/PPStickerKeyboard (本地上传)

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • iOS禁止所有输入法表情的方法

    本文实例为大家分享了iOS禁止所有输入法表情的具体代码,供大家参考,具体内容如下 - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { //禁止所有输入法的表情 if ([self isContainsTwoEmoji:text]) { return NO; } return YES; } //判断是否是表情字符 - (BO

  • IOS程序开发之禁止输入表情符号实例代码

    废话不多说了,先给大家展示效果图. 一,效果图. 二,工程图. 三,代码. RootViewController.h #import <UIKit/UIKit.h> @interface RootViewController : UIViewController <UITextViewDelegate> @end RootViewController.m - (void)viewDidLoad { [super viewDidLoad]; // Do any additional

  • 关于iOS自带九宫格拼音键盘和Emoji表情之间的一些坑

    发现问题 最近产品提了一个需求:要求某个"输入框"禁止输入 Emoji 表情,我们能想到的方案是:在 UITextField 的 textField:shouldChangeCharactersInRange:replacementString: 代理方法中判断即将输入的字符串是否包含 Emoji 表情,如果包含,就在该方法中返回 NO,不允许输入. 关于如何判断一字符串是否包含 Emoji 表情的方法,网上已经有很多代码片段,一般是通过 Unicode 编码范围来判断 ,详见这里:h

  • iOS表情键盘的简单实现代码

    最近用到了表情键盘就去网上找了下,感觉网上的都是为了更大的需求写的,而我并不需要所以就自己写了个简单的实现. 1.用到的表情字符串是从Emojiplist文件里获取到的; 2.需要添加一个观察者: [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil]; - (void)ke

  • iOS组件封装与自动布局自定义表情键盘

    下面的东西是编写自定义的表情键盘,话不多说,开门见山吧!下面主要用到的知识有MVC, iOS开发中的自动布局,自定义组件的封装与使用,Block回调,CoreData的使用.有的小伙伴可能会问写一个自定义表情键盘肿么这么麻烦?下面 将会介绍我们如何用上面提到的东西来定义我们的表情键盘的.下面的内容会比较多,这篇文章还是比较有料的. 还是那句话写技术博客是少不了代码的,下面会结合代码来回顾一下iOS的知识,本篇博文中用到的知识点在前面的博客中都能找到相应的内容,本篇 算是一个小小的功能整合.先来张

  • iOS中判断Emoji表情问题

    先给大家说下问题描述 服务器端不支持Emoji表情,因此客户端在上传用户输入时,不能包含Emoji表情. 解决方案 在客户端发送请求前,判断用户输入中是否含有表情,如果含有表情,则提示用户重新输入.这个过程关键是如何判断字符串中是否含有Emoji表情.要判断是否含有Emoji表情,必须先了解什么是Emoji. Emoji 是一套起源于日本的12x12像素表情符号,由栗田穣崇(Shigetaka Kurit)创作,最早在日本网络及手机用户中流行,自苹果公司发布的iOS 5输入法中加入了emoji后

  • iOS高仿微信表情输入功能代码分享

    最近项目需求,要实现一个类似微信的的表情输入,于是把微信的表情扒拉出来,实现了一把.可以从这里下载源码.看起来表情输入没有多少东西,不外乎就是用NSTextAttachment来实现图文混排,结果在实现的过程中遇到了很多小问题,接下来会一一介绍遇到过的坑.先上一张效果图: 一.实现表情选择View(WKExpressionView) 具体的实现就不细说了,主要功能就是点击表情时,将对应表情的图片名称通知给delegate. 二.实现表情textView(WKExpressionTextView)

  • iOS中表情键盘的完整实现方法详解

    前言 最近在公司做了个表情键盘的需求,这个需求的技术难度不会很大,比较偏向业务.但是要把用户体验做的好也是不容易的,其中有几个点需要特别注意.话不多说,下面开始正文(注:本文对应的Demo放在Github上:https://github.com/VernonVan/PPStickerKeyboard (本地上传) ). 市面上的表情键盘的分析 首先来看一下市面上主要的几个APP上的表情键盘,平时使用的时候不会去关注细节,这次特意去使用了表情键盘,发现各个APP的体验还是有优有劣的. 首先是QQ和

  • iOS中自动实现对象序列化的方法详解

    前言 在iOS 中实现对象序列化,需要遵行NSCoding协议,然后对对象的每个属性进行归档和接档赋值,响应的操作比较繁琐.本文主要介绍 利用 runtime遍历属性 大大简化代码量,下面来看看详细的介绍吧. 具体实现代码如下: 1.先建立NSobject的分类, 定义可能用到的相关类型 static NSString *intType = @"i"; // int_32t(枚举int型) static NSString *longTpye = @"l"; //lo

  • iOS中封装.framework及使用的方法详解

    .framework是什么? 这个问题相信做iOS的都知道答案. 在我们的日常开发中,经常会用到各种已经封装好的库,比如支付宝.微信SDK等等中的库,这些库可以给我们的开发带来很大的便利.有的时候,由于工作的需要,我们需要对自己的项目进行封装,生成库,方便别人的使用.在这里就边参考好点的博客,边总结一下我们经常看到的.framework. 那什么是"库"呢? "库"是共享程序代码的一种方式!同行总结的这句话很简单也很好的说明了它的作用! 一般的分为"静态库

  • jquery 键盘事件的使用方法详解

    jquery 键盘事件的使用方法详解 jQuery处理键盘事件有三个函数,根据事件发生的顺序分别是: jquery 代码: 1.  keydown(); 2.  keyup();  3.  keypress(); keydown() keydown事件会在键盘按下时触发,可以在绑定的函数中欧能够返回false来防止触发浏览器的默认事件. keyup() keyup事件会在按键释放时触发,也就是你按下键盘起来后的事件. keypress() keypress事件会在敲击按键时触发,我们可以理解为按

  • IOS中计算缓存文件的大小判断实例详解

    IOS中计算缓存文件的大小判断实例详解 IOS中计算缓存文件的大小判断,在这里分享一下自己的心得,希望和大家一起分享技术,如果有什么不足,还请大家指正.写出这篇目的,就是希望大家一起成长,我也相信技术之间没有高低,只有互补,只有分享,才能使彼此更加成长. 实例代码: //获取缓存文件路径 -(NSString *)getCachesPath{ // 获取Caches目录路径 NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCaches

  • PHP中filter函数校验数据的方法详解

    介绍PHP中filter函数校验数据的方法详解,PHP过滤器包含两种类型:Validation用来验证验证项是否合法 .Sanitization用来格式化被验证的项目,因此它可能会修改验证项的值,将不合法的字符删除. input_filters_list() 用来列出当前系统所支持的所有过滤器. 复制代码 代码如下: <?php foreach(filter_list() as $id => $filter) {     echo $filter.' '.filter_id($filter).

  • Android中实现ping功能的多种方法详解

    使用java来实现ping功能. 并写入文件.为了使用java来实现ping的功能,有人推荐使用java的 Runtime.exec()方法来直接调用系统的Ping命令,也有人完成了纯Java实现Ping的程序,使用的是Java的NIO包(native io, 高效IO包).但是设备检测只是想测试一个远程主机是否可用.所以,可以使用以下三种方式来实现: 1. Jdk1.5的InetAddresss方式 自从Java 1.5,java.net包中就实现了ICMP ping的功能. 使用时应注意,如

  • C++中new和delete的使用方法详解

    C++中new和delete的使用方法详解 new和delete运算符用于动态分配和撤销内存的运算符 new用法:           1.     开辟单变量地址空间 1)new int;  //开辟一个存放数组的存储空间,返回一个指向该存储空间的地址.int *a = new int 即为将一个int类型的地址赋值给整型指针a. 2)int *a = new int(5) 作用同上,但是同时将整数赋值为5           2.     开辟数组空间 一维: int *a = new in

  • MongoDB 中Limit与Skip的使用方法详解

    MongoDB 中Limit与Skip的使用方法详解 一 MongoDB Limit() 方法 如果你需要在MongoDB中读取指定数量的数据记录,可以使用MongoDB的Limit方法,limit()方法接受一个数字参数,该参数指定从MongoDB中读取的记录条数. 语法 limit()方法基本语法如下所示: >db.COLLECTION_NAME.find().limit(NUMBER) 实例 > db.col.find({},{"title":1,_id:0}).li

  • js基础之DOM中document对象的常用属性方法详解

    -----引入 每个载入浏览器的 HTML 文档都会成为 Document 对象. Document 对象使我们可以从脚本中对 HTML 页面中的所有元素进行访问. 属性 1  document.anchors  返回对文档中所有 Anchor 对象的引用.还有document.links/document.forms/document.images等 2  document.URL       返回当前文档的url 3  document.title       返回当前文档的标题 4  do

随机推荐