更新:
1.解決首次加載鍵盤卡頓的問題软驰;
2.修改聊天布局方式涧窒,現(xiàn)在無需計算,更加絲滑锭亏。
前言:
之前做過【OC版本】和【swift版本】圖文混排和表情鍵盤纠吴,說實在的很low,特別是鍵盤慧瘤,整體只是實現(xiàn)了效果并沒有封裝戴已,很難集成使用!而且之前是使用的附件做的并不支持gif表情锅减,我嘗試各種方法糖儡,想實現(xiàn)類似qq的絲滑gif表情體驗,真的不容易怔匣;經(jīng)過各種嘗試和努力最終基于【YYText】實現(xiàn)了類似qq的gif表情聊天方案握联,大量的表情也不會卡頓!而且這次的鍵盤做了比較全面的封裝集成起來很方便!
先展示一下最終實現(xiàn)的效果:
單行輸入:
多行輸入:
鍵盤的集成方法:
self.keyboard = [LiuqsEmoticonKeyBoard showKeyBoardInView:self.view];
self.keyboard.delegate = self;
項目github地址:LiuqsEmoticonkeyboard
接下來介紹主要的幾個類金闽,包括類的用法纯露、內部的具體實現(xiàn)以及一些細節(jié):
-
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ā)消息汇在;
舉個栗子:
轉換過的字符串是這樣滴:好害羞[害羞]翰萨!
用來展示的效果是這樣滴:
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