编程

当前位置:永利皇宫463登录 > 编程 > iOS事件分发机制介绍与应用,iOS事件分发机制与

iOS事件分发机制介绍与应用,iOS事件分发机制与

来源:http://www.makebuLuo.com 作者:永利皇宫463登录 时间:2019-09-16 23:54

iOS事件的传递与响应是一个重要的话题,网上谈论的很多,但大多讲述并不完整,本文将结合苹果官方的文档对事件的传递与响应原理及应用实践做一个比较完整的总结。文章将依次介绍下列内容:

本文将简单介绍 iOS 的点击事件( TouchEvents )分发机制和一些使用场景。详解请看参考部分。

打开某App点击登录按钮后弹出登录页面。这是一个事件分发与响应的示例。我们来探究下该过程。

  • 事件的传递机制
  • 事件的响应机制
  • 事件传递与响应实践
  • 手势识别器工作机制

从以下两个方面介绍:

介绍事件分发机制自然绕不开事件。

iOS中事件一共有四种类型,包含触摸事件,运动事件,远程控制事件,按压事件,本文将只讨论最常用的触摸事件。事件通过UIEvent对象描述

1. 寻找 hit-TestView 的过程2. 响应链

iOS包含以下几种类型事件:

UIEvent

UIEvent描述了单次的用户与应用的交互行为,例如触摸屏幕会产生触摸事件,晃动手机会产生运动事件。UIEvent对象中记录了事件发生的时间,类型,对于触摸事件,还记录了一组UITouch对象,下面是UIEvent的几个属性:

@property(nonatomic,readonly) UIEventType type NS_AVAILABLE_IOS; //事件的类型@property(nonatomic,readonly) UIEventSubtype subtype NS_AVAILABLE_IOS;@property(nonatomic,readonly) NSTimeInterval timestamp; //事件的时间@property(nonatomic, readonly, nullable) NSSet <UITouch *> *allTouches; //事件包含的touch对象

那么触摸事件中的UITouch对象描述的是什么呢?

一些应用场景:

  • 触摸事件
  • 按压时间
  • 摇动事件
  • 远程控制事件
  • 编辑菜单时间
UITouch

UITouch记录了手指在屏幕上触摸时产生的一组信息,包含触摸的时间,位置,所在的窗口或视图,触摸的状态,力度等信息

@property(nonatomic,readonly) NSTimeInterval timestamp; //时间@property(nonatomic,readonly) UITouchPhase phase; //状态,例如begin,move,end,cancel@property(nonatomic,readonly) NSUInteger tapCount; // 短时间内单击的次数@property(nonatomic,readonly) UITouchType type NS_AVAILABLE_IOS; //类型@property(nonatomic,readonly) CGFloat majorRadius NS_AVAILABLE_IOS; //触摸半径@property(nonatomic,readonly) CGFloat majorRadiusTolerance NS_AVAILABLE_IOS;@property(nullable,nonatomic,readonly,strong) UIWindow *window; //触摸所在窗口@property(nullable,nonatomic,readonly,strong) UIView *view; //触摸所在视图@property(nullable,nonatomic,readonly,copy) NSArray <UIGestureRecognizer *> *gestureRecognizers NS_AVAILABLE_IOS; //正在接收该触摸对象的手势识别器@property(nonatomic,readonly) CGFloat force NS_AVAILABLE_IOS; //触摸的力度

每一根手指的触摸都会产生一个UITouch对象,多个手指触摸便会有多个UITouch对象,当手指在屏幕上移动时,系统会更新UITouch的部分属性值,在触摸结束后系统会释放UITouch对象。

当事件产生后,系统会寻找可以响应该事件的对象来处理事件,如果找不到可以响应的对象,事件就会被丢弃。那么哪些对象可以响应事件呢?只有继承于UIResponder的对象才能够响应事件,UIApplication,UIView,UIViewcontroller均继承于UIResponder,因此它们能够响应事件。UIResponder提供了响应事件的一组方法:

- touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; //手指触摸到屏幕- touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; //手指在屏幕上移动或按压- touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; //手指离开屏幕- touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; //触摸被中断,例如触摸时电话呼入

