今天,我終于更更更更博了。
接著上一篇聊天界面從0到1的實現(xiàn) (一),
今天來聊一聊 聊天頁面的底部橫條。
原文地址 : 聊天界面從0到1的實現(xiàn) (二)
demo 地址: JPChatBottomBar
寫在前面
JPChatBottomBar 與現(xiàn)在主流的聊天頁面的底部橫條頁面相似。
類似于微信中的:
之所以先從這個橫條來折騰,個人想法:從功能上來說,這個模塊可以從Im中獨立出來,但又可以屏蔽掉因通信部分第三方服務(wù)選擇的不同而帶來的差異,服務(wù)于聊天的整個框架。以后如果框架發(fā)生變化,這一模塊受到的影響也會是最小的。
JPChatBottomBar雖然并不整個框架的核心,但卻也提供著基礎(chǔ)的服務(wù)功能——編輯消息。
自己在模仿實現(xiàn)一個橫條的過程中,也遇到了一些麻煩。
礙于篇幅,文章中主要用于記敘一些比較復雜的實現(xiàn)抑或是一些細節(jié)的問題,簡單的邏輯判斷實現(xiàn)就不出現(xiàn)在這里了。
demo的地址放在這里:JPChatBottomBar--github地址
功能分析
結(jié)合前面的圖:可以初步總結(jié)出 JPChatBottomBar 應(yīng)該實現(xiàn)的功能,如下:
- 1.鍵盤的切換;
- 2.用戶生成語音消息;
- 3.用戶對文本消息的操作(編輯、刪除、發(fā)送);
- 4.用戶文本消息中嵌入表情包;
- 5.用戶點擊了‘大’表情包(類似于一些gif圖片);
- 6.用戶點擊更多按鈕,進行選擇其他功能實現(xiàn)(類似微信:圖庫,拍攝,發(fā)送地址等等)。
這里,我們通過 一個代理 JPChatBottomBarDelegate 來將用戶的操作(文本消息、語音消息等等)向外傳遞,即向聊天框架中的其他模塊提供服務(wù)。
先來對我所使用到的類來進行說明:
- 1.
JPChatBottomBar: 整個橫條 - 2.
preview文件中的類用于實現(xiàn)表情包的預覽效果 - 3.
imageResource文件夾中存放了此demo中所用到的圖片資源 - 4.
JPEmojiManager:這個類用于讀取所有的表情包資源 - 5.
JPPlayerHelper:這個類用于實現(xiàn)錄音和播音的效果 - 6.
JPAttributedStringHelper:實現(xiàn)表情包子符和表情包圖片的互轉(zhuǎn) - 7.關(guān)于
model,JPEmojiModel用于綁定單獨一個表情包,JPEmojiGroupModel用于綁定一整組的表情包。 - 8.
category中存放了一些常用的工具類
下面,讓我就上面所羅列的應(yīng)該實現(xiàn)的功能,來講講各功能我是如何實現(xiàn)或者是在實現(xiàn)的過程中我所遇到的問題。
鍵盤切換
效果可以到我的博客或者下載demo中查看。
可以看到,在鍵盤彈出的過程中,controller.view要向上滑動,避免彈出的鍵盤遮擋住了用戶的聊天頁面。這也是非?;A(chǔ)的功能。
但是這里有個細節(jié)的地方:
這一塊一開始我是想通過寫死系統(tǒng)鍵盤的高度,通過監(jiān)聽textView.inputView新舊值的變化(kvo實現(xiàn)參考demo里面):
從demo中的代碼可以看出,在等到chatBottomBar到達了該到的位置之后,再調(diào)用-textView reloadInputView來喚醒鍵盤。如此就可以達到鍵盤從下彈出并且不會有小部分覆蓋的效果。
但是發(fā)現(xiàn),系統(tǒng)鍵盤的高度不是都一樣的,例如漢語拼音的九宮格鍵盤要比26鍵高,而日語九宮格鍵盤要比26鍵低,所以不是很全。
于是最后還是采用了監(jiān)聽鍵盤彈出的通知來實現(xiàn) :
而微信在這一塊的實現(xiàn)就沒有這種覆蓋效果,微信等到viewController.view來到該到的位置之后,再讓鍵盤從下面彈出。
監(jiān)聽鍵盤彈出的通知:
// JPChatBottomBar.m
// 監(jiān)聽textView.inputView屬性新舊值的變化
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeRect:) name:UIKeyboardWillChangeFrameNotification object:nil];
- (void)keyboardWillChangeRect:(NSNotification *)noti {
NSValue * aValue = noti.userInfo[UIKeyboardFrameBeginUserInfoKey];
self.oldRect = [aValue CGRectValue];
NSValue * newValue = noti.userInfo[UIKeyboardFrameEndUserInfoKey];
self.newRect = [newValue CGRectValue];
[UIView animateWithDuration:0.3 animations:^{
if(self.superview.y == 0) {
self.superview.y -= self.newRect.size.height;
}else {
self.superview.y -=(self.newRect.size.height - self.oldRect.size.height);
}
} completion:^(BOOL finished) {
}];
}
路過的讀者如果有更好的改進方法,能在切換鍵盤的時候避免這種覆蓋,歡迎提出,我也是正在學習iOS 的小白??。謝謝??????
關(guān)于鍵盤的切換剩下的就是 根據(jù)用戶的點擊切換鍵盤的狀態(tài)(變化相應(yīng)的視圖)。
這一部分就先到此??????。
語音消息
在參考了別人的Demo(iOS仿微信錄音控件Demo)之后,我也實現(xiàn)了一個。
先給出自己所使用的類的介紹:
- 1.
JPPlayerHelper: 實現(xiàn)錄音和播音的功能。 - 2.
JPAudioView: 展示錄音的狀態(tài)
讓我概括一下 實現(xiàn)的大概步驟。
首先兩個類之間并不是相互依賴的,兩者在JPChatBottomBar中產(chǎn)生耦合。
上滑取消、下滑繼續(xù)錄音的效果
我在JPAudioView中利用了下面著三個方法來讓audioView對用戶手勢變化進行判斷(開始點擊、向上向下滑動、手指離開),并作出相應(yīng)的處理,代碼如下:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event ;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
在上面這三個方面中解決自身的UI問題,再通過block從外部來實現(xiàn)錄音以及根據(jù)語音強度更新UI的效果:
/// AudioView block塊的實現(xiàn)(JPChatBottomBar.m)
- (JPAudioView *)audioView {
if(!_audioView) {
JPAudioView * tmpView = [[JPAudioView alloc] initWithFrame:CGRectMake(self.textView.x, self.textView.y, self.textView.width, _btnWH )];
[self addSubview:tmpView];
_audioView = tmpView;
// 實現(xiàn)audioView的方法
__weak typeof (self) wSelf = self;
_audioView.pressBegin = ^{
[wSelf.audioView setAudioingImage:[UIImage imageNamed:@"zhengzaiyuyin_1"] text:@"松開手指,上滑取消"];
// 開始錄音
[wSelf.recoder jp_recorderStart];
};
_audioView.pressingUp = ^{
[wSelf.audioView setAudioingImage:[UIImage imageNamed:@"songkai"] text:@"松開手指,取消發(fā)送"];
};
_audioView.pressingDown = ^{
NSString * imgStr = [NSString stringWithFormat:@"zhengzaiyuyin_%d",imageIndex];
[wSelf.audioView setAudioingImage:[UIImage imageNamed:imgStr] text:@"松開發(fā)送,上滑取消"];
};
_audioView.pressEnd = ^{
[wSelf.audioView setAudioViewHidden];
[wSelf.recoder jp_recorderStop];
NSString * filePath = [wSelf.recoder getfilePath];
NSData * audioData = [NSData dataWithContentsOfFile:filePath];
/// 將語音消息data通過代理向外傳遞
if(wSelf.agent && [wSelf.agent respondsToSelector:@selector(msgEditAgentAudio:)]){
[wSelf.agent msgEditAgentAudio:audioData];
}
if(wSelf.msgEditAgentAudioBlock){
wSelf.msgEditAgentAudioBlock(audioData);
}
};
}
return _audioView;
}
其次就是這一塊比較關(guān)鍵的點:
根據(jù)語音的強度來刷新audioView的UI
效果可以到博客或者下載demo查看。
我們首先獲取語音強度平均值的方法主要通過:
/// 更新測量值
- (void)updateMeters; /* call to refresh meter values */
/// 獲取峰值
- (float)peakPowerForChannel:(NSUInteger)channelNumber;
/// 獲取平均值
- (float)averagePowerForChannel:(NSUInteger)channelNumber;
在獲取語音強度的時候,需要先updateMeters更新一下測量值。
然后我們可以通過測量值 、 峰峰值之后,根據(jù)一定的算法來計算出此時聲音的相對大小強度。這里,算法很垃圾的我簡單的設(shè)計了一個:
// JPPlayerHelper.m
- (CGFloat)audioPower {
[self.recorder updateMeters]; // 更新測量值
float power = [self.recorder averagePowerForChannel:0]; // 平均值 取得第一個通道的音頻,注意音頻的強度為[-160,0],0最大
// float powerMax = [self.recorder peakPowerForChannel:0];
// CGFloat progress = (1.0/160.0) * (power + 160);
power = power + 160 - 50;
int dB = 0;
if (power < 0.f) {
dB = 0;
} else if (power < 40.f) {
dB = (int)(power * 0.875);
} else if (power < 100.f) {
dB = (int)(power - 15);
} else if (power < 110.f) {
dB = (int)(power * 2.5 - 165);
} else {
dB = 110;
}
return dB;
}
關(guān)于這一塊的算法,如果各位讀者有更好的方法,歡迎提出,我也是個渴望知識的小白。
通過上面的方法可以獲取相應(yīng)的聲音的分貝強度,我們外部可以做一些處理:例如我做了,當新測量值比舊的測量值大一定值的時候,就做提高分貝的UI刷新操作,低的時候就做降低分貝UI的操作,具體可以看下面的代碼:
// JPChatbottomBar.m
- (void) jpHelperRecorderStuffWhenRecordWithAudioPower:(CGFloat)power{
NSLog(@"%f",power);
NSString * newPowerStr =[NSString stringWithFormat:@"%f",[self.helper audioPower]];
if([newPowerStr floatValue] > [self.audioPowerStr floatValue]) {
if(imageIndex == 6){
return;
}
imageIndex ++;
}else {
if(imageIndex == 1){
return;
}
imageIndex --;
}
if(self.audioView.state == JPPressingStateUp) {
self.audioView.pressingDown();
}
self.audioPowerStr = newPowerStr;;
}
其次,我在JPPlayerHepler加了一個計時器來觸發(fā)反復調(diào)用上面的代理方法(- (void) jpHelperRecorderStuffWhenRecordWithAudioPower:(CGFloat)power) ,讓其可以進行UI的刷新,因為如果不加計時器,我們是沒有事件去觸發(fā)audioViewUI刷新的操作,計時器相關(guān)方法如下:
// JPPlayerHelper.m
-(NSTimer *)timer{
if (!_timer) {
_timer=[NSTimer scheduledTimerWithTimeInterval:0.35 target:self selector:@selector(doOutsideStuff) userInfo:nil repeats:YES];
}
return _timer;
}
- (void)doOutsideStuff {
if(self.delegate && [self.delegate respondsToSelector:@selector(jpHelperRecorderStuffWhenRecordWithAudioPower:)]){
[self.delegate jpHelperRecorderStuffWhenRecordWithAudioPower:[self audioPower]];
}
}
完成錄音之后,最終我們的語音數(shù)據(jù)通過JPChatBottomBarDelegate的代理方法向外提供。
關(guān)于獲取語音強度那一塊的算法并不是最優(yōu),我覺得我的算法也是比較笨拙存在缺點(對用戶語音強度的變化不敏感)。如果路過的讀者有什么不錯的建議,歡迎提出補充,我也會采納,謝謝??????。
'更多' 鍵盤 上面的Item
JPChatBottomBar里面的‘更多’鍵盤與微信的類似。
開發(fā)者在使用的時候如果想要鍵入不同的功能實現(xiàn),只要在/ImageResource/JPMoreBundle.bundle 的JPMorePackageList.plist文件中添加相應(yīng)的item
內(nèi)部也已經(jīng)做好了適配的效果,不過當item數(shù)量超過8個時候,沒有完成像微信的那種分頁效果,后期我會繼續(xù)完善。
當用戶點擊了上面的某個item之后,我們就將事件通過JPChatBottomBarDelegate向外面?zhèn)鬟f,開發(fā)者可以再最外層做處理,根據(jù)點擊哪個item響應(yīng)相應(yīng)的方法功能,類似如下代碼:
// ViewController.m
NSString * kJPDictKeyImageStrKey = @"imageStr";
- (void)msgEditAgentClickMoreIVItem:(NSDictionary *)dict {
NSString * judgeStr = dict[kJPDictKeyImageStrKey];
if([judgeStr isEqualToString:@"photo"]){
NSLog(@"點擊了圖冊");
}else if([judgeStr isEqualToString:@"camera"]){
NSLog(@"點擊了攝像頭");
}else if([judgeStr isEqualToString:@"file"]) {
NSLog(@"點擊了文件");
}else if([judgeStr isEqualToString:@"location"]) {
NSLog(@"點擊了位置");
}
}
一開始沒有想著將用戶點擊哪個item暴露在外面,但后來想了開發(fā)者面臨的業(yè)務(wù)多種多樣,為了更好的擴展,簡化JPChatBottomBar的結(jié)構(gòu),就將這部分也通過代理寫出來。
文本消息的編輯(發(fā)送、刪除、嵌入表情包文本)
我花了比較多的時間在這一部分上面,之前沒有真正的做嵌入表情包的方法,只是通過調(diào)用原生的表情來實現(xiàn)表情的編輯
這一部分主要思考 當用戶點擊表情包的時候我們要做哪些處理。
先講我們的問題化簡一下。
表情包分兩種
觀察微信,表情包主要分兩大種,一種是可以嵌入文本框的表情,而另一種是當用戶點擊了該表情之后直接就發(fā)送給聊天對象,下面我們稱這兩種表情分別為SmallEmoji(前者)和LargeEmoji(后者)。
后者的實現(xiàn)方式可以通過每一層間代理將其暴露在外。
// JPChatBottomBar.h
/**
* 用戶點擊了鍵盤的表情包按鈕
* @param bigEmojiData : 大表情包的data
*/
- (void)msgEditAgentSendBigEmoji:(NSData *)bigEmojiData;
關(guān)于“點擊SmallEmoji嵌入文本”,我放后談?wù)劇?/p>
表情包的加載
這里我通過JPEmojiManager 將表情包從/ImageResource/JPEmojiBundle.bundle中加載出來,為一個表情包在JPEmojiPackageList.plist中都有對應(yīng)的item進行綁定,因此,如果后期我們有新的表情包,只要把圖片存進去,并且在plist文件中增加新的item即可以,代碼實現(xiàn)用戶動態(tài)添加表情包的方式也是一樣的。
而為了避免重復地讀取文件,我將JPEmojiManager寫成了單例。
// JPEmojiManager.h
/**
* @return 獲取所有的表情組
*/
- (NSArray <JPEmojiGroupModel *> *)getEmogiGroupArr;
/**
* 根據(jù)位置獲取相應(yīng)的模型數(shù)組
* @param group : 選擇了哪一組表情
* @param page : 頁碼
* @return 根據(jù)前面兩個參數(shù)從所有數(shù)據(jù)中根據(jù)對應(yīng)的位置和大小取出表情模型(<= 20個)
*/
- (NSArray <JPEmojiModel *> *)getEmojiArrGroup:(NSInteger)group page:(NSInteger)page;
這里可以看到兩個類
-
JPEmojiModel用于綁定單獨一個表情包, -
JPEmojiGroupModel用于綁定一整組的表情包
JPEmojiManager中的這兩個方法更多是服務(wù)于分頁表情包的效果(下面我將要談到)
表情包的分頁效果
在看過github上面別人表情包demo之后,有一些并沒有實現(xiàn)滑動切換表情包組,于是自己實現(xiàn)了一個,效果可以到博客或者demo查看。
分也效果的實現(xiàn)方式:通過三個view去復用,在ScrollView中去輪流展示。
// JPEmojiInputView.m
#pragma mark 三個view復用
@property (strong, nonatomic) JPInputPageView * leftPV;
@property (strong, nonatomic) JPInputPageView * currentPV;
@property (strong, nonatomic) JPInputPageView * rightPV;
通過前面JPEmojiManager中取出對應(yīng)頁數(shù)的表情包之后,然后調(diào)用下面的方法講每一頁的表情包傳入每一個分頁
// JPInputPageView.h
/**
* 賦予新的數(shù)組,重新刷新數(shù)據(jù)源
* @param emojiArr : 一頁的表情(作為內(nèi)置CollectionView的數(shù)據(jù)
*/
- (void)setEmojiArr:(NSArray <JPEmojiModel *> *)emojiArr isShowLargeImage:(BOOL)value;
先來講講三個分頁實現(xiàn)展示所有表情的效果:
- 1.首先將ScrollView的contentSize擴大到能容納表情包總共頁數(shù)的大小
- 2.除了第一組表情包的第一頁和最后一組表情包的最后一頁(什么都不用做),其他時刻,展示在用戶面前的那一頁始終是:
self.currentPV。 - 3.當手指向左滑去展示下一頁表情時候,
leftPv移動到了最右邊,同時去除該頁的表情包,做好展示的準備。完成這一步之后,就是更換杯子中的水的問題了,將三個復用view的相互賦值:
// JPEmojiInputView.m
JPInputPageView * tmpView ;
tmpView = self.leftPV;
self.leftPV = self.currentPV;
self.currentPV = self.rightPV;
self.rightPV = tmpView;
我通過下面的圖片來展示這一塊底層的實現(xiàn),可能可以方便大家理解:
當用戶手指向右滑展示上一頁的時候,底部實現(xiàn)的方式也是類似,以此類推。
更多細節(jié)(如何計算當前頁對應(yīng)哪一組表情包的哪一頁等)可以參考我寫在JPEmojiInputView.m里的- (void)scrollViewDidScroll:(UIScrollView *)scrollView。
點擊SmallEmoji嵌入文本
iOS 中textView和textField可以自動識別系統(tǒng)原生的表情:
而針對我們開發(fā)者另外添加的小表情,textView和textField不能直接識別。
這里可以參考了幾個主流app的實現(xiàn)方式,
- 1.微博: 點擊表情包嵌入表情圖片;
- 2.微信:點擊非原生表情包嵌入該表情包描述文本。
而要注意的是,當我們將‘圖文混編’的文本消息發(fā)送出去經(jīng)過我們服務(wù)器的時候,一般是不對字符串中的圖片信息進行解析,因此,底層依舊是向服務(wù)器傳遞純文本消息,而對里面的圖片信息做了處理轉(zhuǎn)換成了表情包的描述文本,下面我用一張圖片解釋這個問題:
可以看到我們本地需要對這些“圖文混編”的文本轉(zhuǎn)換成純文本才能發(fā)送至服務(wù)端。這里主要通過兩個系統(tǒng)的類來進行表情包和其描述文本的匹配。
- 1.
NSTextAttachment: 文本中的‘插件’,我們通過這個類來插入圖片。 - 2.
NSRegularExpression: 使用正則表達式來匹配字符串中的表情包描述文本。
關(guān)于正則表達式,這里有一篇比較全的語法:正則表達式。
這里我的正則匹配的字符如下:
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\[.+?\\]" options:0 error:NULL];
在匹配出每一個表情包描述文本之后,會生成一個數(shù)組存放這些匹配結(jié)果(描述文本、圖片資源、描述文本在原字符串的位置),然后遍歷這個數(shù)組,將這些描述文本通過插入圖片插件textAttachment來替換,這里注意,每次替換,后面還沒有被替換的表情包文本的range就會發(fā)送變化,我們需要遞減他們原來的位置range.location。
具體實現(xiàn)的方式可參考下面代碼:
// JPAttributedStringHelper.m
- (NSAttributedString *)getTextViewArrtibuteFromStr:(NSString *)str {
if(str.length == 0) {
return nil;
}
NSMutableAttributedString * attStr = [[NSMutableAttributedString alloc] initWithString:str
attributes:[JPAttributedStringConfig getAttDict]];
NSMutableParagraphStyle * paraStyle = [[NSMutableParagraphStyle alloc] init];
paraStyle.lineSpacing = 5;
[attStr addAttribute:NSParagraphStyleAttributeName value:paraStyle range:NSMakeRange(0, attStr.length)];
NSArray<JPEmojiMatchingResult *> * emojiStrArr = [self analysisStrWithStr:str];
if(emojiStrArr && emojiStrArr.count != 0) {
NSInteger offset = 0; // 表情包文本的偏移量
for(JPEmojiMatchingResult * result in emojiStrArr ){
if(result.emojiImage ){
// 表情的特殊字符
NSMutableAttributedString * emojiAttStr = [[NSMutableAttributedString alloc] initWithAttributedString:[NSAttributedString attributedStringWithAttachment:result.textAttachment]];
if(!emojiAttStr) {
continue;
}
NSRange actualRange = NSMakeRange(result.range.location - offset, result.range.length);
[attStr replaceCharactersInRange:actualRange withAttributedString:emojiAttStr];
// 一個表情占一個長度
offset += (result.range.length-1);
}
}
return attStr;
}else {
return [[NSAttributedString alloc] initWithString:str attributes:[JPAttributedStringConfig getAttDict]];;
}
}
實現(xiàn)的效果可以到博客或者下載demo查看。
而在按下刪除鍵,要實現(xiàn)刪除表情包描述文本,我們需要判斷textView.selectedRange所在的位置是否為表情描述文本,代碼參考如下:
// 點擊了文本消息和或者表情包鍵盤的刪除按鈕
- (void)clickDeleteBtnInputView:(JPEmojiInputView *)inputView {
NSString * souceText = [self.textView.text substringToIndex:self.textView.selectedRange.location];
if(souceText.length == 0) {
return;
}
NSRange range = self.textView.selectedRange;
if(range.location == NSNotFound) {
range.location = self.textView.text.length;
}
if(range.length > 0) {
[self.textView deleteBackward];
return;
}else {
// 正則表達式匹配要替換的文字的范圍
if([souceText hasSuffix:@"]"]){
// 表示該選取字段最后一個是表情包
if([[souceText substringWithRange:NSMakeRange(souceText.length-2, 1)] isEqualToString:@"]"]) {
// 表示這只是一個單獨的字符@"]"
[self.textView deleteBackward];
return;
}
// 正則表達式
NSString * pattern = @"\\[[a-zA-Z0-9\\u4e00-\\u9fa5]+\\]";
NSError *error = nil;
NSRegularExpression * re = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:&error];if (!re) {NSLog(@"%@", [error localizedDescription]);}
NSArray *resultArray = [re matchesInString:souceText options:0 range:NSMakeRange(0, souceText.length)];
if(resultArray.count != 0) {
/// 表情最后一段存在表情包字符串
NSTextCheckingResult *checkingResult = resultArray.lastObject;
NSString * resultStr = [souceText substringWithRange:NSMakeRange(0, souceText.length - checkingResult.range.length)];
self.textView.text = [self.textView.text stringByReplacingCharactersInRange:NSMakeRange(0, souceText.length) withString:resultStr];
self.textView.selectedRange = NSMakeRange(resultStr.length , 0);
}else {
[self.textView deleteBackward];
}
}else {
// 表示最后一個不是表情包
[self.textView deleteBackward];
}
}
// textView自適應(yīng)
[self textViewDidChange:self.textView];
}
實現(xiàn)效果大家可以看看demo??。
看到這里,我已經(jīng)寫了近5k字了??
表情包的預覽
表情包的預覽效果分為
- 1.小表情的預覽
- 2.大表情的預覽(gif播放)
展示
前者的底部視圖是一張已經(jīng)畫好的圖片,
后者的底部視圖我通過重繪機制(QuartzCore框架并且重寫-drawRect:方法)來進行描邊以及視圖顏色的填充(關(guān)于重繪制:iOS開發(fā)之drawRect的作用和調(diào)用機制),代碼:
// JPGIfPreview.m
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
//1.添加繪圖路徑
CGContextMoveToPoint(context,0,_filletRadius);
CGContextAddLineToPoint(context, 0, _squareHeight - _filletRadius);
CGContextAddQuadCurveToPoint(context, 0, _squareHeight ,_filletRadius, _squareHeight);
CGContextAddLineToPoint(context, (_squareWidht - _triangleWdith )/2,_squareHeight);
CGContextAddLineToPoint(context,BaseWidth /2 , BaseHeight);
CGContextAddLineToPoint(context, (_squareWidht + _triangleWdith )/2,_squareHeight);
CGContextAddLineToPoint(context, _squareWidht - _filletRadius,_squareHeight);
CGContextAddQuadCurveToPoint(context, _squareWidht, _squareHeight ,_squareWidht, _squareHeight - _filletRadius);
CGContextAddLineToPoint(context, _squareWidht ,_filletRadius);
CGContextAddQuadCurveToPoint(context, _squareWidht, 0 ,_squareWidht - _filletRadius, 0);
CGContextAddLineToPoint(context,_filletRadius ,0);
CGContextAddQuadCurveToPoint(context, 0, 0 ,0, _filletRadius);
//2.設(shè)置顏色屬性
CGFloat backColor[4] = {1,1,1, 0.86};
CGFloat layerColor[4] = {0.9,0.9,0.9,0};
//3.設(shè)置描邊顏色,填充顏色
CGContextSetFillColor(context, backColor);
CGContextSetStrokeColor(context, layerColor);
//4.繪圖
CGContextDrawPath(context, kCGPathFillStroke);
}
坐標換算
在完成了布局之后,接下來就是要將我們的預覽視圖添加到界面上。
在collectionView上面添加長按收拾longPress,監(jiān)聽手勢的狀態(tài),并且計算手勢所在位置對應(yīng)的cell,對其內(nèi)容進行預覽效果的展示。
這里我選擇了[UIApplication sharedApplication].windows.lastobject作為superView,即emojiInputView所在的window。
這里有個要注意的點,上面的windowCGPointZero是從手機左上角開始算起,因此換算坐標(cell是每一個表情包,補充一下我是用collectionView來展示每一個分頁上的表情包)時,我將cell.frame轉(zhuǎn)換成了在window上的frame:
CGRect rect = [[UIApplication sharedApplication].windows.lastObject convertRect:cell.frame fromView:self.collectionView];
坐標換算完成之后,剩下的就是添加上去。
gif播放
gif的播放效果,我也是第一次接觸,這里看到一篇不錯的文章:iOS-Gif圖片展示N種方式(原生+第三方),里面有介紹原生和第三方的實現(xiàn)。
考慮到減少項目的依賴庫,這里我就采用了里面原生方式的代碼,具體可以點開鏈接看內(nèi)部代碼,這里不做過多敘述了(5.3k字了??????)。
參考
這些文章對我提供了一定的幫助,也希望對你有用
WWDC 2017 - 優(yōu)化輸入體驗的關(guān)鍵:keyboard技巧全介紹
寫在最后
在完成JPChatBottomBar之后,整個框架訪問用戶編輯的消息或者是用戶的其他操作都可以通過JPChatBottomBarDelegate獲取。
JPChatBottomBar 部分就先到這里,完成這部分內(nèi)容,從demo到文章落筆完成之間,遇到了挺多問題??。
例如‘切換鍵盤覆蓋的問題’那一塊自己就用了兩種方法來實現(xiàn),亦或者是‘表情包組別的切換’,自己都花了有些時間。
針對我的文章和demo中技術(shù)的實現(xiàn),如果讀者有更好的方法,歡迎提出??,謝謝??。
也希望我的文章能夠給你帶來幫助。
如果對你有所幫助,請給我個Star吧?。謝謝!