iOS 表情鍵盤+gif聊天圖文混排,看我的就夠了

更新:
1.解決首次加載鍵盤卡頓的問題软驰;
2.修改聊天布局方式涧窒,現(xiàn)在無需計算,更加絲滑锭亏。
前言:

之前做過【OC版本】【swift版本】圖文混排和表情鍵盤纠吴,說實在的很low,特別是鍵盤慧瘤,整體只是實現(xiàn)了效果并沒有封裝戴已,很難集成使用!而且之前是使用的附件做的并不支持gif表情锅减,我嘗試各種方法糖儡,想實現(xiàn)類似qq的絲滑gif表情體驗,真的不容易怔匣;經(jīng)過各種嘗試和努力最終基于【YYText】實現(xiàn)了類似qq的gif表情聊天方案握联,大量的表情也不會卡頓!而且這次的鍵盤做了比較全面的封裝集成起來很方便!


先展示一下最終實現(xiàn)的效果:

單行輸入:

演示.gif

多行輸入:


演示2.gif

鍵盤的集成方法:

self.keyboard = [LiuqsEmoticonKeyBoard showKeyBoardInView:self.view]; 
self.keyboard.delegate = self;

項目github地址:LiuqsEmoticonkeyboard

接下來介紹主要的幾個類金闽,包括類的用法纯露、內部的具體實現(xiàn)以及一些細節(jié):

  1. LiuqsEmoticonKeyBoard 表情鍵盤的實體類 :
鍵盤的代理:
@protocol LiuqsEmotionKeyBoardDelegate <NSObject>
/*
 * 發(fā)送按鈕的代理事件
 * 參數(shù)PlainStr: 轉碼后的textView的普通字符串
 */
- (void)sendButtonEventsWithPlainString:(NSString *)PlainStr;

/*
 * 代理方法:鍵盤改變的代理事件
 * 用來更新父視圖的UI,比如跟隨鍵盤改變的列表高度
 */
- (void)keyBoardChanged;

@end
/*
 * 輸入框代芜,和topbar上的是同一個輸入框
 */
@property(nonatomic, strong) UITextView *textView;
/*
 * 頂部輸入條
 */
@property(nonatomic, strong) LiuqsTopBarView *topicBar;
/* 
 * 輸入框字體埠褪,用來計算表情的大小
 */
@property(nonatomic, strong) UIFont *font;
/*
 * 鍵盤的代理
 */
@property(nonatomic, weak) id <LiuqsEmotionKeyBoardDelegate> delegate;
/*
 * 收起鍵盤的方法
 */
- (void)hideKeyBoard;
/*
 * 初始化方法
 * 參數(shù)view必須傳入控制器的視圖
 * 會返回一個鍵盤的對象
 * 默認是給17號字體
 */
+ (instancetype)showKeyBoardInView:(UIView *)view;

2.LiuqsEmotionPageView鍵盤的分頁類用來放表情按鈕,內部主要處理按所在行列位置的計算,需要給出當前是第幾頁挤庇,用來加載表情:

/*
 * 當前page的頁數(shù)
 */
@property(nonatomic, assign) NSUInteger page;
/*
 * 表情按鈕的回調事件
 * 參數(shù)button是當前點擊按鈕的對象
 */
@property(nonatomic, copy)void (^emotionButtonClick)(LiuqsButton *button);
/*
 * 鍵盤上刪除按鈕的回調事件
 * 參數(shù)button是當前點擊的刪除按鈕
 */
@property(nonatomic, copy)void (^deleteButtonClick)(LiuqsButton *button);

3.LiuqsKeyBoardHeader全局宏定義的類钞速。

4.LiuqsTopBarView鍵盤上輸入框和一些切換按鈕的實體類,這個可以根據(jù)需求自定義:

topBar的代理:
@protocol LiuqsTopBarViewDelegate <NSObject>
/*
 * 代理方法嫡秕,點擊表情按鈕觸發(fā)方法
 */
- (void)TopBarEmotionBtnDidClicked:(UIButton *)emotionBtn;
/*
 * 代理方法 玉工,點擊數(shù)字鍵盤發(fā)送的事件
 */
- (void)sendAction;
/*
 * 鍵盤改變刷新父視圖
 */
- (void)needUpdateSuperView;

@end
/*
 * 聲明topbar代理
 */
@property(assign,nonatomic)id <LiuqsTopBarViewDelegate> delegate;
/*
 * topbar上面的輸入框
 */
@property(strong,nonatomic)UITextView *textView;
/*
 * 表情按鈕
 */
@property(nonatomic, strong) UIButton *topBarEmotionBtn;
/*
 * 當前鍵盤的高度, 區(qū)分是文字鍵盤還是表情鍵盤
 */
@property(nonatomic, assign) CGFloat CurrentKeyBoardH;
/*
 * 用于主動觸發(fā)輸入框改變的方法
 */
