iOS汽包聊天界面实现思路
现在几乎所有的社交类APP都会需要做气泡聊天界面,而对于没有做过的同学来说还是有一定难度的。那么这篇博文就是记录一下我自己在实现过程遇到一些问题和解决办法。
先来几张截图给大家看看最终的结果:
这是一般状态
这是长按弹出菜单状态
这是编辑模式
下面列出了实现气泡聊天界面需要解决的一些技术问题。
1. 使用iOS8自动算高cell,还是使用手动计算frame的cell;
2. 气泡cell要能自动识别URL,并提供点击,长按等响应;
3. 图片需要裁减成气泡形状,为了和背景区别还要给图片加阴影;
4. tableview进去编辑模式时cell外观的变化和事件响应。
那么现在来一个一个解答。
1, 通过我亲自测试,使用自动算高cell不可取,自动算高的cell在高度不发生变化的情况下(所谓高度不发生变化指的是cell在重用的时候高度不发生变化)滑动并不卡顿现象(即使高度发生变化,滑动过一次后卡顿就消失了,可见apple对约束的计算结果做了缓存,未来可期),但是当我们在聊天过程发送一条新消息并调用insertRowsAtIndexPaths:插入cell后再进行滑动就会有明显的卡顿现象。所以如果你想要丝滑般的滑动,那么不要使用autolayout来实现自动算高的cell。同理可推,所有复杂的cell都不推荐使用autolayout加自动算高来实现,乖乖地手动计算frame才是最省事省力的方法。关于autolayout和手动计算frame性能的比较可以看这篇文章。
这里有一个autolayout比较大坑要提醒大家注意,translatesAutoresizingMaskIntoConstraints这个属性所有的uiview都有,根据apple的文档,这个属性的作用是把之前的用auto resize mask设置的位置约束自动转换成autolayout的constraint。这玩意儿的初衷是让auto layout系统能追踪视图中哪些手动控制的frame的变化(比如你在代码中通过-setFrame:来控制视图的大小位置的时候),这个时候你不可能加入更多的约束,而不导致冲突。所以当你选择添加自己的约束来控制视图的布局的时候,你必须把这个属性设置为NO。在IB中设置约束的时候,IB会自动为你把这个属性设置为NO。
然而在uitabelviewcell里面,Apple没有言行一致。在自定义的cell中使用xib并使用约束时,translatesAutoresizingMaskIntoConstraints并没有自动设置为NO。而且你也不能把其设置为NO,为什么不能?有兴趣的同学可以试试设置为NO的效果。进一步思考,为啥cell的translatesAutoresizingMaskIntoConstraints不能为NO。个人认为,tableview在计算cell的高度的时候还是使用了setFrame等方法,而不全是用autolayout添加约束来实现的,所以不能设为NO,设置为NO就会导致cell的frame计算出错。
2, 气泡对URL的识别,对用户操作的响应。在我的实现过程中,我对聊天气泡做了分类,分为:文本聊天cell,图片聊天cell,语音聊天cell。而这些cell有共同的父类,父类主要负责对用户长按气泡之后弹出uimenucontroller。以及一些公共部件(例如用户头像,昵称)的frame计算。
这里重点介绍文本聊天cell识别连接,识别用户点击连接等功能的实现。文本聊天cell中文本的显示使用了uilabel控件,我们知道uilabel是可以显示attributedString的这就表示识别显示链接是没有问题。而同时uilabel是无法响应用户事件的,我们为了能够让uilabel能够接受用户的点击需要做以下工作:
①把目标label的userInteractionEnabled设置为yes;
②为目标label添加UITapGestureRecognizer;
③响应用户的点击事件时,计算点击区域是否在某个带有URL的文字上,并对该次点击进行处理。
这些操作都没有什么难度,除了第三步。不过幸好我们有强大的GitHub,大神们已经替我们解决第三步的所有问题,在实现的过程中是使用了TTTAttributedLabel,解决了步骤三中的所有问题。
气泡通常要响应用户的长按并弹出一个uimenucontroller,给用户提供复制、收藏等功能。实现这个功能需要做以下几步:
①覆盖气泡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能变成第一响应者,以及在uimenucontroller弹出的时候能够出现那些选项来提供给用户使用。
②为汽包cell添加UILongPressGestureRecognizer以响应用户的长按操作,我们可以在气泡的cell内初始化一个UILongPressGestureRecognizer,至于说这个手势识别器要加在那个控件上由子类来决定。比如:对于文本汽包cell来说,这个手势可以加在背景气泡的uiimageview上,也可以加在显示文本label上(注意我使用TTTAttributedLabel,其本身是有长按手势识别器的,所以可以不用加长按手势,但是需要修改他的源码,让其识别用户未在URL文字上长按时也能调用相关的delegate方法,具体可以看我修改后源码,稍后附上)。
③在手势响应的代码中设置uimenucontroller的相关参数,像下面这样:
- (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为目标显示出来。可以在子类中进行设置。
3,图片裁减和添加阴影,其实这并没有什么难度,但是通过实践发现很多实例代码都无法使用。所以要在这里提一下,以下代码绝对有效:
- (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);设置的阴影还有些问题,阴影总是在某一侧不完整,我还在继续寻找答案。如果有知道的大神也一定要告诉我。
4,tableview进去编辑模式时cell外观的变化和事件响应。这个本来也是不需要特别讲的,但是网络上的许多实例代码都无法正确工作。我的实现步骤如下:
①实现tableview的delegate方法,像下面这样:(这步的作用是取消cell默认的编辑模式控制)
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewCellEditingStyleNone;
}
而不是像其他人所说的实现这个方法:
- (BOOL)tableView:(UITableView *)tableView shouldIndentWhileEditingRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
return NO;
}
反正我实现这个方法并没有什么卵用。
②在气泡cell的父类中覆盖如下方法:
- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
if(editing) {
[selfbuildEidtingFrame];
}
else{
editingFrame=CGRectZero;
}
[supersetEditing:editinganimated:animated];
}
[self buildEidtingFrame];中只是设置了编辑模式下cell应该显示控件的frame,这会导致cell的layoutsubviews调用,而在layoutsubviews中有我们的布局逻辑,这样就能让cell正常地进入编辑模式的显示样式了。
今天先写到这里,明天我们讨论一些其他问题:比如如何把录音获得的mcp文件,在录音的过程中转换成mp3。录音和播放的过程中如何获取音量大小的坑等。
现在我将自己的cell和demo上传到了github目前只实现了基本功能,由于最近上班较忙,会择时更新。