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

分类:

技术点:

相关文章: