在近期的一個項目中,用到了客服功能,然而聊天的實現并不是用的即時通訊的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