环信源码分析---聊天页面相关的事件分发

今日研究了下项目中使用的环信SDK源码中的聊天模块,重点研究了下其中的事件分发的实现。结合最近在看的《Effective Objective-C 2.0》中讲到的事件分发,对iOS事件分发机制有了更深入的了解。

一、环信类的封装

用于展示聊天消息的单元格Cell之间的继承关系如下:

环信源码分析---聊天页面相关的事件分发_第1张图片

而EMChatViewBaseCell拥有一个EMChatBaseBubbleView类型的属性。EMChatBaseBubbleView类如下:

环信源码分析---聊天页面相关的事件分发_第2张图片

因为消息分很多种类型,所以具体消息的展现用EMChatBaseBubbleView子类来实现的,EMChatBaseBubbleView有如下子类,分别实现对应消息的展现以及事件的处理:

环信源码分析---聊天页面相关的事件分发_第3张图片

EMChatViewBaseCell有一个属性MessageModel,EMChatViewBaseCell会根据MessageModel类型来创建与之对应的上面五个EMChatBaseBubbleView的子类中的一个。

二、环信事件分发流程

1、环信给UIResponder类创建了一个分类,并添加了一个新的分发事件的分发:

#import 

@interface UIResponder (Router)

/**
 *  发送一个路由器消息, 对eventName感兴趣的 UIResponsder 可以对消息进行处理
 *
 *  @param eventName 发生的事件名称
 *  @param userInfo  传递消息时, 携带的数据, 数据传递过程中, 会有新的数据添加
 *
 */
- (void)routerEventWithName:(NSString *)eventName userInfo:(NSDictionary *)userInfo;

@end

#import "UIResponder+Router.h"

@implementation UIResponder (Router)

- (void)routerEventWithName:(NSString *)eventName userInfo:(NSDictionary *)userInfo
{
    [[self nextResponder] routerEventWithName:eventName userInfo:userInfo];
}

@end

2、点击事件传递

在EMChatViewCell中有一个点击重发聊天消息的按钮事件:

// 重发按钮事件
-(void)retryButtonPressed:(UIButton *)sender
{
    [self routerEventWithName:kResendButtonTapEventName
                       userInfo:@{kShouldResendCell:self}];
}

其实是走的其父类EMChatViewBaseCell中的实例方法:


//点击头像事件
-(void)headImagePressed:(id)sender
{
    [super routerEventWithName:kRouterEventChatHeadImageTapEventName 
    userInfo:@{KMESSAGEKEY:self.messageModel}];
}

-(void)routerEventWithName:(NSString *)eventName userInfo:(NSDictionary *)userInfo
{
    [super routerEventWithName:eventName userInfo:userInfo];
}

//注意,这里都是用的super 而不是self,
因为当前类中也实现了一个同名routerEventWithName:userInfo:方法。
调用super,走NSResponder+Ronter中添加的方法。

然后在EMChatBaseBubbleView的五个子类中分别具体分发相关的点击事件,比如播放音频的Cell中:

-(void)bubbleViewPressed:(id)sender
{
    [self routerEventWithName:kRouterEventAudioBubbleTapEventName 
    userInfo:@{KMESSAGEKEY:self.model}];
}

3、事件最终捕获与处理

上面说的UITableViewCell都是展示在聊天页面UITableView中的,在聊天页面ChatViewController中实现了捕获各种类型的点击事件并具体实现。

- (void)routerEventWithName:(NSString *)eventName userInfo:(NSDictionary *)userInfo
{
    MessageModel *model = [userInfo objectForKey:KMESSAGEKEY];
    // 点击url链接
    if ([eventName isEqualToString:kRouterEventTextURLTapEventName]) {
        [self chatTextCellUrlPressed:[userInfo objectForKey:@"url"]];
    }
    // 点击音频
    else if ([eventName isEqualToString:kRouterEventAudioBubbleTapEventName]) {
        [self chatAudioCellBubblePressed:model];
    }
    // 点击图片
    else if ([eventName isEqualToString:kRouterEventImageBubbleTapEventName]){
        [self chatImageCellBubblePressed:model];
    }
    // 点击地理位置
    else if ([eventName isEqualToString:kRouterEventLocationBubbleTapEventName]){
        [self chatLocationCellBubblePressed:model];
    }
    // 点击重发
    else if([eventName isEqualToString:kResendButtonTapEventName]){
        EMChatViewCell *resendCell = [userInfo objectForKey:kShouldResendCell];
        MessageModel *messageModel = resendCell.messageModel;
        if ((messageModel.status != eMessageDeliveryState_Failure) && (messageModel.status != eMessageDeliveryState_Pending))
        {
            return;
        }
        id  chatManager = [[EaseMob sharedInstance] chatManager];
        [chatManager asyncResendMessage:messageModel.message progress:nil];
        NSIndexPath *indexPath = [self.tableView indexPathForCell:resendCell];
        [self.tableView beginUpdates];
        [self.tableView reloadRowsAtIndexPaths:@[indexPath]
                              withRowAnimation:UITableViewRowAnimationNone];
        [self.tableView endUpdates];
    }
    // 点击视频
    else if([eventName isEqualToString:kRouterEventChatCellVideoTapEventName]){
        [self chatVideoCellPressed:model];
    }
}

