在近期的一个项目中,用到了客服功能,然而聊天的实现并不是用的即时通讯的SDK,而是用的一个网页版的某通,这就需要自己写一个聊天的界面,下面总结一下这个聊天界面的搭建过程。
一、基础UI
谈到聊天界面,我们最熟悉的就是QQ、微信等的聊天界面,看到这两种的聊天界面的形式,都是将消息逐条展示出来,我们不难想到,聊天界面的主体要用UItableView实现,而每条消息就是一个cell,至于cell中的内容,无非就是三项:时间、对话人的头像、消息的内容。既然大概结构已经清楚,那么下面我们俩看看具体的实现。
1、数据的处理
在自定义cell之前,我们首先来看看消息的数据结构,将数据转换成Model,以便对数据进行更方便的处理。
数据看起来非常简单,其意义分别为:text-消息内容;time-消息的发送时间;type-消息的类型(0:自己发送的消息,1:对方发送的消息)。
那么我们在定制Model的时候是不是就只需要这几种属性呢?答案是NO。因为我们知道,聊天的内容不是只有文本内容,有时候还会有图片,表情,语音,设置还会有文件,那么我们就还需要一个区分消息种类的属性messageType。此外我们知道,QQ聊天界面中,并不是所有的消息发送时间都会显示的,只有当新消息与上一条消息的发送时间不同时才会显示时间,那么我们就还需要一个区别时间是否展示的属性hiddenTime。
下面是消息模型XTMessage的属性:
@property (nonatomic, copy) NSString *time;//对话类型:0:自己的消息;1:对方的消息
@property (nonatomic, strong) NSNumber *type;//消息类型:0:文字消息;1:图片;2:文件
@property(nonatomic,assign)int messageType;
@property(nonatomic,copy)NSString *message;//文本消息
@property(nonatomic,strong)UIImage *messageImage;//图片消息
@property(nonatomic,strong)NSData *messageData;//文件消息
@property (nonatomic, assign, getter = isHiddenTime) BOOL hiddenTime;//是否隐藏时间
下面是字典转模型的具体实现方法:
- (instancetype)initWithDictionary:(NSDictionary *)dict
{
self = [super init];
if (self) {
_type = dict[@"type"];
_time = dict[@"time"];
if (_messageType == 0) {//文本消息
_message = dict[@"text"];
} else if (_messageType == 1) {//图片消息
_messageImage = [UIImage imageNamed:dict[@"text"]];
} else {//文件或语音消息
_messageData = dict[@"text"];
}
}
return self;
}
2、FrameModel的处理
上面我们将原始数据处理完了,接下来我们就要为自定义cell做准备了。对于cell中的控件无非就是要显示头像的一个UIimageView,一个显示时间的label,然后就是展示消息内容的,而展示内容我们这里选择的是UIbutton,因为UIbutton既可以展示文本,又能展示图片,能最大限度满足我们的需求。这里我们要注意了,首先对于消息发送者的不同,头像的位置是不同的;消息内容的不同,展示内容控件的size也是动态变化的。下面我们说说对此的处理。
首先我们声明一下XTMessageFrame的公开属性:
@property (nonatomic, assign, readonly) CGRect titleLabelFrame;//时间标题的高度
@property (nonatomic, assign, readonly) CGRect contentBtnFrame;
@property (nonatomic, assign, readonly) CGRect iconImageViewFrame;
@property (nonatomic, assign) CGFloat cellHeight;//行高
@property(nonatomic,strong)XTMessage *messageModel;
下面我们来实现XTMessageFrame的私有方法。首先我们先定义一个- (CGSize)sizeWithText:(NSString *)text;根据文本长度自适应大小,代码如下:
- (CGSize)sizeWithText:(NSString *)text {
return [text boundingRectWithSize:CGSizeMake(SCREEN_WIDTH - W(150), MAXFLOAT)options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName : FONTONE} context:nil].size;
}
下面我们来实现MessageModel的set方法:
首先判断时间label的高度:
//设置时间标题label的高度 如果时间隐藏 高度为0 即不显示
CGFloat titleLabelHeight = messageModel.hiddenTime ? 0 : H(30);
时间label的frame:
_titleLabelFrame = CGRectMake(0, 0, SCREEN_WIDTH, titleLabelHeight);
下面我们定义两个宏:
#define kContentBtnWidth (contentBtnSize.width + W(40))//内容的宽度
#define kContentBtnHeight (contentBtnSize.height + H(35))//内容的高度
下面根据消息的类型,得出内容按钮的size:
CGSize contentBtnSize;
if (_messageModel.messageType == 0) {
//文字大小
contentBtnSize = [self sizeWithText:_messageModel.message];
} else if (_messageModel.messageType == 1) {
CGSize size = _messageModel.messageImage.size;
contentBtnSize = CGSizeMake(W(150), size.height/size.width * W(150));
} else {
contentBtnSize = CGSizeMake(W(150), H(150));
}
接下来根据发送消息的对象,设置头像和内容button的frame:
//头像大小 50 * 50
if ([_messageModel.type isEqualToNumber:@0]) {//如果是自己发送的消息
_iconImageViewFrame = CGRectMake(SCREEN_WIDTH - iconWH - margin, CGRectGetMaxY(_titleLabelFrame), iconWH, iconWH);//头像在左侧
_contentBtnFrame = CGRectMake(SCREEN_WIDTH - kContentBtnWidth - iconWH - margin - margin, CGRectGetMaxY(_titleLabelFrame), kContentBtnWidth, kContentBtnHeight);//设置内容button
}else {//如果是对方发送的消息
_iconImageViewFrame = CGRectMake(margin, CGRectGetMaxY(_titleLabelFrame), iconWH, iconWH);//头像在右侧
_contentBtnFrame = CGRectMake(margin + iconWH + margin,CGRectGetMaxY(_titleLabelFrame),kContentBtnWidth,kContentBtnHeight);
}
最后根据cell中控件的frame设置cell的行高:
_cellHeight = MAX(CGRectGetMaxY(_contentBtnFrame), CGRectGetMaxY(_titleLabelFrame)) + margin;
3、自定义cell
首先重写initWithStyle方法,在cell中初始化timeTitleLab(时间label)、headImageView(头像imageView)、contentBtn(内容按钮),并进行一些初始设置。
实现setMessageFrame方法:
- (void)setMessageFrame:(XTMessageFrame *)messageFrame
{
_messageFrame = messageFrame;
[self setSubViewData];
[self setSubViewFrame];
}
setSubViewFrame方法,就是给cell中控件的frame赋值,这个方法相信就不用多说了,我们来说说setSubViewData方法,在这个方法中,我们要做的就是根据消息发送人,以及消息类型,分别给控件的内容赋值,比如当消息是自己发送的时候,代码如下:
self.headImageView.image = [UIImage imageNamed:@"me"];
UIImage *btnBackImage = [UIImage imageNamed:@"chat_send_nor"];
[self.contentBtn setBackgroundImage:[btnBackImage stretchableImageWithLeftCapWidth:btnBackImage.size.width / 2 topCapHeight:btnBackImage.size.height / 2] forState:UIControlStateNormal];
UIImage *btnBackImageHelight = [UIImage imageNamed:@"chat_send_press_pic"];
[self.contentBtn setBackgroundImage:[btnBackImageHelight stretchableImageWithLeftCapWidth:btnBackImageHelight.size.width / 2 topCapHeight:btnBackImageHelight.size.height / 2] forState:UIControlStateHighlighted];
if (self.messageFrame.messageModel.messageType == 0) {//如果是文本信息
[self.contentBtn setTitle:self.messageFrame.messageModel.message forState:UIControlStateNormal];
} else if (self.messageFrame.messageModel.messageType == 1) {
[self.contentBtn setImage:self.messageFrame.messageModel.messageImage forState:UIControlStateNormal];
[self.contentBtn setImageEdgeInsets:UIEdgeInsetsMake(H(20), W(20), H(20), W(20))];
} else {
[self.contentBtn setImage:[UIImage imageNamed:@"wenj"] forState:UIControlStateNormal];
}
这里要强调一点,对于聊天内容button的backgroundImage,需要根据内容的大小进行大小的拉伸,这里我们用到的是- (UIImage *)stretchableImageWithLeftCapWidth:(NSInteger)leftCapWidth topCapHeight:(NSInteger)topCapHeight方法,这个函数是UIImage的一个实例函数,它的功能是创建一个内容可拉伸,而边角不拉伸的图片,需要两个参数,第一个是左边不拉伸区域的宽度,第二个参数是上面不拉伸的高度。我自己的理解(认识)是在左边、上面找一个基点,对基点范围以外的内容拉伸。
具体的代码已上传GitHub:XTChatViewController