iOS开源项目学习——JSQMessagesViewController

JSQMessagesViewController是一个优雅的iOS消息类UI库。JSQMessagesViewController使用UICollectionView来展示消息,其UI布局用Reveal来查看是如下的形式:
这里写图片描述

  • 底部的输入框:JSQMessagesInputToolbar
  • 接收的消息Cell:JSQMessagesCollectionViewCellIncoming
  • 发送的消息Cell:JSQMessagesCollectionViewCellOutgoing

JSQMessagesViewController的布局如下,基本由两部分组成:JSQMessagesCollectionView和底部的JSQMessagesInputToolbar,主要有两个约束,JSQMessagesInputToolbar的高度约束和JSQMessagesInputToolbar的底部距离JSQMessagesViewController的底部的margin约束。

这里写图片描述


键盘事件处理JSQMessagesKeyboardController

JSQMessagesInputToolbar中的JSQMessagesComposerTextView在编辑时,会触发keyboard事件。例如在keyboard弹起时,JSQMessagesCollectionView的frame或者contentInset要改变,JSQMessagesInputToolbar的位置要上移。
JSQMessagesViewController中keyboard事件的处理主要通过JSQMessagesKeyboardController类处理,在keyboard的frame改变的时候它会通过代理和通知,来告知JSQMessagesViewController做出何种的改变。
JSQMessagesKeyboardController支持拖动手势,可以实现keyboard的拖动。效果如下:
这里写图片描述

JSQMessagesKeyboardController这个idea是来自Daniel Amitay的DAKeyboardControl

JSQMessagesKeyboardController的主要逻辑是,通过beginListeningForKeyboard方法来开始监听keyboard。

- (void)beginListeningForKeyboard
{
    if (self.textView.inputAccessoryView == nil) {
        self.textView.inputAccessoryView = [[UIView alloc] init];
    }
    //注册通知
    [self jsq_registerForNotifications];
}

其注册的通知有UIKeyboardDidShowNotificationUIKeyboardWillChangeFrameNotificationUIKeyboardDidChangeFrameNotificationUIKeyboardDidHideNotification
在处理UIKeyboardDidShowNotification 通知时,会获取到keyboardView,并给拖动手势添加上事件:

- (void)jsq_didReceiveKeyboardDidShowNotification:(NSNotification *)notification
{
    //获取到keyboardView
    self.keyboardView = self.textView.inputAccessoryView.superview;
    [self jsq_setKeyboardViewHidden:NO];

    [self jsq_handleKeyboardNotification:notification completion:^(BOOL finished) {
        //拖动手势的事件
        [self.panGestureRecognizer addTarget:self action:@selector(jsq_handlePanGestureRecognizer:)];
    }];
}

重写了self.keyboardView的setter方法,在setter的时候会使用KVO来观察keyboardView的frame的变化:

- (void)setKeyboardView:(UIView *)keyboardView
{
    if (_keyboardView) {
        [self jsq_removeKeyboardFrameObserver];
    }

    _keyboardView = keyboardView;

    if (keyboardView && !_jsq_isObserving) {
        //添加KVO观察者 观察frame
        [_keyboardView addObserver:self
                        forKeyPath:NSStringFromSelector(@selector(frame))
                           options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
                           context:kJSQMessagesKeyboardControllerKeyValueObservingContext];

        _jsq_isObserving = YES;
    }
}

每种类型的通知处理方法都会调用- (void)jsq_handleKeyboardNotification:(NSNotification *)notification completion:(JSQAnimationCompletionBlock)completion方法:

- (void)jsq_handleKeyboardNotification:(NSNotification *)notification completion:(JSQAnimationCompletionBlock)completion
{
    NSDictionary *userInfo = [notification userInfo];
    //键盘frame
    CGRect keyboardEndFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];

    if (CGRectIsNull(keyboardEndFrame)) {
        return;
    }

    //curve动画曲线
    UIViewAnimationCurve animationCurve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
    NSInteger animationCurveOption = (animationCurve << 16);
    //动画时间
    double animationDuration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
    //转换keyboardEndFrame
    CGRect keyboardEndFrameConverted = [self.contextView convertRect:keyboardEndFrame fromView:nil];
    //动画
    [UIView animateWithDuration:animationDuration
                          delay:0.0
                        options:animationCurveOption
                     animations:^{
                         [self jsq_notifyKeyboardFrameNotificationForFrame:keyboardEndFrameConverted];
                     }
                     completion:^(BOOL finished) {
                         if (completion) {
                             completion(finished);
                         }
                     }];
}