三、iOS事件分发机制

1、hit-Testing

iOS中的事件大概分为三种,分别是 Milti-Touch Events, Motion Events 和Remote Control Events(events for controlling multimedia)。

每当我们点击了一下iOS设备的屏幕,UIKit就会生成一个事件对象UIEvent,然后会把这个Event分发给当前active的app。

告知当前活动的app有事件之后,UIApplication 单例就会从事件队列中去取最新的事件,然后分发给能够处理该事件的对象。UIApplication 获取到Event之后,Application就纠结于到底要把这个事件传递给谁,这时候就要依靠HitTest来决定了。

iOS中,hit-Testing的作用就是找出这个触摸点下面的View是什么,HitTest会检测这个点击的点是不是发生在这个View上,如果是的话,就会去遍历这个View的subviews,直到找到最小的能够处理事件的view,如果整了一圈没找到能够处理的view,则返回自身。如图:

环信源码分析---聊天页面相关的事件分发_第4张图片
hit-Testing.jpg

假设我们现在点击到了图中的E,hit-testing将进行如下步骤的检测(不包含重写hit-test并且返回非默认View的情况)

1、触摸点在ViewA内,所以检查ViewA的Subview B、C

2、触摸点不在ViewB内,触摸点在ViewC内部,所以检查ViewC的Subview D、E

3、触摸点不在ViewD内,触摸点发生在ViewE内部,并且ViewE没有subview,所以ViewE属于ViewA中包含这个点的最小单位,所以ViewE变成了该次触摸事件的hit-TestView

PS.

1、默认的hit-testing顺序是按照UIView中Subviews的逆顺序

2、如果View的同级别Subview中有重叠的部分,则优先检查顶部的Subview,如果顶部的Subview返回nil, 再检查底部的Subview

3、Hit-Test也是比较聪明的,检测过程中有这么一点,就是说如果点击没有发生在某View中,那么该事件就不可能发生在View的Subview中,所以检测过程中发现该事件不在ViewB内,也直接就不会检测在不在ViewF内。也就是说,如果你的Subview设置了clipsToBounds=NO,实际显示区域可能超出了superView的frame,你点击超出的部分,是不会处理你的事件的,就是这么任性!

hit-Test 是事件分发的第一步,就算你的app忽略了事件,也会发生hit-Test。确定了hit-TestView之后,才会开始进行下一步的事件分发。

2、The Responder Chain

响应链简单来说,就是一系列的相互关联的对象,从firstResponder开始,到application对象结束,如果firstResponder 无法响应事件,则交给nextResponder来处理,直到结束为止。iOS中很多类型的事件分发,都依赖于响应链;在响应链中,所有对象的基类都是UIResponder,也就是说所有能响应事件的类都是UIResponder的子类,UIApplication/UIView/UIViewController都是UIResponder的子类,这说明所有的Views,绝大部分Controllers(不用来管理View的Controller除外)都可以响应事件。

PS.CALayer 不是UIResponder的子类,这说明CALayer无法响应事件,这也是UIView和CALayer的重要区别之一。

nextResponder的事件传递过程:


环信源码分析---聊天页面相关的事件分发_第5张图片
nextResponder的事件传递过程.jpg

PS.View处理事件的方式有手势或者重写touchesEvent方法或者利用系统封装好的组件(UIControls)。

图中所表示的正是nextResponder的查找过程,两种方式分别对应两种app的架构,左边的那种app架构比较简单,只有一个VC,右边的稍微复杂一些,但是寻找路线的原则是一样的,先解释一下,UIResponder本身是不会去存储或者设置nextResponder的,所谓的nextResponder都是子类去实现的(这里说的是UIView,UIViewController,UIApplication),关于nextResponder的值总结如下:

1、UIView的nextResponder是直接管理它的UIViewController(也就是VC.view.nextResponder=VC),如果当前View不是ViewController直接管理的View,则nextResponder是它的superView(view.nextResponder = view.superView)

2、UIViewController的nextResponder是它直接管理的View的superView(VC.nextResponder = VC.view.superView)

3、UIWindow的nextResponder是UIApplication

4、UIApplication的nextResponder是UIApplicationDelegate(官方文档说是nil)


需要注意的是:

如果你自己想自定义一个非TouchEvent的事件,当需要继续传递事件的话,切记不要在实现内直接显示的调用nextResponder的对应方法, 而是直接调用父类中对应的事件处理方法并让UIKit来处理响应链的遍历。

参考:
1.Event Delivery: The Responder Chain

2.iOS事件分发机制(一) hit-Testing

3.iOS事件分发机制(二)The Responder Chain

你可能感兴趣的:(环信源码分析---聊天页面相关的事件分发)