转 iOS事件触摸事件处理机制

简介:

       移动平台上的开发主要关注数据以及数据的处理,事件的处理以及UI。所以事件的分发处理是很重要的一个环节,对于一个平台的优劣来说也是一项重要的参数。如果事件的分发设计的不好,一些复杂的UI场景就会变得很难写甚至没法写。从小屏没有触摸的功能机开始到现在大屏多点触摸的智能机,对于事件的分发处理基本思路都是一样的——链(设计模式中有个模式就是职责链chain of responsibility),只是判定的复杂程度不同。

        iOS中的事件有3类,触摸事件(单点,多点,手势)、传感器事件(加速度传感器)和远程控制事件,这里我介绍的是第一种事件的分发处理。

        

        上面的这张图来自苹果的官方。描述了Responder的链,同时也是事件处理的顺序。通过这两张图,我们可以发现:

        1. 事件顺着responder chain传递,如果一环不处理,则传递到下一环,如果都没有处理,最后回到UIApplication,再不处理就会抛弃

        2. view的下一级是包含它的viewController,如果没有viewController则是它的superView

        3. viewController的下一级是它的view的superView

        4. view之后是window,最后传给application,这点iOS会比OS X简单(application就一个,window也一个)

         总结出来传递规则是这样的:

        

        这样事件就会从first responder逐级传递过来,直到被处理或者被抛弃。


        由于UI的复杂,这个responder chain是需要根据事件来计算的。比如,我现在在一个view内加入了2个Button,先点击了一个,则first responder肯定是这个点击过的button,但我下面可以去点击另一个button,所以显然,当触摸事件来时,这个chain是需要重新计算更新的,这个计算的顺序是事件分发的顺序,基本上是分发的反过来。

        

        无论是哪种事件,都是系统本身先获得,是iOS系统来传给UIApplication的,由Application再决定交给谁去处理,所以如果我们要拦截事件,可以在UIApplication层面或者UIWindow层面去拦截。

        

        

        UIView是如何判定这个事件是否是自己应该处理的呢?iOS系统检测到一个触摸操作时会打包一个UIEvent对象,并放入Application的队列,Application从队列中取出事件后交给UIWindow来处理,UIWindow会使用hitTest:withEvent:方法来递归的寻找操作初始点所在的view,这个过程成为hit-test view。

        hitTest:withEvent:方法的处理流程如下:调用当前view的pointInside:withEvent:方法来判定触摸点是否在当前view内部,如果返回NO,则hitTest:withEvent:返回nil;如果返回YES,则向当前view内的subViews发送hitTest:withEvent:消息,所有subView的遍历顺序是从数组的末尾向前遍历,直到有subView返回非空对象或遍历完成。如果有subView返回非空对象,hitTest方法会返回这个对象,如果每个subView返回都是nil,则返回自己。

处理原理如下:

• 当用户点击屏幕时,会产生一个触摸事件,系统会将该事件加入到一个由UIApplication管理的事件队列中

• UIApplication会从事件队列中取出最前面的事件进行分发以便处理,通常,先发送事件给应用程序的主窗口(UIWindow)

• 主窗口会调用hitTest:withEvent:方法在视图(UIView)层次结构中找到一个最合适的UIView来处理触摸事件

(hitTest:withEvent:其实是UIView的一个方法,UIWindow继承自UIView,因此主窗口UIWindow也是属于视图的一种)

• hitTest:withEvent:方法大致处理流程是这样的:

首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内:

▶ 若pointInside:withEvent:方法返回NO,说明触摸点不在当前视图内,则当前视图的hitTest:withEvent:返回nil

▶ 若pointInside:withEvent:方法返回YES,说明触摸点当前视图内,则遍历当前视图的所有子视图(subviews),调用子视图的hitTest:withEvent:方法重复前面的步骤,子视图的遍历顺序是从top到bottom,即从subviews数组的末尾向前遍历,直到有子视图的hitTest:withEvent:方法返回非空对象或者全部子视图遍历完毕:

▷ 若第一次有子视图的hitTest:withEvent:方法返回非空对象,则当前视图的hitTest:withEvent:方法就返回此对象,处理结束

▷ 若所有子视图的hitTest:withEvent:方法都返回nil,则当前视图的hitTest:withEvent:方法返回当前视图自身(self)