- (void)jsq_notifyKeyboardFrameNotificationForFrame:(CGRect)frame
{
    //通知代理
    [self.delegate keyboardController:self keyboardDidChangeFrame:frame];
    //抛出通知
    [[NSNotificationCenter defaultCenter] postNotificationName:JSQMessagesKeyboardControllerNotificationKeyboardDidChangeFrame
                                                        object:self
                                                      userInfo:@{ JSQMessagesKeyboardControllerUserInfoKeyKeyboardDidChangeFrame : [NSValue valueWithCGRect:frame] }];
}

拖动手势的处理逻辑如下图:
这里写图片描述

UITextView自增长

在输入信息的时候,UITextView的高度会随着输入内容的大小而自动调节。
这里写图片描述

JSQMessagesViewController的方式是通过KVO的方式来观察JSQMessagesInputToolbartextViewcontentSize的变化

- (void)jsq_addObservers
{
    if (self.jsq_isObserving) {
        return;
    }

    [self.inputToolbar.contentView.textView addObserver:self
                                             forKeyPath:NSStringFromSelector(@selector(contentSize))
                                                options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                                                context:kJSQMessagesKeyValueObservingContext];

    self.jsq_isObserving = YES;
}

然后处理在- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context方法中处理:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (context == kJSQMessagesKeyValueObservingContext) {

        if (object == self.inputToolbar.contentView.textView
            && [keyPath isEqualToString:NSStringFromSelector(@selector(contentSize))]) {

            CGSize oldContentSize = [[change objectForKey:NSKeyValueChangeOldKey] CGSizeValue];
            CGSize newContentSize = [[change objectForKey:NSKeyValueChangeNewKey] CGSizeValue];

            CGFloat dy = newContentSize.height - oldContentSize.height;

            [self jsq_adjustInputToolbarForComposerTextViewContentSizeChange:dy];
            [self jsq_updateCollectionViewInsets];
            if (self.automaticallyScrollsToMostRecentMessage) {
                [self scrollToBottomAnimated:NO];
            }
        }
    }
}

创建气泡和头像图片

如下图所示,气泡图片有两种类型,发送的和接收的,每种气泡图片对应两种状态,一个正常的,一个高亮。
头像图片也有两种状态,正常的和高亮的。
这里写图片描述

聊天气泡图片

聊天气泡图片是在一张名为”bubble_min.png”这里写图片描述图片的基础上绘制出来的。从它的形状可以看出它是一张“outgoing”的图片。
JSQMessagesViewController中,表示气泡的类是JSQMessagesBubbleImage类,它实现了JSQMessageBubbleImageDataSource协议。JSQMessageBubbleImageDataSource协议,有两个方法,一个是提供正常的bubble图片,一个是提供高亮bubble图片。通过JSQMessagesBubbleImageFactory来创建JSQMessagesBubbleImage
这里写图片描述

创建图片的主要方式是,做遮罩,然后填充颜色:

- (UIImage *)jsq_imageMaskedWithColor:(UIColor *)maskColor
{
    NSParameterAssert(maskColor != nil);

    CGRect imageRect = CGRectMake(0.0f, 0.0f, self.size.width, self.size.height);
    UIImage *newImage = nil;

    UIGraphicsBeginImageContextWithOptions(imageRect.size, NO, self.scale);
    {
        CGContextRef context = UIGraphicsGetCurrentContext();

        CGContextScaleCTM(context, 1.0f, -1.0f);
        CGContextTranslateCTM(context, 0.0f, -(imageRect.size.height));
        //mask
        CGContextClipToMask(context, imageRect, self.CGImage);
        CGContextSetFillColorWithColor(context, maskColor.CGColor);
        CGContextFillRect(context, imageRect);

        newImage = UIGraphicsGetImageFromCurrentImageContext();
    }
    UIGraphicsEndImageContext();

    return newImage;
}

水平镜像图片的方法为:

- (UIImage *)jsq_horizontallyFlippedImageFromImage:(UIImage *)image
{
    return [UIImage imageWithCGImage:image.CGImage
                               scale:image.scale
                         orientation:UIImageOrientationUpMirrored];
}

拉伸图片的方法为:

- (UIImage *)jsq_stretchableImageFromImage:(UIImage *)image withCapInsets:(UIEdgeInsets)capInsets
{
    return [image resizableImageWithCapInsets:capInsets resizingMode:UIImageResizingModeStretch];
}
聊天头像图片

avatar图片与bubble图片类似,如下:
这里写图片描述

创建圆形的图片方法,如下:

/**
 *  创建带有文字的头像
 *
 *  @param initials        文字
 *  @param backgroundColor 背景颜色
 *  @param textColor       文字颜色
 *  @param font            字体大小
 *  @param diameter        直径
 *
 *  @return 创建后的图片
 */
+ (UIImage *)jsq_imageWitInitials:(NSString *)initials
backgroundColor:(UIColor *)backgroundColor
                        textColor:(UIColor *)textColor
                             font:(UIFont *)font
                         diameter:(NSUInteger)diameter
{
    NSParameterAssert(initials != nil);
    NSParameterAssert(backgroundColor != nil);
    NSParameterAssert(textColor != nil);
    NSParameterAssert(font != nil);
    NSParameterAssert(diameter > 0);

    CGRect frame = CGRectMake(0.0f, 0.0f, diameter, diameter);

    NSDictionary *attributes = @{ NSFontAttributeName : font,
                                  NSForegroundColorAttributeName : textColor };

    CGRect textFrame = [initials boundingRectWithSize:frame.size
                                              options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading)
                                           attributes:attributes
                                              context:nil];

    CGPoint frameMidPoint = CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame));
    CGPoint textFrameMidPoint = CGPointMake(CGRectGetMidX(textFrame), CGRectGetMidY(textFrame));

    CGFloat dx = frameMidPoint.x - textFrameMidPoint.x;
    CGFloat dy = frameMidPoint.y - textFrameMidPoint.y;
    CGPoint drawPoint = CGPointMake(dx, dy);
    UIImage *image = nil;

    UIGraphicsBeginImageContextWithOptions(frame.size, NO, [UIScreen mainScreen].scale);
    {
        CGContextRef context = UIGraphicsGetCurrentContext();

        CGContextSetFillColorWithColor(context, backgroundColor.CGColor);
        CGContextFillRect(context, frame);
        [initials drawAtPoint:drawPoint withAttributes:attributes];

        image = UIGraphicsGetImageFromCurrentImageContext();

    }
    UIGraphicsEndImageContext();

    return [JSQMessagesAvatarImageFactory jsq_circularImage:image withDiameter:diameter highlightedColor:nil];
}

/**
 *  创建圆形的头像
 *
 *  @param image            图片
 *  @param diameter         直径
 *  @param highlightedColor 高亮颜色
 *
 *  @return 图片
 */
+ (UIImage *)jsq_circularImage:(UIImage *)image withDiameter:(NSUInteger)diameter highlightedColor:(UIColor *)highlightedColor
{
    NSParameterAssert(image != nil);
    NSParameterAssert(diameter > 0);

    CGRect frame = CGRectMake(0.0f, 0.0f, diameter, diameter);
    UIImage *newImage = nil;

    UIGraphicsBeginImageContextWithOptions(frame.size, NO, [UIScreen mainScreen].scale);
    {
        CGContextRef context = UIGraphicsGetCurrentContext();

        UIBezierPath *imgPath = [UIBezierPath bezierPathWithOvalInRect:frame];
        //clip
        [imgPath addClip];
        //绘制
        [image drawInRect:frame];

        if (highlightedColor != nil) {
            CGContextSetFillColorWithColor(context, highlightedColor.CGColor);
            CGContextFillEllipseInRect(context, frame);
        }

        newImage = UIGraphicsGetImageFromCurrentImageContext();

    }
    UIGraphicsEndImageContext();

    return newImage;
}

JSQMessagesCollectionView

JSQMessagesCollectionView继承自UICollectionView。类之间的关系如下图所示:

这里写图片描述

JSQMessagesCollectionViewCell

JSQMessagesCollectionViewCell是一个抽象类,它有两个实体类:JSQMessagesCollectionViewCellIncoming和JSQMessagesCollectionViewCellOutgoing。collection view的基本布局如下:

这里写图片描述这里写图片描述

  • cell top label: 用来显示时间戳
  • bubble top label: 用来显示消息的发送者
  • cell bottom label: 用来显示消息的发送状态
  • JSQMessagesCellTextView:显现消息的文本
  • messageBubbleImageView:显示气泡
  • avatarImageView:显示头像

聊天气泡大小的计算是由JSQMessagesCollectionViewFlowLayout计算的。计算bubble的大小是- (CGSize)messageBubbleSizeForItemAtIndexPath:(NSIndexPath *)indexPath方法。
计算item的大小是- (CGSize)sizeForItemAtIndexPath:(NSIndexPath *)indexPath方法。
基本就是通过字符串来计算size:

    CGRect stringRect = [[messageItem text] boundingRectWithSize:CGSizeMake(maximumTextWidth, CGFLOAT_MAX)
                                                         options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading)
                                                      attributes:@{ NSFontAttributeName : self.messageBubbleFont }
                                                         context:nil];
相关推荐
©️2020 CSDN 皮肤主题: 程序猿惹谁了 设计师:白松林 返回首页