現(xiàn)在幾乎所有的社交類APP都會需要做氣泡聊天界面,而對于沒有做過的同學(xué)來說還是有一定難度的。那么這篇博文就是記錄一下我自己在實現(xiàn)過程遇到一些問題和解決辦法。
先來幾張截圖給大家看看最終的結(jié)果:
這是一般狀態(tài)
這是長按彈出菜單狀態(tài)
這是編輯模式
下面列出了實現(xiàn)氣泡聊天界面需要解決的一些技術(shù)問題。
1. 使用iOS8自動算高cell,還是使用手動計算frame的cell;
2. 氣泡cell要能自動識別URL,并提供點擊,長按等響應(yīng);
3. 圖片需要裁減成氣泡形狀,為了和背景區(qū)別還要給圖片加陰影;
4. tableview進(jìn)去編輯模式時cell外觀的變化和事件響應(yīng)。
那么現(xiàn)在來一個一個解答。
1, 通過我親自測試,使用自動算高cell不可取,自動算高的cell在高度不發(fā)生變化的情況下(所謂高度不發(fā)生變化指的是cell在重用的時候高度不發(fā)生變化)滑動并不卡頓現(xiàn)象(即使高度發(fā)生變化,滑動過一次后卡頓就消失了,可見apple對約束的計算結(jié)果做了緩存,未來可期),但是當(dāng)我們在聊天過程發(fā)送一條新消息并調(diào)用insertRowsAtIndexPaths:插入cell后再進(jìn)行滑動就會有明顯的卡頓現(xiàn)象。所以如果你想要絲滑般的滑動,那么不要使用autolayout來實現(xiàn)自動算高的cell。同理可推,所有復(fù)雜的cell都不推薦使用autolayout加自動算高來實現(xiàn),乖乖地手動計算frame才是最省事省力的方法。關(guān)于autolayout和手動計算frame性能的比較可以看這篇文章。
這里有一個autolayout比較大坑要提醒大家注意,translatesAutoresizingMaskIntoConstraints這個屬性所有的uiview都有,根據(jù)apple的文檔,這個屬性的作用是把之前的用auto resize mask設(shè)置的位置約束自動轉(zhuǎn)換成autolayout的constraint。這玩意兒的初衷是讓auto layout系統(tǒng)能追蹤視圖中哪些手動控制的frame的變化(比如你在代碼中通過-setFrame:來控制視圖的大小位置的時候),這個時候你不可能加入更多的約束,而不導(dǎo)致沖突。所以當(dāng)你選擇添加自己的約束來控制視圖的布局的時候,你必須把這個屬性設(shè)置為NO。在IB中設(shè)置約束的時候,IB會自動為你把這個屬性設(shè)置為NO。
然而在uitabelviewcell里面,Apple沒有言行一致。在自定義的cell中使用xib并使用約束時,translatesAutoresizingMaskIntoConstraints并沒有自動設(shè)置為NO。而且你也不能把其設(shè)置為NO,為什么不能?有興趣的同學(xué)可以試試設(shè)置為NO的效果。進(jìn)一步思考,為啥cell的translatesAutoresizingMaskIntoConstraints不能為NO。個人認(rèn)為,tableview在計算cell的高度的時候還是使用了setFrame等方法,而不全是用autolayout添加約束來實現(xiàn)的,所以不能設(shè)為NO,設(shè)置為NO就會導(dǎo)致cell的frame計算出錯。
2, 氣泡對URL的識別,對用戶操作的響應(yīng)。在我的實現(xiàn)過程中,我對聊天氣泡做了分類,分為:文本聊天cell,圖片聊天cell,語音聊天cell。而這些cell有共同的父類,父類主要負(fù)責(zé)對用戶長按氣泡之后彈出uimenucontroller。以及一些公共部件(例如用戶頭像,昵稱)的frame計算。
這里重點介紹文本聊天cell識別連接,識別用戶點擊連接等功能的實現(xiàn)。文本聊天cell中文本的顯示使用了uilabel控件,我們知道uilabel是可以顯示attributedString的這就表示識別顯示鏈接是沒有問題。而同時uilabel是無法響應(yīng)用戶事件的,我們?yōu)榱四軌蜃寀ilabel能夠接受用戶的點擊需要做以下工作:
①把目標(biāo)label的userInteractionEnabled設(shè)置為yes;
②為目標(biāo)label添加UITapGestureRecognizer;
③響應(yīng)用戶的點擊事件時,計算點擊區(qū)域是否在某個帶有URL的文字上,并對該次點擊進(jìn)行處理。
這些操作都沒有什么難度,除了第三步。不過幸好我們有強(qiáng)大的GitHub,大神們已經(jīng)替我們解決第三步的所有問題,在實現(xiàn)的過程中是使用了TTTAttributedLabel,解決了步驟三中的所有問題。
氣泡通常要響應(yīng)用戶的長按并彈出一個uimenucontroller,給用戶提供復(fù)制、收藏等功能。實現(xiàn)這個功能需要做以下幾步:
①覆蓋氣泡cell的如下方法:
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
return (action == @selector(copy:) ||
action == @selector(relay:) ||
action == @selector(collect:)) ||
action == @selector(more:);
}
這兩個方法是繼承自uiview的,他們作用就是表示該uiview能變成第一響應(yīng)者,以及在uimenucontroller彈出的時候能夠出現(xiàn)那些選項來提供給用戶使用。
②為汽包cell添加UILongPressGestureRecognizer以響應(yīng)用戶的長按操作,我們可以在氣泡的cell內(nèi)初始化一個UILongPressGestureRecognizer,至于說這個手勢識別器要加在那個控件上由子類來決定。比如:對于文本汽包cell來說,這個手勢可以加在背景氣泡的uiimageview上,也可以加在顯示文本label上(注意我使用TTTAttributedLabel,其本身是有長按手勢識別器的,所以可以不用加長按手勢,但是需要修改他的源碼,讓其識別用戶未在URL文字上長按時也能調(diào)用相關(guān)的delegate方法,具體可以看我修改后源碼,稍后附上)。
③在手勢響應(yīng)的代碼中設(shè)置uimenucontroller的相關(guān)參數(shù),像下面這樣:
- (void)longPressHandler:(UILongPressGestureRecognizer*)sender {
if(sender.state!=UIGestureRecognizerStateBegan) return;
[selfbecomeFirstResponder];
UIMenuItem*relay = [[UIMenuItemalloc]initWithTitle:@"Relay"action:@selector(relay:)];
UIMenuItem*upload = [[UIMenuItemalloc]initWithTitle:@"Upload"action:@selector(upload:)];
UIMenuItem*collect = [[UIMenuItemalloc]initWithTitle:@"Collect"action:@selector(collect:)];
UIMenuItem*more = [[UIMenuItemalloc]initWithTitle:@"More"action:@selector(more:)];
UIMenuController*menu = [UIMenuControllersharedMenuController];
[menusetMenuItems:[NSArrayarrayWithObjects:relay, upload, collect, more,nil]];
[menusetTargetRect:self.menuTargetView.frameinView:self];
[menusetMenuVisible:YESanimated:YES];
}
這里有一個self.menuTargetView,這個view我是氣泡父類的一個屬性,是uiview類型,他的作用是定位menu以那個view為目標(biāo)顯示出來。可以在子類中進(jìn)行設(shè)置。
3,圖片裁減和添加陰影,其實這并沒有什么難度,但是通過實踐發(fā)現(xiàn)很多實例代碼都無法使用。所以要在這里提一下,以下代碼絕對有效:
- (UIImage*) maskWithImage:(constUIImage*) maskImage
{
constCGColorSpaceRefcolorSpace =CGColorSpaceCreateDeviceRGB();
constCGImageRefmaskImageRef = maskImage.CGImage;
constCGContextRefmainViewContentContext =CGBitmapContextCreate(NULL, maskImage.size.width, maskImage.size.height,8,0, colorSpace,kCGImageAlphaPremultipliedLast);
CGColorSpaceRelease(colorSpace);
if(! mainViewContentContext)
{
returnnil;
}
CGFloatratio = maskImage.size.width/self.size.width;
if(ratio *self.size.height< maskImage.size.height)
{
ratio = maskImage.size.height/self.size.height;
}
constCGRectmaskRect=CGRectMake(0,0, maskImage.size.width, maskImage.size.height);
constCGRectimageRect=CGRectMake(-((self.size.width* ratio) - maskImage.size.width) /2,
-((self.size.height* ratio) - maskImage.size.height) /2,
self.size.width* ratio,
self.size.height* ratio);
CGContextClipToMask(mainViewContentContext, maskRect, maskImageRef);
CGContextSetShadowWithColor(mainViewContentContext,CGSizeMake(0,0),1, [UIColorblackColor].CGColor);
CGContextDrawImage(mainViewContentContext, imageRect,self.CGImage);
CGImageRefnewImage =CGBitmapContextCreateImage(mainViewContentContext);
CGContextRelease(mainViewContentContext);
UIImage*theImage = [UIImageimageWithCGImage:newImage];
CGImageRelease(newImage);
return theImage;
}
這個方法唯一的問題是CGContextSetShadowWithColor(mainViewContentContext,CGSizeMake(0,0),1, [UIColorblackColor].CGColor);設(shè)置的陰影還有些問題,陰影總是在某一側(cè)不完整,我還在繼續(xù)尋找答案。如果有知道的大神也一定要告訴我。
4,tableview進(jìn)去編輯模式時cell外觀的變化和事件響應(yīng)。這個本來也是不需要特別講的,但是網(wǎng)絡(luò)上的許多實例代碼都無法正確工作。我的實現(xiàn)步驟如下:
①實現(xiàn)tableview的delegate方法,像下面這樣:(這步的作用是取消cell默認(rèn)的編輯模式控制)
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewCellEditingStyleNone;
}
而不是像其他人所說的實現(xiàn)這個方法:
- (BOOL)tableView:(UITableView *)tableView shouldIndentWhileEditingRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
return NO;
}
反正我實現(xiàn)這個方法并沒有什么卵用。
②在氣泡cell的父類中覆蓋如下方法:
- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
if(editing) {
[selfbuildEidtingFrame];
}
else{
editingFrame=CGRectZero;
}
[supersetEditing:editinganimated:animated];
}
[self buildEidtingFrame];中只是設(shè)置了編輯模式下cell應(yīng)該顯示控件的frame,這會導(dǎo)致cell的layoutsubviews調(diào)用,而在layoutsubviews中有我們的布局邏輯,這樣就能讓cell正常地進(jìn)入編輯模式的顯示樣式了。
今天先寫到這里,明天我們討論一些其他問題:比如如何把錄音獲得的mcp文件,在錄音的過程中轉(zhuǎn)換成mp3。錄音和播放的過程中如何獲取音量大小的坑等。
現(xiàn)在我將自己的cell和demo上傳到了github目前只實現(xiàn)了基本功能,由于最近上班較忙,會擇時更新。