• 最终,这个触摸事件交给主窗口的hitTest:withEvent:方法返回的视图对象去处理。

拿到这个UIView后,就调用该UIView的touches系列方法。

1.2、消息处理过程,在找到的那个视图里处理,处理完后根据需要,利用响应链nextResponder可将消息往下一个响应者传递。

UIAppliactionDelegate <- UIWindow <- UIViewController <- UIView <- UIView

【关键】:要理解的有三点:1、iOS判断哪个界面能接受消息是从View层级结构的父View向子View传递,即树状结构的根节点向叶子节点递归传递。2、hitTest和pointInside成对,且hitTest会调用pointInside。3、iOS的消息处理是,当消息被人处理后默认不再向父层传递。

        好了,我们还是看个例子:

        

        这里ViewA包含ViewB和ViewC,ViewC中继续包含ViewD和ViewE。假设我们点击了viewE区域,则hit-test View判定过程如下:

       1. 触摸在A内部,所以需要检查B和C

       2. 触摸不在B内部,在C内部,所以需要检查D和E

       3. 触摸不在D内部,但在E内部,由于E已经是叶子了,所以判定到此结束


        我们可以运行一段代码来验证,首先从UIView继承一个类myView,重写里面的

[objc] view plain copy 在CODE上查看代码片派生到我的代码片

  1. - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event  

  2. {  

  3.     UIView *retView = nil;  

  4.     NSLog(@"hitTest %@ Entry! event=%@"self.name, event);  

  5.       

  6.     retView = [super hitTest:point withEvent:event];  

  7.     NSLog(@"hitTest %@ Exit! view = %@"self.name, retView);  

  8.      

  9.     return retView;  

  10. }  


[objc] view plain copy 在CODE上查看代码片派生到我的代码片

  1. - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event  

  2. {  

  3.     BOOL ret = [super pointInside:point withEvent:event];  

  4. //    if ([self.name isEqualToString:@"viewD"]) {  

  5. //        ret = YES;  

  6. //    }  

  7.     if (ret) {  

  8.         NSLog(@"pointInside %@ = YES"self.name);  

  9.     } else {  

  10.         NSLog(@"pointInside %@ = NO"self.name);  

  11.     }  

  12.       

  13.     return ret;  

  14. }  

        在viewDidLoad方法中手动加入5个view,都是myView的实例。

[objc] view plain copy 在CODE上查看代码片派生到我的代码片

  1. - (void)viewDidLoad  

  2. {  

  3.     [super viewDidLoad];  

  4.       

  5.     _viewA = [[myView alloc] initWithFrame:CGRectMake(1010300200) Color:[UIColor blackColor] andName:@"viewA"];  

  6.     [self.view addSubview:_viewA];  

  7.     [_viewA release];  

  8.       

  9.     _viewB = [[myView alloc] initWithFrame:CGRectMake(10240300200) Color:[UIColor blackColor] andName:@"viewB"];  

  10.     [self.view addSubview:_viewB];  

  11.     [_viewB release];  

  12.       

  13.     _viewC = [[myView alloc] initWithFrame:CGRectMake(1010120180) Color:[UIColor blueColor] andName:@"viewC"];  

  14.     [_viewB addSubview:_viewC];  

  15.     [_viewC release];  

  16.       

  17.     _viewD = [[myView alloc] initWithFrame:CGRectMake(17010120180) Color:[UIColor blueColor] andName:@"viewD"];  

  18.     [_viewB addSubview:_viewD];  

  19.     [_viewD release];  

  20.       

  21.     _viewE = [[myView alloc] initWithFrame:CGRectMake(304060100) Color:[UIColor redColor] andName:@"viewE"];  

  22.     [_viewD addSubview:_viewE];  

  23.     [_viewE release];  

  24.   

  25. }  

        这个样式如下:


        当我点击viewE的时候,打印信息如下:

2014-01-25 18:32:46.538 eventDemo[1091:c07] hitTest viewB Entry! event=<UITouchesEvent: 0x8d0cae0> timestamp: 6671.26 touches: {(

)}

2014-01-25 18:32:46.538 eventDemo[1091:c07] pointInside viewB = YES

2014-01-25 18:32:46.539 eventDemo[1091:c07] hitTest viewD Entry! event=<UITouchesEvent: 0x8d0cae0> timestamp: 6671.26 touches: {(

)}

2014-01-25 18:32:46.539 eventDemo[1091:c07] pointInside viewD = YES

2014-01-25 18:32:46.539 eventDemo[1091:c07] hitTest viewE Entry! event=<UITouchesEvent: 0x8d0cae0> timestamp: 6671.26 touches: {(

)}

2014-01-25 18:32:46.540 eventDemo[1091:c07] pointInside viewE = YES

2014-01-25 18:32:46.540 eventDemo[1091:c07] hitTest viewE Exit! view = <myView: 0x8c409f0; frame = (30 40; 60 100); layer = <CALayer: 0x8c40a90>>

2014-01-25 18:32:46.540 eventDemo[1091:c07] hitTest viewD Exit! view = <myView: 0x8c409f0; frame = (30 40; 60 100); layer = <CALayer: 0x8c40a90>>

2014-01-25 18:32:46.541 eventDemo[1091:c07] hitTest viewB Exit! view = <myView: 0x8c409f0; frame = (30 40; 60 100); layer = <CALayer: 0x8c40a90>>

2014-01-25 18:32:46.541 eventDemo[1091:c07] touchesBegan viewE

2014-01-25 18:32:46.624 eventDemo[1091:c07] touchesEnded viewE

        从打印信息可以看到,先判断了viewB,然后是viewD,最后是viewE,但事件就是直接传给了viewE。

目录
相关文章
|
存储
13-iOS消息转发机制以及常用场景
13-iOS消息转发机制以及常用场景
75 0
|
16天前
|
存储 安全 数据安全/隐私保护
探索安卓与iOS的隐私保护机制####
【10月更文挑战第15天】 本文深入剖析了安卓和iOS两大操作系统在隐私保护方面的策略与技术实现,旨在揭示两者如何通过不同的技术手段来保障用户数据的安全与隐私。文章将逐一探讨各自的隐私控制功能、加密措施以及用户权限管理,为读者提供一个全面而深入的理解。 ####
32 1
|
4月前
|
调度 Swift Android开发
苹果iOS新手开发之Swift中的并发任务和消息机制
Swift的消息机制类似Android的Handler,实现任务调度有三种方式: 1. **Grand Central Dispatch (GCD)**:使用`DispatchQueue`在主线程或后台线程执行任务。 2. **OperationQueue**:提供高级接口管理`Operation`对象。 3. **RunLoop**:处理事件如输入源、计时器,类似Android的`Looper`和`Handler`。 **示例**: - GCD:在不同线程执行代码块。 - OperationQueue:创建操作并执行。 - RunLoop:用Timer添加到RunLoop中。
97 2
|
5月前
|
安全 算法 数据安全/隐私保护
探索iOS与Android的隐私保护机制
【6月更文挑战第5天】在数字时代,隐私保护已成为用户最关心的问题之一。iOS和Android作为两大主流操作系统,各自发展出了独特的隐私保护技术。本文将深入探讨这两个平台在隐私保护方面的策略、技术和挑战。
130 3
|
iOS开发
(六)IOS手势和触摸的用法
(六)IOS手势和触摸的用法
224 0
|
API iOS开发
iOS 沙盒机制
iOS 沙盒机制
148 0
|
iOS开发
iOS开发 - touchBegan事件判断点击的位置在View上还是在View的子View上
iOS开发 - touchBegan事件判断点击的位置在View上还是在View的子View上
275 0
iOS开发 - touchBegan事件判断点击的位置在View上还是在View的子View上
|
Android开发 iOS开发
关于监听微信浏览器返回按钮事件处理安卓IOS通用
关于监听微信浏览器返回按钮事件处理安卓IOS通用
343 0
关于监听微信浏览器返回按钮事件处理安卓IOS通用
|
iOS开发
iOS之UIPickerView滚动事件
在开发中,我们会用到pickerview滚动条,那如何监听到pickview的滚动事件呢,之前开发就遇到过一个问题,快速滑动秒选确定按钮,地址显示不对的问题,解决办法为在点选确定辅助按钮的时候判断当时的pickerView是否正在滚动,如果在滚动则不允许触发点选确定后的其他操作。
383 0
iOS之UIPickerView滚动事件
|
iOS开发
iOS开发- runtime基本用法解析和用runtime给键盘添加工具栏和按钮响应事件
iOS开发- runtime基本用法解析和用runtime给键盘添加工具栏和按钮响应事件
138 0