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

更新:
1.解決首次加載鍵盤卡頓的問題;
2.修改聊天布局方式,現在無需計算,更加絲滑。
前言:

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


先展示一下最終實現的效果:

單行輸入:

演示.gif

多行輸入:


演示2.gif

鍵盤的集成方法:

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

項目github地址:LiuqsEmoticonkeyboard

接下來介紹主要的幾個類,包括類的用法、內部的具體實現以及一些細節:

  1. LiuqsEmoticonKeyBoard 表情鍵盤的實體類 :
鍵盤的代理:
@protocol LiuqsEmotionKeyBoardDelegate <NSObject>
/*
 * 發送按鈕的代理事件
 * 參數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;
/*
 * 初始化方法
 * 參數view必須傳入控制器的視圖
 * 會返回一個鍵盤的對象
 * 默認是給17號字體
 */
+ (instancetype)showKeyBoardInView:(UIView *)view;

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

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

3.LiuqsKeyBoardHeader全局宏定義的類。

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

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

@end
/*
 * 聲明topbar代理
 */
@property(assign,nonatomic)id <LiuqsTopBarViewDelegate> delegate;
/*
 * topbar上面的輸入框
 */
@property(strong,nonatomic)UITextView *textView;
/*
 * 表情按鈕
 */
@property(nonatomic, strong) UIButton *topBarEmotionBtn;
/*
 * 當前鍵盤的高度, 區分是文字鍵盤還是表情鍵盤
 */
@property(nonatomic, assign) CGFloat CurrentKeyBoardH;
/*
 * 用于主動觸發輸入框改變的方法
 */
- (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方法主要是通過遍歷富文本中的附件(在這里是指表情圖片)并使用普通的字符串(比如:[大笑])替換,得到普通的字符串編碼,拿字符串編碼去通訊,比如調用接口發消息;
舉個栗子:
轉換過的字符串是這樣滴:好害羞[害羞]!
用來展示的效果是這樣滴:

示例.png

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

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


8.LiuqsDecoder轉碼的核心類:

主要方法:

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

詳細說一下內部的實現:
首先是靜態屬性:

//表情的size
static CGSize                    _emotionSize;
//文本的字體
static UIFont                    *_font;
//文本的顏色
static UIColor                   *_textColor;
//正則匹對結果數組
static NSArray                   *_matches;
//需要轉碼的普通字符串
static NSString                  *_string;
//通過plist加載的對照表:[害羞] <-> 害羞圖片
static NSDictionary              *_emojiImages;
//存放圖片對應range的字典數組
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;
    }
}
在這個方法里主要初始化對照表,以及根據字體計算表情的尺寸
+ (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);
    //根據字體計算表情的高度
    _emotionSize = [@"/" boundingRectWithSize:maxsize options:NSStringDrawingUsesLineFragmentOrigin attributes:dict context:nil].size;
    
    _attStr = [[NSMutableAttributedString alloc]initWithString:_string attributes:dict];
}
在這個方法中根據定的正則規則匹對字符串中的富文本
+ (void)executeMatch {
    //正則規則
    NSString *regexString = checkStr;
    
    NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:regexString options:NSRegularExpressionCaseInsensitive error:nil];
    
    NSRange totalRange = NSMakeRange(0, [_string length]);
    //保存執行結果
    _matches = [regex matchesInString:_string options:0 range:totalRange];
}
這個方法是根據匹對結果將對應表情圖片名字和相對的range保存到字典(比如:@{imagename:{0,4}})并將這些字典存在數組中,隨后會在`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改變,這樣字典數組中存放的range就不正確了,可能會引發越界崩潰!
+ (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附件。
可能講不夠全面,具體細節可以看項目demo
寫的比較辛苦,如果對你有用希望可以支持一下,記得給個star哦!
有任何意見和建議都可以提出來,我的郵箱:liuquanshui@100tal.com

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容