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

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

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


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

單行輸入:

演示.gif

多行輸入:


演示2.gif

鍵盤的集成方法:

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

項(xiàng)目github地址:LiuqsEmoticonkeyboard

接下來介紹主要的幾個(gè)類,包括類的用法、內(nèi)部的具體實(shí)現(xiàn)以及一些細(xì)節(jié):

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

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

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

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

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

3.LiuqsKeyBoardHeader全局宏定義的類。

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

topBar的代理:
@protocol LiuqsTopBarViewDelegate <NSObject>
/*
 * 代理方法,點(diǎn)擊表情按鈕觸發(fā)方法
 */
- (void)TopBarEmotionBtnDidClicked:(UIButton *)emotionBtn;
/*
 * 代理方法 ,點(diǎn)擊數(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;
/*
 * 當(dāng)前鍵盤的高度, 區(qū)分是文字鍵盤還是表情鍵盤
 */
@property(nonatomic, assign) CGFloat CurrentKeyBoardH;
/*
 * 用于主動(dòng)觸發(fā)輸入框改變的方法
 */
- (void)resetSubsives;

5.LiuqsButton鍵盤上的表情按鈕,自定義是為了更好的和圖片一一對(duì)應(yīng),更容易處理。

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方法主要是通過遍歷富文本中的附件(在這里是指表情圖片)并使用普通的字符串(比如:[大笑])替換,得到普通的字符串編碼,拿字符串編碼去通訊,比如調(diào)用接口發(fā)消息;
舉個(gè)栗子:
轉(zhuǎn)換過的字符串是這樣滴:好害羞[害羞]!
用來展示的效果是這樣滴:

示例.png

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

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


8.LiuqsDecoder轉(zhuǎn)碼的核心類:

主要方法:

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

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

//表情的size
static CGSize                    _emotionSize;
//文本的字體
static UIFont                    *_font;
//文本的顏色
static UIColor                   *_textColor;
//正則匹對(duì)結(jié)果數(shù)組
static NSArray                   *_matches;
//需要轉(zhuǎn)碼的普通字符串
static NSString                  *_string;
//通過plist加載的對(duì)照表:[害羞] <-> 害羞圖片
static NSDictionary              *_emojiImages;
//存放圖片對(duì)應(yīng)range的字典數(shù)組
static NSMutableArray            *_imageDataArray;
//全局的富文本
static NSMutableAttributedString *_attStr;
//最終要返回的結(jié)果,是一個(gè)富文本
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;
    }
}
在這個(gè)方法里主要初始化對(duì)照表,以及根據(jù)字體計(jì)算表情的尺寸
+ (void)initProperty {
    
    // 讀取并加載對(duì)照表
    NSString *path = [[NSBundle mainBundle] pathForResource:@"LiuqsEmotions" ofType:@"plist"];
    _emojiImages = [NSDictionary dictionaryWithContentsOfFile:path];
    //設(shè)置文本的行間距
    NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc]init];
    
    [paragraphStyle setLineSpacing:4.0f];
    
    NSDictionary *dict = @{NSFontAttributeName:_font,NSParagraphStyleAttributeName:paragraphStyle};
    
    CGSize maxsize = CGSizeMake(1000, MAXFLOAT);
    //根據(jù)字體計(jì)算表情的高度
    _emotionSize = [@"/" boundingRectWithSize:maxsize options:NSStringDrawingUsesLineFragmentOrigin attributes:dict context:nil].size;
    
    _attStr = [[NSMutableAttributedString alloc]initWithString:_string attributes:dict];
}
在這個(gè)方法中根據(jù)定的正則規(guī)則匹對(duì)字符串中的富文本
+ (void)executeMatch {
    //正則規(guī)則
    NSString *regexString = checkStr;
    
    NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:regexString options:NSRegularExpressionCaseInsensitive error:nil];
    
    NSRange totalRange = NSMakeRange(0, [_string length]);
    //保存執(zhí)行結(jié)果
    _matches = [regex matchesInString:_string options:0 range:totalRange];
}
這個(gè)方法是根據(jù)匹對(duì)結(jié)果將對(duì)應(yīng)表情圖片名字和相對(duì)的range保存到字典(比如:@{imagename:{0,4}})并將這些字典存在數(shù)組中,隨后會(huì)在`setResultStrUseReplace`中用來一個(gè)一個(gè)替換
+ (void)setImageDataArray {
    
    NSMutableArray *imageDataArray = [NSMutableArray array];
    //遍歷結(jié)果
    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;
}
這個(gè)方法就是最終的遍歷替換過程,需要注意的是:
#要從后往前替換,否則會(huì)出問題。
原因:先替換了前邊的,導(dǎo)致整個(gè)字符range改變,這樣字典數(shù)組中存放的range就不正確了,可能會(huì)引發(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有很多強(qiáng)大的功能,大家自己可以隨意擴(kuò)展,在這里只用到了imageView附件。
可能講不夠全面,具體細(xì)節(jié)可以看項(xiàng)目demo!
寫的比較辛苦,如果對(duì)你有用希望可以支持一下,記得給個(gè)star哦!
有任何意見和建議都可以提出來,我的郵箱:liuquanshui@100tal.com

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容