如果我们想要对事件进行自定义的处理(比如手指在屏幕滑动时让某个view跟着移动),我们需要重写以上四个方法,对于UIViewcontroller,我们只需要在UIViewcontroller中重写上面四个方法,对于UIView,我们需要创建继承于UIView的子类,然后在子类中重写上面的方法,这点需要注意

  1. 一个内容是圆形的按钮(指定只允许视图的 frame 内某个区域可以响应事件)
  2. tabBar 上中间凸起的按钮(让超出父视图边界的子视图区域也能响应事件)

本文选择以触摸事件进行介绍。因为相对而言它包含的事件分发机制最全面。

事件的传递

事件产生之后,会被加入到由UIApplication管理的事件队列里,接下来开始自UIApplication往下传递,首先会传递给主window,然后按照view的层级结构一层层往下传递,一直找到最合适的view(发生touch的那个view)来处理事件。查找最合适的view的过程是一个递归的过程,其中涉及到两个重要的方法 hitTest:withEvent:pointInside:withEvent:

当事件传递给某个view之后,会调用view的hitTest:withEvent:方法,该方法会递归查找view的所有子view,其中是否有最合适的view来处理事件,整个流程如下所示:

图片 1hitTest工作流程

hitTest:withEvent:代码实现:

- hitTest:point withEvent:(UIEvent *)event{ //首先判断是否可以接收事件 if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil; //然后判断点是否在当前视图上 if ([self pointInside:point withEvent:event] == NO) return nil; //循环遍历所有子视图,查找是否有最合适的视图 for (NSInteger i = self.subviews.count - 1; i >= 0; i--) { UIView *childView = self.subviews[i]; //转换点到子视图坐标系上 CGPoint childPoint = [self convertPoint:point toView:childView]; //递归查找是否存在最合适的view UIView *fitView = [childView hitTest:childPoint withEvent:event]; //如果返回非空,说明子视图中找到了最合适的view,那么返回它 if  { return fitView; } } //循环结束,仍旧没有合适的子视图可以处理事件,那么就认为自己是最合适的view return self;}
  • pointInside:withEvent:方法作用是判断点是否在视图内,是则返回YES,否则返回NO
  • 判断一个view是否能够接收事件有三个条件,分别是,是否禁止用户交互(userInteractionEnabled = NO),是否被隐藏(hidden = YES)以及透明度是否小于等于0.01(alpha <=0.01)
  • 从递归的逻辑我们知道,如果触摸的点不在父view上,那么其上的所有子view的hitTest都不会被调用,需要指出的是,如果子view尺寸超出了父view,并且属性clipsToBounds设置为NO,触摸发生在子view超出父view的区域内,依旧不返回子view。反过来,如果触摸的点在父view上并且父view就是最合适的view,那么它的所有子view的hitTest还是会被调用,因为如果不调用无法知道是否还有比父view更合适的子view存在。

寻找 hit-TestView 的过程的总结

在 iOS 中,当产生一个 touch 事件之后,通过 hit-Testing 找到触摸点所在的 View( hit-TestView )。寻找过程总结如下:

寻找顺序如下:

1. 从视图层级最底层的 window 开始遍历它的子 View。2. 默认的遍历顺序是按照 UIView 中 Subviews 的逆顺序。3. 找到 hit-TestView 之后,寻找过程就结束了。

确定一个 View 是不是 hit-TestView 的过程如下:

1. 如果 View 的 userInteractionEnabled = NO,enabled = NO( UIControl ),或者 alpha <= 0.01, hidden = YES 等情况的时候,直接返回 nil。2. 如果触摸点不在 view 中,直接返回 nil。3. 如果触摸点在 view 中,逆序遍历它的子 View ,重复上面的过程。4. 如果 view 的 子view 都返回 nil(都不是 hit-TestVeiw ),那么返回自身(自身是 hit-TestView )。

UIView 提供两个方法来来确定 hit-TestView:

// 返回一个 hit-TestView- hitTest:point withEvent:(UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system// 判断触摸点是否在 view 中- pointInside:point withEvent:(UIEvent *)event; // default returns YES if point is in bounds 

hitTest:withEvent: 方法的具体实现可以写成这样:

- hitTest:point withEvent:(UIEvent *)event { //1 if (self.alpha <= 0.01 || !self.userInteractionEnabled || self.hidden) { return nil; } //2 if (![self pointInside:point withEvent:event]) { return nil; } //3 NSEnumerator *enumerator = [self.subviews reverseObjectEnumerator]; for (UIView *subview in enumerator) { UIView *hitTestView = [subview hitTest:point withEvent:event]; if (hitTestView) { return hitTestView; } } //4 return self;}

看完了理论,再结合实际,这样就好理解了。

以下讲解基于这样的视图层级结构

图片 2视图层级结构.png

+-UIWindow +-MainView +-RedView | +-UIButton | +-UIButtonLabel +-YellowView +-UILabel

下面是测试过程中的一些日志(请结合上面的总结来分析):ps:在实际项目中点击一次视图会打印两次下面的信息中间插入一次 UIStatusBarWindow 的信息,目前也不知道什么原因,如果有知道的请分享出来,非常感谢!

点击红色 View 时:

UIWindow:[hitTest:withEvent:]----MainView:[hitTest:withEvent:]--------YellowView:[hitTest:withEvent:]--------RedView:[hitTest:withEvent:]------------UIButton:[hitTest:withEvent:]hit-TestView is RedView !

分析:

1. 先使用了 YellowView 的 [hitTest:withEvent:] 方法可以看出:默认的遍历顺序是按照 UIView 中 Subviews 的逆顺序。2. 当判断 YellowView 是不是 hit-TestView 的时候,判断触摸点不在 YellowView 上就不会再遍历它的子 View 了。3. 触摸点在 RedView 上,所以会继续遍历它的子 View( UIButton ),触摸点不在 UIButton 上,所以返回 nil ( UIButton 不是 hit-TestView ),所以返回它本身( 是 hit-TestView )。

点击灰色 button 时:

UIWindow:[hitTest:withEvent:]----MainView:[hitTest:withEvent:]--------YellowView:[hitTest:withEvent:]--------RedView:[hitTest:withEvent:]------------UIButton:[hitTest:withEvent:]----------------UIButtonLabel:[hitTest:withEvent:]hit-TestView is UIButton !

根据上面的分析,触摸点在 RedView 上,所以会继续遍历它的子 View( UIButton ),触摸点在 UIButton 上,所以返回它本身( 是 hit-TestView )。

点击黄色 View 时:

UIWindow:[hitTest:withEvent:]----MainView:[hitTest:withEvent:]--------YellowView:[hitTest:withEvent:]------------UILabel:[hitTest:withEvent:]hit-TestView is YellowView !

分析:

触摸点在 YellowView 上,遍历它的子 View( UILabel ),触摸点不在 UILabel 上,所以返回 nil,所以 YellowView 是 hit-TestView。找到 hit-TestView 后,就不再检查 RedView 了。

点击 label 时:

UIWindow:[hitTest:withEvent:]UIWindow pointInside:1----MainView:[hitTest:withEvent:]MainView pointInside:1--------YellowView:[hitTest:withEvent:]YellowView pointInside:1------------UILabel:[hitTest:withEvent:]hit-TestView is YellowView !

分析:

触摸点在 YellowView 上,所以遍历它的子 View( UILabel ),但是 UILabel 的 userInteractionEnabled = NO,所以返回 nil,这个时候其实还没有判断触摸点是不是在 UILabel上。

iOS应用启动后UIApplicationMain函数会创建一个UIApplication的单例。该单例会维护一个FIFO的队列进行事件分发。系统检测到触摸事件后就会发给当前Application单例,由该单例进行派发。派发分为下面三个过程:

事件的响应

在找到最合适的view之后,会调用view的touches方法对事件进行响应,如果没有重写view的touches方法,touches默认的做法是将事件沿着响应者链往上抛,交给下一个响应者对象。也就是说,touches方法默认不处理事件,只是将事件沿着响应者链往上传递。那么响应者链是什么呢?

响应链

找到 hit-TestView 之后,事件就交给它来处理,hit-TestView 就是 firstResponder,如果它无法响应事件,则把事件交给它的 nextResponder,直到有处理事件的响应者或者结束(传递到 AppDelegate 为止)。这一系列的响应者和事件的传递方向就是响应链。在响应链中,所有响应者的基类都是 UIResponder,也就是说所有可以响应事件的类都是 UIResponder 的子类,UIApplication/UIView/UIViewController 都是 UIResponder 的子类。

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

只要知道 nextResponder 是什么,就可以确定响应链了。

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 是 AppDelegate。

下面是测试过程中的一些日志:

点击红色 View 时:

------------------The Responder Chain------------------RedView|MainView|ViewController|UIWindow|UIApplication|AppDelegate------------------The Responder Chain------------------

分析:

1. RedView 不是 UIViewController 管理的 View,所以它的 nextResponder 是它的 superView( MainView )。2. MainView 是 UIViewController 管理的 View,所以它的 nextResponder 是管理它的 ViewController。3. ViewController 的 nextResponder 是它管理的 MainView 的superView( UIWindow )。4. UIWindow 的 nextResponder 是 UIApplication。5. UIApplication 的 nextResponder 是 AppDelegate。

一般来说,某个 UIResponder 的子类想要自己处理一些事件,就需要重写它的这些方法:

- touchesBegan:touches withEvent:(UIEvent *)event;- touchesMoved:touches withEvent:(UIEvent *)event;- touchesEnded:touches withEvent:(UIEvent *)event;- touchesCancelled:touches withEvent:(UIEvent *)event;

响应链上的某个对象处理事件之后可以选择让事件传递继续下去或者终止,如果需要让事件继续传递下去则需要在 touchesBegan 方法里面,调用父类对应的方法:

- touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ // Responding to Touch Events [super touchesBegan:touches withEvent:event];}

一、hitTest

UIWindow一旦接收到事件后就进行hit-test以查找哪个对象接收该事件。hitTest:withEvent方法用于发现在触摸位置的视图。pointInside:withEvent:用于检测该点击是否在视图的边界内。hitTest:withEvent调用pointInside:withEvent:

hitTest以递归的形式调用直至找到能处理触摸事件的最顶部叶子视图(一般就是手指点中区域所属的视图),那么该视图就会被选中。称为第一响应者。

做个类比就是:老板说我这有个事谁干给干了?经过经理等层层指派,大家都觉得小王合适。

该过程可以通过复写下面方法断点了解。

- pointInside:point withEvent:(UIEvent *)event

具体情况如图所示:

图片 3hit_test.png

响应者链

在应用程序中,视图放置都是有一定层次关系的,点击屏幕之后该由下方的哪个view来响应需要有一个判断的方式。响应者链是由一系列可以响应事件的对象(继承于UIResponder)组成的,它决定了响应者对象响应事件的先后顺序关系。下图展示了UIApplication,UIViewcontroller以及UIView之间的响应关系链:

图片 4响应者链

响应者链在递归查找最合适的view的时候形成,所找到的view将成为第一响应者,会调用它的touches方法来响应事件,touches方法默认的处理是将事件往上抛给下一个响应者,而如果下一个响应者的touches方法没有重写,事件会继续沿着响应者链往上走,一直到UIApplication,如果依旧不能处理事件那么事件就被丢弃。

  • UIView如果view是viewcontroller的根view,那么下一个响应者是viewcontroller,否则是super view
  • UIViewcontroller如果viewcontroller的view是window的根view,那么下一个响应者是window;如果viewcontroller是另一个viewcontroller模态推出的,那么下一个响应者是另一个viewcontroller;如果viewcontroller的view被add到另一个viewcontroller的根view上,那么下一个响应者是另一个viewcontroller的根view
  • UIWindowUIWindow的下一个响应者是UIApplication
  • UIApplication通常UIApplication是响应者链的顶端(如果app delegate也继承了UIResponder,事件还会继续传给app delegate)

下面分享一个实际开发中的应用场景

场景:自定义一个这样的 tabBar,中间有个凸起一丢丢的 item。

图片 5自定义 tabBar.png

UI的实现:自定义一个大小和 tabBar 一样的 View 覆盖在 tabBar 上,然后然后中间的 item 超出自定义 View 的边界,让自定义的 View 的 clipsToBounds 为 NO,把超出边界的部分也显示出来。

分析:

根据寻找 hit-TestView 过程的原理可以知道,如果点击超出边界的部分是不能响应事件的。

解决过程:

1. 打印view的层级

+-UIWindow +-UILayoutContainerView +-UITransitionView | +-UIViewControllerWrapperView | +-UILayoutContainerView | +-UINavigationTransitionView | | +-UIViewControllerWrapperView | | +-UIView | +-UINavigationBar | +-_UINavigationBarBackground | | +-_UIBackdropView | | | +-_UIBackdropEffectView | | | +-UIView | | +-UIImageView | +-UINavigationItemView | | +-UILabel | +-_UINavigationBarBackIndicatorView +-MSCustomTabBar +-_UITabBarBackgroundView | +-_UIBackdropView | +-_UIBackdropEffectView | +-UIView +-UITabBarButton +-UITabBarButton +-UITabBarButton +-UITabBarButton +-UIImageView +-MSTabBarView +-UIButton | +-UIImageView +-MSVerticalCenterButton | +-UIImageView | +-UIButtonLabel +-MSVerticalCenterButton | +-UIImageView | +-UIButtonLabel +-MSVerticalCenterButton | +-UIImageView | +-UIButtonLabel +-MSVerticalCenterButton +-UIImageView +-UIButtonLabel

分析:(有点长,不过只要看 MSCustomTabBar 那部分就可以了)

  • MSTabBarView 就是自定义覆盖在 MSCustomTabBar 上面的 View,它的子 ViewUIButton 就是中间凸起一丢丢的 item。
  • 如果我们点击了 tabBar 的内部,寻找 hit-TestView 的时候是会查询自定义的 MSTabBarView 的,从而它的子 View 也会被查询,所以只要触摸点在 view 的范围内就可以响应事件了,所以没有任何问题。
  • 如果我们点击了凸起的那一丢丢部分,寻找 hit-TestView 的时候,查询到 MSCustomTabBar 之后,由于触摸点不在它的内部,所以不会查询它的子 View( MSTabBarView ),所以凸起的那一丢丢是响应不了事件的。所以我们需要重写 MSCustomTabBar 的 [hitTest:withEvent:] 方法。

分析 view 的层级主要是为了确定在哪里重写 [hitTest:withEvent:] 方法。

2. 重写 [hitTest:withEvent:] 方法,让超出 tabBar 的那部分也能响应事件

- hitTest:point withEvent:(UIEvent *)event{ // 先使用默认的方法来寻找 hit-TestView UIView *result = [super hitTest:point withEvent:event]; // 如果 result 不为 nil,说明触摸事件发生在 tabbar 里面,直接返回就可以了 if  { return result; } // 到这里说明触摸事件不发生在 tabBar 里面 // 这里遍历那些超出的部分就可以了,不过这么写比较通用。 for (UIView *subview in self.tabBarView.subviews) { // 把这个坐标从tabbar的坐标系转为subview的坐标系 CGPoint subPoint = [subview convertPoint:point fromView:self]; result = [subview hitTest:subPoint withEvent:event]; // 如果事件发生在subView里就返回 if  { return result; } } return nil;}

分析:

如果触摸点在 tabBar 里面的时候,使用默认方法就可以找到 hit-TestView 了,所以先使用 [super hitTest:point withEvent:event] (因为我们是重写方法,所以使用 super 就是使用原始的方法)来寻找,如果找不到,说明触摸点不在 tabBar 里面,这个时候就需要我们手动的判断触摸点在不在超出的那一丢丢里面了。(其实只要判断凸起的 View 就可以了,不过遍历所有 子View 比较通用,如果有多个凸起的 view 也可以这么写),先把坐标转换为 子View 的坐标(这样才能使用默认的 [pointInside:withEvent:] 方法来判断触摸点是否在 view 里面),然后遍历 子View 调用默认的 [hitTest:withEvent:] 方法,如果触摸点在 view 的内部,就能找到 hit-TestView,如果遍历完所有 子View 都没有找到 hit-TestView 说明触摸点也不在凸起的那一丢丢里面,然后返回 nil 就可以了。

二、sendEvent

一旦确定第一响应者后,UIApplication单例就会发送相关触摸事件到第一响应者。

就好比老本拍板说那就让小王干了。

该过程可以断点button的处理方法进行了解。如图所示:

图片 6sent_enent

事件传递与响应实践

首先我们通过代码创建一个具有层次结构的视图集合,在viewcontroller的viewDidLoad中添加如下代码:

 greenView *green = [[greenView alloc] initWithFrame:CGRectMake(50, 50, 300, 500)]; [self.view addSubview:green]; redView *red = [[redView alloc] initWithFrame:CGRectMake(0, 0, 200, 300)]; [green addSubview:red]; orangeView *orange = [[orangeView alloc] initWithFrame:CGRectMake(0, 350, 200, 100)]; [green addSubview:orange]; blueView *blue = [[blueView alloc] initWithFrame:CGRectMake(10, 10, 100, 100)]; [red addSubview:blue];

执行后如下所示:

图片 7视图

要实现我们自定义的事件处理逻辑,通常有两种方式,我们可以重写hitTest:withEvent:方法指定最合适处理事件的视图,即响应链的第一响应者,也可以通过重写touches方法来决定该由响应链上的谁来响应事件。

  • 情景1:点击黄色视图,红色视图响应黄色视图和红色视图均为绿色视图的子视图,我们可以重写绿色视图的hitTest:withEvent:方法,在其中直接返回红色视图,代码示例如下:
- hitTest:point withEvent:(UIEvent *)event{ if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil; if ([self pointInside:point withEvent:event] == NO) return nil; //红色视图是先被add的,所以是第一个元素 return self.subviews[0];}

我们这里是重写了父视图的hitTest方法,而不是重写红色视图的hitTest方法并让它返回自身,道理也很显然,在遍历绿色视图所有子视图的过程中,可能还没来得及调用到红色视图的hitTest方法时,就已经遍历到了触摸点真正所在的绿色视图,这个时候重写红色视图的hitTest方法是无效的。

  • 情景2:点击红色视图,绿色视图响应我们可以重写红色视图的hitTest方法,让其返回空,这时候便没有了合适的子视图来响应事件,父视图即绿色视图就成为了最合适的响应事件的视图,代码示例如下:
- hitTest:point withEvent:(UIEvent *)event{ return nil;}

当然,我们也可以重写绿色视图的hitTest方法,让其直接返回自身,也能实现同样效果,不过这样的话点击其它子视图就也不能响应事件了,因此如何处理需要视情况而定。

  • 情景3:点击红色视图,红色和绿色视图均做响应我们知道,事件在不能被处理时,会沿着响应者链传递给下一个响应者,因此我们可以重写响应者对象的touches方法来实现让一个事件多个响应者对象响应的目的。因此我们可以通过重写红色视图的touches方法,先做自己的处理,然后在把事件传递给下一个响应者,代码示例如下:
- touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ NSLog(@"red touches begin"); //自己的处理 [super touchesBegan:touches withEvent:event];}- touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ NSLog(@"red touches moved"); //自己的处理 [super touchesBegan:touches withEvent:event];}- touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ NSLog(@"red touches end"); //自己的处理 [super touchesBegan:touches withEvent:event];}- touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ NSLog(@"red touches canceled"); //自己的处理 [super touchesBegan:touches withEvent:event];}

需要说明的是,事件传递给下一个响应者时,用的是super而不是superview,这并没有问题,因为super调用了父类的实现,而父类默认的实现就是调用下一个响应者的touches方法。如果直接调用superview反而会有问题,因为下一个响应者可能是viewcontroller

分享一个demo

非矩形区域的点击:比如一个圆角为宽度一半的Button,只有点击圆形区域才会响应事件。

图片 8圆形的 button.png

分析:

因为触摸点在 View 内,想要限制 view 内的点击区域,所以重写 button 的 [pointInside:withEvent:] 这个方法。如下:

- pointInside:point withEvent:(UIEvent *)event{ // 圆形区域的半径 CGFloat maxRadius = CGRectGetWidth(self.frame)/2; // 触摸点相对圆心的坐标 CGFloat xOffset = point.x - maxRadius; CGFloat yOffset = point.y - maxRadius; // 触摸点的半径 CGFloat radius = sqrt(xOffset * xOffset + yOffset * yOffset); return radius <= maxRadius;}

demo 比较简单,稍微动手一下就可以掌握了。

  • iOS事件分发机制 hit-Testing
  • iOS事件分发机制The Responder Chain

三、事件处理

针对触摸事件的处理是重载如下方法:

- touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;- touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;- touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;- touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

第一响应者接到事件后有以下三种选择:

  1. 什么都不不处理(选择不重载上述方法)
  2. 处理一部分,剩下的交由其它对象处理(重载处理后调用super)
  3. 独自进行处理(重载处理并且不调用super)

如果第一响应者什么都不处理或者处理部分然后调用super,则事件将被发送到一个链式响应路径中,即响应链。该事件遵循以下路径进行转发:

  1. 第一响应者
  2. 第一响应者的父视图
  3. 父视图的父视图,直至关联的视图控制器
  4. 视图控制器的父视图控制器,直至根视图
  5. 根视图下一响应者是window
  6. window的下一响应者是application
  7. 最后的响应者是App delegate

若转发至App delegate,且App delegate未进行处理则该事件将被丢弃。

那么事件处理的方式有哪些?我们大致可以划分为下面三类。

  1. UIControl的子类
  2. TouchEvent
  3. 触摸相关手势。

有可能一个对象同时实现了两个或者更多处理方式。那么谁的优先级高些呢?

这个我们可以做个类比,让你实现判断鼠标单机还是双击,那么检测单击的时间会比双击要长。因为双击失效后才会进行单击判定。同理手势识别不了了,其它才有机会处理,UIControl子类也类似,那么优先级如下所示:

触摸相关手势 > UIControl的子类 > TouchEvent

一般情况下,我们遇到的是手势触发了就不会继续转发事件了。这就是为什么有人在UIButton中添加个tap手势看起来正常。

手势识别器

事实上,我们要处理事件除了使用前面提到的方式,还有另一种方式,就是手势识别器。手势识别器可以很方便的处理常用的各种触摸事件,常见的手势包括单击、拖动,长按,横扫或竖扫,缩放,旋转等,另外我们还可以创建自定义的手势。

UIGestureRecognize是手势识别器的父类,所有具体的手机识别器均继承于该父类,如果我们自定义手势,也需要继承该类。该类并没有继承于UIResponder,所以手势识别器并不参与响应者链。那么手势识别器是如何工作的呢?

事件分发不同场景方法选择

  1. 更改事件分发流程,实现相关类的hitTest:withEvent:方法为佳。

  2. 扩大点击范围,覆写pointInside:withEvent:是个不错的选择。

  3. 解决一写如收起键盘等问题,可尝试直接发送事件。如:

    [[UIApplication sharedApplication] sendAction:@selector(resignFirstResponder) to:nil from:nil forEvent:nil];
    
  4. 选择让父视图或者关联视图控制器处理事件可以这么写(虽然很少见有这么玩的):

    [button addTarget:nil action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside];
    
手势识别器工作机制

当触摸屏幕产生touch事件后,UIApplication会将事件往下分发,如果视图绑定了手势识别器,那么touch事件会优先传递给绑定在视图上的手势识别器,然后手势识别器会对手势进行识别,如果识别出了手势,就会调用创建手势时所绑定的回调方法,并且会取消将touch事件继续传递给其所绑定的视图,如果手势识别器没有识别出对应的手势,那么touch事件会继续向手势识别器所绑定的视图传递。

虽然手势识别器并不是响应者链中的一员,但是手势识别器会观察touch事件,并延迟事件向所绑定的视图传递,这短暂的延迟使手势识别器有机会优先去识别手势处理touch事件。

对于UIKit提供的的标准控件,可以很方便地通过Target-Action的方式增加事件处理逻辑,默认情况下,发生在标准控件上的touch事件会优先被标准控件通过target-action方式处理,而不会去响应手势。举个例子,如果视图上绑定了单击的手势识别器,然后视图上又添加了一个UIButton,button通过target-action的方式设置了点击执行的操作,那么当点击button时,响应的是button的点击事件,而不是父视图上的单击手势。如果希望手势识别器优先标准控件的target-action进行事件处理,那么可以直接在标准控件上绑定手势识别器,比如上例,如果直接在button上绑定了单击手势,那么响应的就是单击手势了

具体应用场景

根据响应链规则,我们能更好的设计一个自定义悬浮窗。

  1. 悬浮窗作为UIWindow的rootController存在
  2. 覆写pointInside:withEvent:判断事件所属UIWindow
  3. 完善自己的事件命中测试,实现视图层级管理

详情参加Flex写法。

通过了解分析事件分发过程,我们能很清楚的知道要实现模拟点击我们只需把点击事件发送给UIApplication单例的队列就可以了。

  1. 创建UIEvent对象。
  2. 发送UIEvent对象[[UIApplication sharedApplication] sendEvent:event]
  1. hook UIApplication类的- sendEvent:(UIEvent *)event;方法
  2. 进行统计或修改

注:创建UITouch对象和对UITouch对象进行修改,可以参考kif-framework。

参考资料

  1. flex
  2. kif-framework
  3. using responders and the responder chain to handle events

本文由永利皇宫463登录发布于编程,转载请注明出处:iOS事件分发机制介绍与应用,iOS事件分发机制与

关键词: