最近把公司项目的聊天模块从”XMPP”转到”网易云信”官网、github。在转的过程中,上手很快,基本上没遇到什么难题,很多程度上感谢云信的NIMKit。以前也接触过几个IM SDK服务商的代码,那个时候看他们的代码根本没什么欲望,但这次看云信的代码有种被吸引的感觉,恨不得一下子把它的代码全部看完,封装的很好,扩展性很强(ps:就我目前的水平只能说出这些优点)。对于一般的聊天UI完全可以满足,就算不用网易的IM SDK,但他们的代码真的值得一下(尽管他们的UIKit代码注释比较少)。
Base tipscell的组成结构
对于聊天”MessageCell”的介绍,一定要记住下面这张图片,以及相关参数的解释。\' z$ e L8 w [) H
- 蓝色区域:为具体内容,如文字 UILabel ,图片 UIImageView 等 。(对应的NIMMessageModel对象的contentSize属性)。注:NIMMessageModel为消息(NIMMessage) 在NIMKit中的封装。这个封装主要是为了对计算结果和布局配置进行缓存,以避免反复的计算和读取相同的信息,从而提高应用性能。
- 绿色区域:为消息的气泡,具体的内容和气泡之间会有一定的内间距,这里为 contentViewInsets 。(对应的NIMMessageModel对象的contentViewInsets属性)
- 紫色区域:为整个 UITableViewCell ,具体的气泡和整个cell会有一定的内间距,这里为 cellInsets 。(对应的NIMMessageModel对象的bubbleViewInsets属性)
config配置协议
在聊天界面有几个config配置代理,先熟悉一下。
- NIMSessionConfig:消息对应的session配置。如:录音、输入框、表情、更多等操作的选择;点击”+”号出来的多媒体按钮;是否禁用输入控件;输入控件的最大长度;输入控件的placeholder;一次最多消息的消息内容;间隔多久显示时间;语音红点是否禁用;是否自动切换成听筒模式;是否自动获取历史消息;消息数据提供器;消息的排版配置等。可以说这个是贯穿整个聊天模块的配置,修改聊天界面一般就得调整这里。
- NIMCellLayoutConfig:消息对应的布局配置。我们可以在这个config里面根据消息类型是否显示头像、姓名、头像与姓名之间的margin等;然后你会在项目里面看到自定义消息类型对应的NTESSessionCustomLayoutConfig,以及default默认的配置NIMCellLayoutDefaultConfig;
- NIMSessionContentConfig:消息内容配置。这个配置主要是为NIMSessionMessageContentView(请看下面对 聊天 NIMMessageCell.h 的介绍)对象为设置的。2 d5 ^/ A# i1 m
聊天 NIMMessageCell.h
先来看看头文件定义的属性
- @property (nonatomic, strong) NIMAvatarImageView *headImageView;
- @property (nonatomic, strong) UILabel *nameLabel; //姓名(群显示 个人不显示)0 C4 ?3 h& J& `5 E1 x. l
- @property (nonatomic, strong) NIMSessionMessageContentView *bubbleView; //内容区域
- @property (nonatomic, strong) UIActivityIndicatorView *traningActivityIndicator; //发送loading. G: I; t6 R8 L a& C( f" q
- @property (nonatomic, strong) UIButton *retryButton; //重试" y7 E% q! E3 j\' p7 n
- @property (nonatomic, strong) NIMBadgeView *audioPlayedIcon; //语音未读红点
复制代码
NIMSessionMessageContentView,顾名思义就是MessageCell的内容View(包括下面的bubble气泡View)。而 NIMSessionContentConfig 配置主要是配置 contentSize、contentViewInsets以及这个配置所应的 messageContentView 类名(NIMSessionMessageContentView的子类,每种聊天类型对应一个messageContentView)。注意,这里并没有提到 bubbleViewInsets,因为气泡隔cell的距离不会因不同类型而改变,我们只需在 cellLayoutConfig 里面处理即可,当然想要做到不同的话,也可以在 NIMSessionContentConfig 配置里面增加一个协议方法。注意 NIMSessionMessageContentView 是继承自 UIControl,这样不仅能处理点击事件,还能很好的处理点击高亮的效果。
NIMSessionViewController(聊天回话控制器基类)
先来看看最重要的计算高度方法
- - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
- {
- CGFloat cellHeight = 0;" }! x8 j! M7 u3 c! W+ u* Y
- id modelInArray = [[_sessionDatasource modelArray] objectAtIndex:indexPath.row];
- if ([modelInArray isKindOfClass:[NIMMessageModel class]])4 [/ p\' y! ^8 V
- {
- NIMMessageModel *model = (NIMMessageModel *)modelInArray;0 T/ ^) P9 R5 J. D" A\' a. z7 q, ]! T
- NSAssert([model respondsToSelector:@selector(contentSize)], @"config must have a cell height value!!!");% C. m3 @( |3 L\' ~# t+ a K& N
- [self layoutConfig:model];; Z$ ~) k& |$ `6 m
- CGSize size = model.contentSize;! i7 C! B. d P
- UIEdgeInsets contentViewInsets = model.contentViewInsets;
- UIEdgeInsets bubbleViewInsets = model.bubbleViewInsets;4 i2 ^- `! e, e) [) a
- cellHeight = size.height + contentViewInsets.top + contentViewInsets.bottom + bubbleViewInsets.top + bubbleViewInsets.bottom;" @. V( j% ]; S. ^( J
- }
- else if ([modelInArray isKindOfClass:[NIMTimestampModel class]])
- {
- cellHeight = [modelInArray height];! v- S% w" t9 i" M
- }$ ~5 t7 b& j! K; e\' D( a$ ^
- else d; K8 G9 n. f0 V6 |& x+ M/ h. j
- {8 k, s% t7 s q M% H! E! p4 \
- NSAssert(0, @"not support model");
- }1 ?% x; Z- P2 I _\' z$ }
- return cellHeight;0 _4 w4 I. W; ] a! T7 M2 j
- }
复制代码
某个数据源所对应的高度就是三个颜色区域的高度之和(contentSize.height + contentViewInsets.top + contentViewInsets.bottom + bubbleViewInsets.top + bubbleViewInsets.bottom)。5 R8 _\' Y8 K7 A, F 然后我们在来看看layoutConfig:方法:
- - (void)layoutConfig:(NIMMessageModel *)model{0 z; ^4 F5 v8 \8 N% T; R0 j% |
- model.sessionConfig = self.sessionConfig;- Y- ^! t4 M8 W& a, w% W: D6 \
- if (model.layoutConfig == nil)
- {
- id layoutConfig = nil;
- if ([self.sessionConfig respondsToSelector:@selector(layoutConfigWithMessage:)]) {
- layoutConfig = [self.sessionConfig layoutConfigWithMessage:model.message];" j( C5 B$ _, u/ {
- }, g5 i( t! J4 ]1 [% T7 j
- if (!layoutConfig) {. J5 Y* c- r7 _" b1 D: y
- layoutConfig = [NIMDefaultValueMaker sharedMaker].cellLayoutDefaultConfig;2 a* o7 k0 }; u" z6 B) n" z, e
- }) @5 J) F, ?7 ?" j
- model.layoutConfig = layoutConfig;
- }
- [model calculateContent:self.tableView.nim_width];
- }
复制代码
其实这里就是,先配置model的sessionConfig,然后配置layoutConfig,配置完后就去计算该model所对应内容的contentSize。注:layoutConfigWithMessage:方法是自定义消息类型需要处理,还有记得在写代码中做好判nil的处理,如果为nil的话给default值。
好了,现在到了一个我当时比较蛋疼的地方了,请看下图8 C+ V: h, o* k0 ?; b <ignore_js_op> Image2/ J2 \% w/ y9 b 看到很多的方法,仔细看看,除了layoutConfig的配置方法以外,还有很多Attachment(Attachment 是属于自定义消息的配置协议)结尾的方法,其实这里应该只会提示NIMCellLayoutDefaultConfig和NTESSessionCustomLayoutConfig(NTESChatroomCellLayoutConfig聊天室的布局配置请忽略)才对,它们只是方法名相同,应该是Xcode抽风而导致的。
NIMCellLayoutDefaultConfig计算contentSize
先看三个相关部分的代码
NIMCellLayoutDefaultConfig
- - (CGSize)contentSize:(NIMMessageModel *)model cellWidth:(CGFloat)cellWidth{
- 7 B$ z8 {: \- F, E/ q1 E
- idconfig = [[NIMSessionContentConfigFactory sharedFacotry] configBy:model.message];/ r3 M8 }& D" K2 c. p/ O) A
- return [config contentSize:cellWidth];
- }) z$ r5 \" b4 p# y( y* h% M& G9 p( T& F
- 0 Y; r3 w9 t+ i5 |" @3 Y5 g
- - (NSString *)cellContent:(NIMMessageModel *)model{
- 1 |8 G& L& \4 }4 i) \) g
- idconfig = [[NIMSessionContentConfigFactory sharedFacotry] configBy:model.message];
- NSString *cellContent = [config cellContent];5 x\' H+ e- ~2 G: T& n1 d
- return cellContent ? : @"NIMSessionUnknowContentView";
- }* _" V7 g% ]% ~, w: z: ~
-
- 1 u t, Y$ E% g% X8 i6 t. L+ w
- - (UIEdgeInsets)contentViewInsets:(NIMMessageModel *)model{7 g\' R9 a+ ]5 o% C1 o
- idconfig = [[NIMSessionContentConfigFactory sharedFacotry] configBy:model.message];
- return [config contentViewInsets];* \# l2 B9 I/ D0 m# q u
- }
复制代码
NIMSessionContentConfigFactory
- - (instancetype)init5 n& i\' ?4 z0 c4 N: c T
- {: w" g, t" a( b" h
- if (self = [super init])$ Z4 U. n: Y$ n; M1 W! z2 H4 h
- {
- _dict = @{@(NIMMessageTypeText) : [NIMTextContentConfig new],. J2 a& ?# v6 K
- @(NIMMessageTypeImage) : [NIMImageContentConfig new],& J( M$ i* U X9 s
- @(NIMMessageTypeAudio) : [NIMAudioContentConfig new],
- @(NIMMessageTypeVideo) : [NIMVideoContentConfig new],) [3 h2 e; H* l\' ]/ T
- @(NIMMessageTypeFile) : [NIMFileContentConfig new],
- @(NIMMessageTypeLocation) : [NIMLocationContentConfig new],
- @(NIMMessageTypeNotification) : [NIMNotificationContentConfig new],- r5 y" |\' Z! V4 m, N5 i
- @(NIMMessageTypeTip) : [NIMTipContentConfig new]};
- }/ w7 ~" Z5 m$ [# U( K
- return self;
- }) a0 ~( K1 k1 g3 Y" G) y4 w0 y! E
-
- - (id)configBy:(NIMMessage *)message
- {# f$ h$ b* w+ M2 d
- NIMMessageType type = message.messageType;/ U: K- O* Q8 ~4 w6 V\' H
- idconfig = [_dict objectForKey:@(type)];# t# e% H( H7 ?- |5 G$ d- E
- if (config == nil)
- {
- config = [NIMUnsupportContentConfig sharedConfig];7 A, \- u6 O i4 n. n
- }
- if ([config isKindOfClass:[NIMBaseSessionContentConfig class]])
- {$ r" Z# x- t0 `& c" l& [9 U
- [(NIMBaseSessionContentConfig *)config setMessage:message];0 `2 G\' M* _* X$ f" T6 B
- }7 t# K7 t; F; d1 I+ A$ k
- return config;$ j8 Z3 J" j+ |) u\' X
- }
复制代码
NIMImageContentConfig
- #import "NIMBaseSessionContentConfig.h"9 O+ ^. o3 J. H9 T, z. l$ F( ?3 i
- , |$ A! {) D# L# m/ K
- @interface NIMTextContentConfig : NIMBaseSessionContentConfig1 U7 y7 y( R" R7 i4 D5 C$ j
- . A% b8 z9 U+ k\' b8 Y
- @end
复制代码
- @interface NIMTextContentConfig()
-
- @property (nonatomic,strong) NIMAttributedLabel *label;+ w7 R4 o& E( A
-
- @end- d3 W7 J4 M0 j\' K7 @2 o% f
- $ L2 c% q* o5 g3 o, T
- 2 X. b! A! A; ]; Y4 j
- @implementation NIMTextContentConfig3 E) e0 h* N\' Y
-
- - (CGSize)contentSize:(CGFloat)cellWidth
- {2 a/ d4 Z& [9 L$ ] I) C
- NSString *text = self.message.text;+ m" r! n3 u\' T7 w! _
- [self.label nim_setText:text];
- \' J* i" Y+ m9 y0 U\' a; X% {
- CGFloat msgBubbleMaxWidth = (cellWidth - 130);( _/ o; f8 k- H7 f0 o0 t; s* E) T
- CGFloat bubbleLeftToContent = 14;
- CGFloat contentRightToBubble = 14;
- CGFloat msgContentMaxWidth = (msgBubbleMaxWidth - contentRightToBubble - bubbleLeftToContent);
- return [self.label sizeThatFits:CGSizeMake(msgContentMaxWidth, CGFLOAT_MAX)];1 C: G! |& {8 Q
- }* N9 S1 c# a: w, f* E, n+ w
- ; ~9 s. f" N0 X j; q
- - (NSString *)cellContent
- {
- return @"NIMSessionTextContentView";
- }
-
- - (UIEdgeInsets)contentViewInsets z" @+ W8 B& v3 V\' P
- {
- return self.message.isOutgoingMsg ? UIEdgeInsetsMake(11,11,9,15) : UIEdgeInsetsMake(11,15,9,9);
- }
- 9 g0 M& R ]; `! M7 \) O& i, y
-
- - (NIMAttributedLabel *)label5 y O$ B; Z4 r h% n\' f6 H6 W7 D
- {* {$ c" j1 A% v2 h8 Q\' X
- if (_label) {, w+ r& f* V! I4 p
- return _label;
- }* @- r$ w! m\' M\' F: B, d0 K# t
- _label = [[NIMAttributedLabel alloc] initWithFrame:CGRectZero];& B$ D; l& W& l\' D- J
- _label.font = [UIFont systemFontOfSize:NIMKit_Message_Font_Size];5 X- c4 F8 E0 X7 U
- return _label;
- }" _, O) o# U0 T2 v; }\' h
- @end
复制代码
这里就是实现了NIMSessionContentConfig配置协议,我举例一个文本消息类型的sessionContentView的处理方式,其他类型是一样的处理方法,实现相关配置协议方法即可。你可能会想到如果某个sessionContentView上面的元素有很多时该怎么处理,我该不会把某个sessionContentView的元素都定义一次,然后全部赋值再计算contentSize么?我将会在说自定义消息类型的时候谈谈我简单的处理方式。 这里我个人觉得有两点可以改变一下。
- NIMBaseSessionContentConfig的NIMMessage对象应该改为NIMMessageModel对象比较好。因为我需要用到contentSize,根据contentSize来设置控件的宽度适应屏幕。所以我在自定义的消息里面,将NTESCustomAttachmentInfo协议需要传入的NIMMessage对象改为NIMMessageModel对象。
- 在返回contentView类名时,改为NSStringFromClass([NIMSessionTextContentView Class])会好点,怕输入字符串时时产生错误嘛。
在NIMSessionContentConfigFactory类里面定义了基本消息类型所对应的contentConfig配置协议(注意,在云信demo里面,每个sessionContentView都对应一个sessionContentConfig)。请看NIMUnsupportContentConfig判nil处理,如果没有这段判断处理,你在添加自定义消息时候,忘记在 NTESSessionCustomLayoutConfig类的supportAttachmentType方法里面添加你的自定义消息,程序就会崩溃。ps:防止崩溃,请从细节做起,谢谢!
那NIMCellLayoutDefaultConfig计算contentSize就简单明了了,就是调用相关 sessionContentConfig 的方法嘛。
NNTESSessionCustomContentConfig计算contentSize
当我们看到NTESSessionCustomLayoutConfig类时,有两个地方是值得我们注意,也是与NIMCellLayoutDefaultConfig不同的地方。
- 一个NTESSessionCustomContentConfig类的属性
- supportAttachmentType 内部方法,用来获取customLayoutConfig直接的类型。
NTESSessionCustomContentConfig
它有一个NIMMessage类型的public属性,而在介绍NIMBaseSessionContentConfig配置协议时,我有说过建议将它的NIMMessage对象改为NIMMessageModel对象,在这里我也同样建议,原因上面有提过。 请看它的.m文件:
- @interface NTESSessionCustomContentConfig()5 K1 Y. U- D) |+ B$ T
- , _( o) C; K5 Y! s
- @property (nonatomic, strong) id attachmentInfo; o6 V: t* j% K& I8 \* @
-
- @end\' q% I/ K# @8 g7 \8 ~) x
-
- @implementation NTESSessionCustomContentConfig
- ! |+ y9 ^/ y5 x% S* ~+ [
- - (void)setMessage:(NIMMessage *)message
- {3 [. l; E W- L2 e2 y
- NIMCustomObject *object = message.messageObject;
- _message = message;
- _attachmentInfo = (id)object.attachment;
- }! a% B8 y# R" g
-
- - (CGSize)contentSize:(CGFloat)cellWidth: e, v9 G7 O: p
- {
- return [self.attachmentInfo contentSize:self.message cellWidth:cellWidth];6 r* u9 w3 G7 X2 q x
- }
-
- - (NSString *)cellContent" |4 J: @" k* F) V
- {: a, n+ G% s; ~$ x$ I\' j D
- return [self.attachmentInfo cellContent:self.message];\' `$ q2 {8 N# @" p$ ?! D( ]- ]
- }5 s5 O* Y5 P+ {: U1 P. y p
-
- - (UIEdgeInsets)contentViewInsets
- {
- return [self.attachmentInfo contentViewInsets:self.message];
- }
- 1 Y7 U\' A( n6 i% u& W, }
- @end
复制代码
attachmentInfo对象代表不同类型的自定义消息,只要它遵守NTESCustomAttachmentInfo协议即可。(ps:其实NTESCustomAttachmentInfo协议就相当于上面基本消息类型所对应的NIMSessionContentConfig协议;注:这里所谓的基本消息类型,即云信SDK已定义的消息类型,相对于自定义消息类型而言而已。)
下面来看看 NTESCustomAttachmentInfo 协议(已添加注释)。6 V/ Z* M1 ^( i; d: @
- @protocol NTESCustomAttachmentInfo
- 0 _: P+ k- x: _, p/ T- l7 t1 n7 d
- @optional
- /// contentView类名
- - (NSString *)cellContent:(NIMMessage *)message;
- /// contentSize. n/ J1 Z. {) S4 d7 h0 i
- - (CGSize)contentSize:(NIMMessage *)message cellWidth:(CGFloat)width;" @1 j- G2 w+ E2 x: C6 G3 u
- /// 内容距离bubble气泡的相关距离* `3 l! m/ y6 ?) z" r# h7 a9 ?
- - (UIEdgeInsets)contentViewInsets:(NIMMessage *)message;
- /// 格式化消息 某些消息需要在最近回话列表特殊文字 如:收到一段文字,但是需要显示[系统消息]0 I6 q/ Q) y2 P8 B
- - (NSString *)formatedMessage;
- /// 封面图片 如果一个视频 得显示一张图片在界面
- - (UIImage *)showCoverImage;& Z9 E1 u% d\' q0 c
- /// 设置封面图片
- - (void)setShowCoverImage:(UIImage *)image;
复制代码
在这里我提出两点建议
- NIMMessage对象改为NIMMessageModel对象;
- cellContent:、contentSize: cellWidth:、contentViewInsets:这三个方法改为@required类型的;方法名前面加上attachmentInfo与NIMSessionContentConfig协议的相关方法作为区分。
复杂自定义sessionContentView的简单处理方式
上面在介绍NIMBaseSessionContentConfig配置协议时,我有提到如果某个sessionContentView上面的元素有很多时该怎么处理。下面我说说我的处理方式。
- 把计算contentSize和contentViewInsets的方法丢到contentView里面,这样一来,那么只要在attachMentInfo里面调用所属contentView的计算方法。
-
我会在NIMSessionMessageContentView类里面增加两个方法
- - (CGSize)attachmentInfoViewContentSize:(NIMMessageModel *)messageModel cellWidth:(CGFloat)width;
- - (UIEdgeInsets)attachmentInfoViewcontentViewInsets:(NIMMessageModel *)messageModel;
复制代码
-
在具体的contentView里面,我定义方法,它有一个Bool类型的isInit(是否初始化)入参,在这个方法里面我创建和实例变量一样的临时变量,当attachmentInfoViewContentSize: cellWidth方法调用它时,我只是为了方便计算contentSize,如果是initSessionMessageContentView方法调用时,我就将相应的临时变量赋值给例变量。
原文链接:http://joakimliu.github.io/2016/03/27/NIMKit%E6%B5%85%E6%9E%90/
|