iOS表情鍵盤的完整實(shí)現(xiàn)

最近在公司做了個(gè)表情鍵盤的需求,這個(gè)需求的技術(shù)難度不會(huì)很大骚勘,比較偏向業(yè)務(wù)铐伴。但是要把用戶體驗(yàn)做的好也是不容易的撮奏,其中有幾個(gè)點(diǎn)需要特別注意。話不多說(shuō)当宴,下面開始正文(注:本文對(duì)應(yīng)的Demo放在Github上:https://github.com/VernonVan/PPStickerKeyboard)畜吊。

市面上的表情鍵盤的分析

首先來(lái)看一下市面上主要的幾個(gè)APP上的表情鍵盤,平時(shí)使用的時(shí)候不會(huì)去關(guān)注細(xì)節(jié)户矢,這次特意去使用了表情鍵盤玲献,發(fā)現(xiàn)各個(gè)APP的體驗(yàn)還是有優(yōu)有劣的。

首先是QQ和微信梯浪,這兩者差不多捌年,切換到表情鍵盤的時(shí)候都是沒有光標(biāo)的,這樣的用戶體驗(yàn)是非常不好的挂洛,沒有辦法在輸入表情的時(shí)候框選區(qū)域礼预,也不能拖動(dòng)光標(biāo)進(jìn)行特定位置的復(fù)制黏貼刪除等操作,微信甚至在輸入框里顯示的都不是點(diǎn)擊的表情圖片虏劲,而是文字描述托酸。

微信QQ表情鍵盤.JPG

接下來(lái)看一下微博國(guó)際版,國(guó)際版調(diào)起表情鍵盤時(shí)是有光標(biāo)的伙单,是一個(gè)"真正的"鍵盤获高,但是想要拖拽光標(biāo)的時(shí)候,很大概率上會(huì)觸發(fā)到保存圖片的行為(如下圖所示)吻育,導(dǎo)致根本沒辦法拖動(dòng)光標(biāo)念秧。


微博國(guó)際版誤觸.JPG

同時(shí)微博國(guó)際版輸入框表情黏貼后的光標(biāo)定位是錯(cuò)誤的,如下圖布疼,開始時(shí)光標(biāo)是在第4個(gè)表情后面摊趾,然后復(fù)制狗頭+害羞兩個(gè)表情黏貼到光標(biāo)后,光標(biāo)還是在第4個(gè)表情后游两,同時(shí)黏貼的表情前后都莫名多了空格砾层。


微博國(guó)際版黏貼.JPG

最后是微博,微博客戶端的表情鍵盤的體驗(yàn)是非常好的贱案,上面說(shuō)到的問(wèn)題都不存在肛炮,而且表情鍵盤的刪除按鈕還能長(zhǎng)按刪除輸入框的內(nèi)容。


微博表情鍵盤.jpg

表情鍵盤的實(shí)現(xiàn)

實(shí)現(xiàn)效果

主要實(shí)現(xiàn)了以下幾個(gè)功能

  • 能輸入表情宝踪,有光標(biāo)侨糟,支持復(fù)制黏貼刪除表情等

  • 長(zhǎng)按預(yù)覽表情

  • 刪除表情、長(zhǎng)按連續(xù)刪除表情

  • 適配 iPhone X


    演示.GIF

基本思路

首先瘩燥,表情包的圖片是用bundle的形式組織的秕重,用PPSticker類表征一套表情包,用PPEmoji類表征某一個(gè)表情厉膀,用一個(gè)plist作為配置文件溶耘,存儲(chǔ)表情包的信息二拐。

表情的組織.jpg

PPStickerDataManager類主要負(fù)責(zé)數(shù)據(jù)部分,用單例的形式凳兵,這樣可以在初始化的時(shí)候只會(huì)讀取一次plist文件中的所有表情信息百新;同時(shí)我們把輸入框內(nèi)容發(fā)到服務(wù)端以及從服務(wù)端請(qǐng)求到的都是純文本的,比如會(huì)把 "笑死了??" 轉(zhuǎn)成 "笑死了[笑哭]" 這樣的純文本庐扫,而不是直接把表情圖片直接發(fā)到服務(wù)端吟孙,也就是說(shuō)項(xiàng)目中有大量的地方會(huì)有把文本->表情的操作,所以PPStickerDataManager類也提供匹配某段純文本中的表情聚蝶,并把文本替換為圖片的功能杰妓,PPStickerDataManager類的頭文件如下:

@interface PPStickerDataManager : NSObject

+ (instancetype)sharedInstance;

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

/* 匹配給定attributedString中的所有emoji,如果匹配到的emoji有本地圖片的話會(huì)直接換成本地的圖片
*
* @param attributedString 可能包含表情包的attributedString
* @param font 表情圖片的對(duì)齊字體大小
*/
- (void)replaceEmojiForAttributedString:(NSMutableAttributedString *)attributedString font:(UIFont *)font;

@end

"真正的"鍵盤

真正的鍵盤也就是說(shuō)調(diào)起表情鍵盤時(shí)輸入框是有光標(biāo)的碘勉,能進(jìn)行拖拽光標(biāo)巷挥、選中區(qū)域等的操作,這樣的體驗(yàn)才是與系統(tǒng)鍵盤一致的验靡。其實(shí)系統(tǒng)已經(jīng)提供好了接口給我們直接使用倍宾,UITextViewUITextField都有的inputViewinputAccessoryView就是用來(lái)實(shí)現(xiàn)自定義鍵盤的,這兩個(gè)屬性的定義如下:

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

同時(shí)系統(tǒng)鍵盤在 設(shè)置->聲音->按鍵音 選項(xiàng)打開且手機(jī)非靜音狀態(tài)下輸入是有按鍵的聲音的胜嗓,這個(gè)按鍵音也是可以支持的高职,只要自定義鍵盤類遵循UIInputViewAudioFeedback協(xié)議,同時(shí)實(shí)現(xiàn) enableInputClicksWhenVisible方法并返回YES辞州,這樣就可以在點(diǎn)擊表情的時(shí)候調(diào)用[[UIDevice currentDevice] playInputClick]方法發(fā)出按鍵音了怔锌,詳情請(qǐng)查看蘋果的官方文檔

下面是Demo中鍵盤切換方法的實(shí)現(xiàn):

- (void)changeKeyboardTo:(PPKeyboardType)toType
{
    switch (toType) {
        case PPKeyboardTypeSystem:
            self.textView.inputView = nil;    // 切換到系統(tǒng)鍵盤
            [self.textView reloadInputViews]; // 調(diào)用reloadInputViews方法會(huì)立刻進(jìn)行鍵盤的切換
            break;
        case PPKeyboardTypeSticker:            
            self.textView.inputView = self.stickerKeyboard; // 切換到自定義的表情鍵盤
            [self.textView reloadInputViews];
            break;
        default:
            break;
    }
}

去除表情的拖拽交互

在iOS11上变过,UITextView上的NSTextAttachment(表情)默認(rèn)可以進(jìn)行拖拽交互埃元,但是卻導(dǎo)致拖動(dòng)光標(biāo)時(shí)很容易觸發(fā)這個(gè)交互(圖示可以查看上面說(shuō)到的微博國(guó)際版中的誤觸)。一番查找之后才找到一個(gè)比較隱蔽的屬性:textDragInteraction媚狰,直接設(shè)置為NO就能禁止掉NSTextAttachment的拖拽交互岛杀。

if (@available(iOS 11.0, *)) {  // 只在iOS11及以上才有這個(gè)屬性
     _textView.textDragInteraction.enabled = NO;
}

與服務(wù)端的交互

我們?cè)谳斎肟蛑休斎氲膬?nèi)容與服務(wù)端進(jìn)行交互的時(shí)候都是用純文本的,比如會(huì)把 "笑死了??" 轉(zhuǎn)成 "笑死了[笑哭]" 這樣的純文本發(fā)到服務(wù)端崭孤,而不是直接發(fā)表情圖片类嗤,向服務(wù)端請(qǐng)求內(nèi)容的時(shí)候也是傳回 "笑死了[笑哭]",然后客戶端再根據(jù)正則匹配找出表情替換成對(duì)應(yīng)的表情圖片辨宠,然后顯示到頁(yè)面上遗锣。具體過(guò)程可以看下圖:


與服務(wù)端的交互.png

也就是說(shuō),我們?cè)O(shè)置到輸入框的NSAttributedString中的每一個(gè)NSTextAttachment都有一個(gè)"隱藏的"屬性—表情的文本描述彭羹,這里對(duì)NSAttributedString進(jìn)行拓展就能實(shí)現(xiàn)黄伊。pp_setTextBackedString可以對(duì)NSAttributedString的指定range設(shè)置一個(gè)PPTextBackedString類型的屬性泪酱,而pp_plainTextForRange能拿到NSAttributedString指定range的純文本派殷。具體實(shí)現(xiàn)如下:

@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

靈活的光標(biāo)

表情功能还最,UITextView都是用NSAttributedString進(jìn)行賦值的,并且我們底層其實(shí)還是用上面說(shuō)到的純文本進(jìn)行實(shí)現(xiàn)的毡惜,那么把 [笑死] 轉(zhuǎn)成 ?? 就會(huì)從4個(gè)字符變成1個(gè)字符拓轻,這里是有差值的,如果不處理的話就會(huì)出現(xiàn)上面提到的微博國(guó)際版中復(fù)制黏貼輸入框的表情會(huì)導(dǎo)致光標(biāo)位置不對(duì)经伙,甚至莫名其妙多出前后空格的問(wèn)題扶叉。為了精準(zhǔn)的定位光標(biāo),我們需要自行處理好這些問(wèn)題帕膜。

這里自己繼承并實(shí)現(xiàn)了UITextView的子類PPStickerTextView枣氧,在這個(gè)類中重載復(fù)制、黏貼垮刹、剪切等操作达吞,分別對(duì)應(yīng)的方法如下:

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

- (void)copy:(id)sender;    // 復(fù)制

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

下面以剪切方法舉例,看看怎么處理光標(biāo)的問(wèn)題荒典,需要注意的地方請(qǐng)看對(duì)應(yīng)的注釋:

- (void)cut:(id)sender
{
    // 1.從textView中拿到對(duì)應(yīng)的純文本酪劫,比如:笑死了[笑死]
    NSString *string = [self.attributedText pp_plainTextForRange:self.selectedRange];
    if (string.length) {
        // 2. 將純文本寫入到剪貼板中
        [UIPasteboard generalPasteboard].string = string;

        // 3. 記住當(dāng)前的光標(biāo)位置
        NSRange selectedRange = self.selectedRange;
        NSMutableAttributedString *attributeContent = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedText];
        // 4. 將檢測(cè)到是表情的文本替換成對(duì)應(yīng)的圖片
        [attributeContent replaceCharactersInRange:self.selectedRange withString:@""];
        self.attributedText = attributeContent;
      
        // 5. 重新設(shè)置光標(biāo)
        self.selectedRange = NSMakeRange(selectedRange.location, 0);
    }
}

技術(shù)點(diǎn)的分析就是以上這些,詳細(xì)的代碼可以到Github上clone下來(lái)查看:https://github.com/VernonVan/PPStickerKeyboard

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末寺董,一起剝皮案震驚了整個(gè)濱河市覆糟,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌遮咖,老刑警劉巖滩字,帶你破解...
    沈念sama閱讀 206,013評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異御吞,居然都是意外死亡踢械,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,205評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門魄藕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)内列,“玉大人,你說(shuō)我怎么就攤上這事背率』扒疲” “怎么了?”我有些...
    開封第一講書人閱讀 152,370評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵寝姿,是天一觀的道長(zhǎng)交排。 經(jīng)常有香客問(wèn)我,道長(zhǎng)饵筑,這世上最難降的妖魔是什么埃篓? 我笑而不...
    開封第一講書人閱讀 55,168評(píng)論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮根资,結(jié)果婚禮上架专,老公的妹妹穿的比我還像新娘同窘。我一直安慰自己,他們只是感情好部脚,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,153評(píng)論 5 371
  • 文/花漫 我一把揭開白布想邦。 她就那樣靜靜地躺著,像睡著了一般委刘。 火紅的嫁衣襯著肌膚如雪丧没。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,954評(píng)論 1 283
  • 那天锡移,我揣著相機(jī)與錄音呕童,去河邊找鬼。 笑死淆珊,一個(gè)胖子當(dāng)著我的面吹牛拉庵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播套蒂,決...
    沈念sama閱讀 38,271評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼钞支,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了操刀?” 一聲冷哼從身側(cè)響起烁挟,我...
    開封第一講書人閱讀 36,916評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎骨坑,沒想到半個(gè)月后撼嗓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,382評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡欢唾,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,877評(píng)論 2 323
  • 正文 我和宋清朗相戀三年且警,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片礁遣。...
    茶點(diǎn)故事閱讀 37,989評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡斑芜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出祟霍,到底是詐尸還是另有隱情杏头,我是刑警寧澤,帶...
    沈念sama閱讀 33,624評(píng)論 4 322
  • 正文 年R本政府宣布沸呐,位于F島的核電站醇王,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏崭添。R本人自食惡果不足惜寓娩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,209評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧棘伴,春花似錦寞埠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,199評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)缰犁。三九已至淳地,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間帅容,已是汗流浹背颇象。 一陣腳步聲響...
    開封第一講書人閱讀 31,418評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留并徘,地道東北人遣钳。 一個(gè)月前我還...
    沈念sama閱讀 45,401評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像麦乞,于是被迫代替她去往敵國(guó)和親蕴茴。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,700評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容