- (void)resetSubsives;

5.LiuqsButton鍵盤上的表情按鈕淘菩,自定義是為了更好的和圖片一一對應遵班,更容易處理。

6.NSAttributedString+LiuqsExtension富文本的分類:

- (NSString *)getPlainString {
    
    NSMutableString *plainString = [NSMutableString stringWithString:self.string];
    __block NSUInteger base = 0;
    [self enumerateAttribute:NSAttachmentAttributeName inRange:NSMakeRange(0, self.length)
                     options:0
                  usingBlock:^(LiuqsTextAttachment *value, NSRange range, BOOL *stop) {
                      if (value) {
                          [plainString replaceCharactersInRange:NSMakeRange(range.location + base, range.length)
                                                     withString:value.emojiTag];
                          base += value.emojiTag.length - 1;
                      }
                  }];
    return plainString;
}

getPlainString方法主要是通過遍歷富文本中的附件(在這里是指表情圖片)并使用普通的字符串(比如:[大笑])替換潮改,得到普通的字符串編碼狭郑,拿字符串編碼去通訊,比如調用接口發(fā)消息汇在;
舉個栗子:
轉換過的字符串是這樣滴:好害羞[害羞]翰萨!
用來展示的效果是這樣滴:

示例.png

7.LiuqsTextAttachment自定義附件類,繼承于NSTextAttachment糕殉。

上邊這7個類主要是鍵盤部分的亩鬼,或者說輸入部分,就是用來拿數(shù)據(jù)和別的端交互阿蝶;接下來是轉碼部分或者說是輸出部分雳锋,就是負責拿到別人給的編碼來轉換成富文本展示給用戶看!


8.LiuqsDecoder轉碼的核心類:

主要方法:

/*
 * 轉碼方法羡洁,把普通字符串轉為富文本字符串(包含圖片文字等)
 * 參數(shù) font 是用來展示的字體大小
 * 參數(shù) plainStr 是普通的字符串
 * 返回值:用來展示的富文本玷过,直接復制給label展示
 */
+ (NSMutableAttributedString *)decodeWithPlainStr:(NSString *)plainStr font:(UIFont *)font;

詳細說一下內部的實現(xiàn):
首先是靜態(tài)屬性:

//表情的size
static CGSize                    _emotionSize;
//文本的字體
static UIFont                    *_font;
//文本的顏色
static UIColor                   *_textColor;
//正則匹對結果數(shù)組
static NSArray                   *_matches;
//需要轉碼的普通字符串
static NSString                  *_string;
//通過plist加載的對照表:[害羞] <-> 害羞圖片
static NSDictionary              *_emojiImages;
//存放圖片對應range的字典數(shù)組
static NSMutableArray            *_imageDataArray;
//全局的富文本
static NSMutableAttributedString *_attStr;
//最終要返回的結果,是一個富文本
static NSMutableAttributedString *_resultStr;
+ (NSMutableAttributedString *)decodeWithPlainStr:(NSString *)plainStr font:(UIFont *)font {

    if (!plainStr) {return [[NSMutableAttributedString alloc]initWithString:@""];}else {
        
        _font      = font;
        _string    = plainStr;
        _textColor = [UIColor blackColor];
        [self initProperty];
        [self executeMatch];
        [self setImageDataArray];
        [self setResultStrUseReplace];
        return _resultStr;
    }
}
在這個方法里主要初始化對照表筑煮,以及根據(jù)字體計算表情的尺寸
+ (void)initProperty {
    
    // 讀取并加載對照表
    NSString *path = [[NSBundle mainBundle] pathForResource:@"LiuqsEmotions" ofType:@"plist"];
    _emojiImages = [NSDictionary dictionaryWithContentsOfFile:path];
    //設置文本的行間距
    NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc]init];
    
    [paragraphStyle setLineSpacing:4.0f];
    
    NSDictionary *dict = @{NSFontAttributeName:_font,NSParagraphStyleAttributeName:paragraphStyle};
    
    CGSize maxsize = CGSizeMake(1000, MAXFLOAT);
    //根據(jù)字體計算表情的高度
    _emotionSize = [@"/" boundingRectWithSize:maxsize options:NSStringDrawingUsesLineFragmentOrigin attributes:dict context:nil].size;
    
    _attStr = [[NSMutableAttributedString alloc]initWithString:_string attributes:dict];
}
在這個方法中根據(jù)定的正則規(guī)則匹對字符串中的富文本
+ (void)executeMatch {
    //正則規(guī)則
    NSString *regexString = checkStr;
    
    NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:regexString options:NSRegularExpressionCaseInsensitive error:nil];
    
    NSRange totalRange = NSMakeRange(0, [_string length]);
    //保存執(zhí)行結果
    _matches = [regex matchesInString:_string options:0 range:totalRange];
}
這個方法是根據(jù)匹對結果將對應表情圖片名字和相對的range保存到字典(比如:@{imagename:{0,4}})并將這些字典存在數(shù)組中,隨后會在`setResultStrUseReplace`中用來一個一個替換
+ (void)setImageDataArray {
    
    NSMutableArray *imageDataArray = [NSMutableArray array];
    //遍歷結果
    for (int i = (int)_matches.count - 1; i >= 0; i --) {
        
        NSMutableDictionary *record = [NSMutableDictionary dictionary];
        
        LiuqsTextAttachment *attachMent = [[LiuqsTextAttachment alloc]init];
        
        attachMent.bounds = CGRectMake(0, -4, _emotionSize.height, _emotionSize.height);
        
        NSTextCheckingResult *match = [_matches objectAtIndex:i];
        
        NSRange matchRange = [match range];
        
        NSString *tagString = [_string substringWithRange:matchRange];
        
        NSString *imageName = [_emojiImages objectForKey:tagString];
        
        if (imageName == nil || imageName.length == 0) continue;
        
        [record setObject:[NSValue valueWithRange:matchRange] forKey:@"range"];
        
        [record setObject:imageName forKey:@"imageName"];
        
        [imageDataArray addObject:record];
    }
    _imageDataArray = imageDataArray;
}
這個方法就是最終的遍歷替換過程辛蚊,需要注意的是:
#要從后往前替換,否則會出問題真仲。
原因:先替換了前邊的袋马,導致整個字符range改變,這樣字典數(shù)組中存放的range就不正確了秸应,可能會引發(fā)越界崩潰虑凛!
+ (void)setResultStrUseReplace{
    
    NSMutableAttributedString *result = _attStr;
    
    for (int i = 0; i < _imageDataArray.count ; i ++) {
        
        NSRange range = [_imageDataArray[i][@"range"] rangeValue];
        
        NSDictionary *imageDic = [_imageDataArray objectAtIndex:i];
        
        NSString *imageName = [imageDic objectForKey:@"imageName"];
        
        NSString *path = [[NSBundle mainBundle]pathForResource:imageName ofType:@"gif"];
        
        NSData *data = [NSData dataWithContentsOfFile:path];
        
        YYImage *image = [YYImage imageWithData:data scale:2];
        
        image.preloadAllAnimatedImageFrames = YES;

        YYAnimatedImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image];
        
        NSMutableAttributedString *attachText = [NSMutableAttributedString yy_attachmentStringWithContent:imageView contentMode:UIViewContentModeCenter attachmentSize:imageView.frame.size alignToFont:_font alignment:YYTextVerticalAlignmentCenter];
        
        [result replaceCharactersInRange:range withAttributedString:attachText];
    }
    _resultStr = result;
}

到此基本就說完了碑宴!YYText有很多強大的功能,大家自己可以隨意擴展卧檐,在這里只用到了imageView附件。
可能講不夠全面焰宣,具體細節(jié)可以看項目demo霉囚!
寫的比較辛苦,如果對你有用希望可以支持一下匕积,記得給個star哦盈罐!
有任何意見和建議都可以提出來,我的郵箱:liuquanshui@100tal.com

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末闪唆,一起剝皮案震驚了整個濱河市盅粪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌悄蕾,老刑警劉巖票顾,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異帆调,居然都是意外死亡奠骄,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門番刊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來含鳞,“玉大人,你說我怎么就攤上這事芹务〔醣粒” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵枣抱,是天一觀的道長熔吗。 經(jīng)常有香客問我,道長佳晶,這世上最難降的妖魔是什么磁滚? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮宵晚,結果婚禮上垂攘,老公的妹妹穿的比我還像新娘。我一直安慰自己淤刃,他們只是感情好晒他,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著逸贾,像睡著了一般陨仅。 火紅的嫁衣襯著肌膚如雪津滞。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天灼伤,我揣著相機與錄音触徐,去河邊找鬼。 笑死狐赡,一個胖子當著我的面吹牛撞鹉,可吹牛的內容都是我干的。 我是一名探鬼主播颖侄,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼鸟雏,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了览祖?” 一聲冷哼從身側響起孝鹊,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎展蒂,沒想到半個月后又活,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡锰悼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年皇钞,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片松捉。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡夹界,死狀恐怖,靈堂內的尸體忽然破棺而出隘世,到底是詐尸還是另有隱情可柿,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布丙者,位于F島的核電站复斥,受9級特大地震影響,放射性物質發(fā)生泄漏械媒。R本人自食惡果不足惜目锭,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望纷捞。 院中可真熱鬧痢虹,春花似錦、人聲如沸主儡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽糜值。三九已至丰捷,卻和暖如春坯墨,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背病往。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工捣染, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人停巷。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓耍攘,卻偏偏與公主長得像,于是被迫代替她去往敵國和親叠穆。 傳聞我的和親對象是個殘疾皇子少漆,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內容