最近在公司作了个表情键盘的需求,这个需求的技术难度不会很大,比较偏向业务。可是要把用户体验作的好也是不容易的,其中有几个点须要特别注意。话很少说,下面开始正文(注:本文对应的Demo放在Github上:github.com/VernonVan/P…)。html
首先来看一下市面上主要的几个APP上的表情键盘,平时使用的时候不会去关注细节,此次特地去使用了表情键盘,发现各个APP的体验仍是有优有劣的。git
首先是QQ和微信,这二者差很少,切换到表情键盘的时候都是没有光标的,这样的用户体验是很是很差的,没有办法在输入表情的时候框选区域,也不能拖动光标进行特定位置的复制黏贴删除等操做,微信甚至在输入框里显示的都不是点击的表情图片,而是文字描述。github
接下来看一下微博国际版,国际版调起表情键盘时是有光标的,是一个"真正的"键盘,可是想要拖拽光标的时候,很大几率上会触发到保存图片的行为(以下图所示),致使根本没办法拖动光标。bash
同时微博国际版输入框表情黏贴后的光标定位是错误的,以下图,开始时光标是在第4个表情后面,而后复制狗头+害羞两个表情黏贴到光标后,光标仍是在第4个表情后,同时黏贴的表情先后都莫名多了空格。微信
最后是微博,微博客户端的表情键盘的体验是很是好的,上面说到的问题都不存在,并且表情键盘的删除按钮还能长按删除输入框的内容。app
主要实现了如下几个功能字体
能输入表情,有光标,支持复制黏贴删除表情等ui
长按预览表情atom
删除表情、长按连续删除表情spa
适配 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复制代码
真正的键盘也就是说调起表情键盘时输入框是有光标的,能进行拖拽光标、选中区域等的操做,这样的体验才是与系统键盘一致的。其实系统已经提供好了接口给咱们直接使用,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;
}复制代码
咱们在输入框中输入的内容与服务端进行交互的时候都是用纯文本的,好比会把 "笑死了🤣" 转成 "笑死了[笑哭]" 这样的纯文本发到服务端,而不是直接发表情图片,向服务端请求内容的时候也是传回 "笑死了[笑哭]",而后客户端再根据正则匹配找出表情替换成对应的表情图片,而后显示到页面上。具体过程能够看下图:
也就是说,咱们设置到输入框的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);
}
}复制代码
技术点的分析就是以上这些,详细的代码能够clone下来查看:github.com/VernonVan/P…