![]()
UIControl是继承于UIView的一个子类,能够响应触摸事件(基本能够响应触摸事件都是继承于此类)
首先我们来了解一个触摸事件在iOS中是怎么进行的,系统是怎么反馈的
1手指触碰屏幕,屏幕感应到触碰后,将事件交由IOKit处理。
2IOKit将触摸事件封装成一个IOHIDEvent对象,并通过mach port传递给SpringBoad进程。
3SpringBoard进程因接收到触摸事件,触发了主线程runloop的source1事件源的回调。
此时SpringBoard会根据当前桌面的状态,判断应该由谁处理此次触摸事件。因为事件发生时,你可能正在桌面上翻页,也可能正在刷微博。若是前者(即前台无APP运行),则触发SpringBoard本身主线程runloop的source0事件源的回调,将事件交由桌面系统去消耗;若是后者(即有app正在前台运行),则将触摸事件通过IPC传递给前台APP进程,接下来的事情便是APP内部对于触摸事件的响应了。
那app是如何做响应的呢?
每个响应者都是一个UIResponder对象,即所有派生自UIResponder的对象,本身都具备响应事件的能力。因此以下类的实例都是响应者:
UIView
UIViewController
UIApplication
AppDelegate
响应者之所以能响应事件,因为其提供了4个处理触摸事件的方法:
这四个方法稍后再说,我们来看触摸事件是如何传递的
APP接收到触摸事件后,会被放入当前应用的一个事件队列中,出队后,application首先将事件传递给当前应用最后显示的窗口(UIWindow)询问其能否响应事件。若窗口能响应事件,则传递给子视图询问是否能响应,子视图若能响应则继续询问子视图。子视图询问的顺序是优先询问后添加的子视图,即子视图数组中靠后的视图。
可以看图
视图层级如下:
现在假设在E视图所处的屏幕位置触发一个触摸,应用接收到这个触摸事件事件后,先将事件传递给UIWindow,然后自下而上开始在子视图中寻找最佳响应者。事件传递的顺序如下所示:
·UIWindow将事件传递给其子视图A
·A判断自身能响应该事件,继续将事件传递给C(因为视图C比视图B后添加,因此优先传给C)。
·C判断自身能响应事件,继续将事件传递给F(同理F比E后添加)。
·F判断自身不能响应事件,C又将事件传递给E。
·E判断自身能响应事件,同时E已经没有子视图,因此最终E就是最佳响应者。
每个响应者必定都是UIResponder对象,通过4个响应触摸事件的方法来响应事件。每个UIResponder对象默认都已经实现了这4个方法,但是默认不对事件做任何处理,单纯只是将事件沿着响应链传递。若要截获事件进行自定义的响应 *** 作,就要重写相关的方法。例如,通过重写 touchesMoved: withEvent: 方法实现简单的视图拖动。
UIControl作为能够响应事件的控件,有其独特的跟踪方式
这4个方法和UIResponder的那4个方法几乎吻合,只不过UIControl只能接收单点触控,因此接收的参数是单个UITouch对象。这几个方法的职能也和UIResponder一致,用来跟踪触摸的开始、滑动、结束、取消。不过,UIControl本身也是UIResponder,因此同样有 touches 系列的4个方法。事实上,UIControl的 Tracking 系列方法是在 touch 系列方法内部调用的。比如 beginTrackingWithTouch 是在 touchesBegan 方法内部调用的, 因此它虽然也是UIResponder,但 touches 系列方法的默认实现和UIResponder本类还是有区别的。
当UIControl跟踪事件的过程中,识别出事件交互符合响应条件,就会触发target-action进行响应。UIControl控件通过 addTarget:action:forControlEvents: 添加事件处理的target和action,当事件发生时,UIControl通知target执行对应的action。说是“通知”其实很笼统,事实上这里有个action传递的过程。当UIControl监听到需要处理的交互事件时,会调用 sendAction:to:forEvent: 将target、action以及event对象发送给全局应用,Application对象再通过 sendAction:to:from:forEvent: 向target发送action。
因此,可以通过重写UIControl的 sendAction:to:forEvent: 或 sendAction:to:from:forEvent: 自定义事件执行的target及action。
另外,若不指定target,即 addTarget:action:forControlEvents: 时target传空,那么当事件发生时,Application会在响应链上从上往下寻找能响应action的对象。
按照时间顺序,事件的生命周期是这样的:
事件的产生和传递(事件如何从父控件传递到子控件并寻找到最合适的view、寻找最合适的view的底层实现、拦截事件的处理)->找到最合适的view后事件的处理(touches方法的重写,也就是事件的响应)
在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接受并处理事件,我们称之为“响应者对象”。以下都是继承自UIResponder的,所以都能接收并处理事件。
UIApplication
UIViewController
UIView(superView、subView)
事件的传递:是从上到下(父控件到子控件)
事件的响应:是从下到上(顺着响应者链条向上传递:子控件到父控件)
当用户用手指触摸屏幕时,会创建一个与手指相关的UITouch对象。
触摸事件的传递是从父控件传递到子控件
也就是UIApplication->window->寻找处理事件最合适的view
注 意: 如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件
1首先判断主窗口(keyWindow)自己是否能接受触摸事件
2判断触摸点是否在自己身上
3子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤)
4view,比如叫做fitView,那么会把这个事件交给这个fitView,再遍历这个fitView的子控件,直至没有更合适的view为止。
5如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view。
UIView不能接收触摸事件的三种情况:
1点击一个UIView或产生一个触摸事件A,这个触摸事件A会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。
2UIApplication会从事件对列中取出最前面的事件(此处假设为触摸事件A),把事件A传递给应用程序的主窗口(keyWindow)。
3窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,第一步已完成)
//==================" 系统框架 "==================
//==================" 类别 "==================
在事件(如触摸屏幕)产生后,系统是如何通知到你的 App,在 App 内部是如何进行传递,最终又是如何确定最终的响应者的。
这些肯定是有规则的,在 App 内部,一个事件会按照一个规则(视图层级关系)去遍历寻找这个事件的最佳响应者,但是这个响应者有可能不处理事件,那么它又需要沿着一定的规则(响应者链)去传递这个事件,如果最终都无人处理,那么将这个事件抛弃,也就是不处理。
先来看看什么是事件。
事件对应的对象为 UIEvent ,它有一个属性为 type,是 EventType 类型,EventType 是一个枚举类型:
所以 iOS 中的事件有四种:
触摸事件就是我们的 手指 或者 苹果的 Pencil(触笔) 在屏幕中所引发的互动,比如轻点、长按、滑动等 *** 作,是我们最常接触到的事件类型。触摸事件对象可以包含 一个或多个触摸 ,并且每个触摸由 UITouch 对象表示。当触摸事件发生时,系统会将其沿着线路传递,找到适当的响应者并调用适当的方法,例如 touchedBegan:withEvent: 。响应者对象会根据触摸来确定适当的方法。
触摸事件分为以下几类:
触摸事件对应的对象为 UITouch 。
iPhone 内置陀螺仪、加速和磁力仪,可以感知手机的运动情况。iOS 提供了 Core Motion 框架来处理这些运动事件。根据这些内置硬件,运动事件大致分为三类:
不过官方文档中指出,这些都是属于 Core Motion 库框架 ,Core Motion 库中的事件直接 由 Core Motion 内部进行处理 ,不会通过响应者链,所以 UIKit 框架能接收的事件暂时只包括摇一摇 (EventSubtypemotionShake)。
远程控制事件允许响应者对象 从外部附件或耳机接受命令 ,以便它可以管理音频和视频。目前 iOS 仅提供我们远程控制音频和视频的权限,即对音频实现暂停/播放、上一曲/下一曲、快进/快退 *** 作。以下是它能识别的类型:
iOS 90 之后提供了 3D Touch 事件,通过使用这个功能可以做如下 *** 作:
我们一般说的事件传递的起点在于 UIApplication 所管理的事件队列中开始 分发 的时候,但事件真正的 起点 在于你 手指触摸到屏幕 的那一刻开始(以触摸事件为例),那么在触摸屏幕到事件队列开始分发发生了什么?我们就以一个触摸事件来说明这个过程。
7 Source0 回掉内部,将 IOHIDEvent 对象转化为 UIEvent
UIWindow 接收到这个事件后, 开始传递事件
注意:对于一个手指的触摸,是UITouch每次状态改变的时候都会回调UIResponder相对应的处理方法。对于多个手指的触摸,也许多个UITouch状态的改变一起回调UIResponder的处理方法,也许每个UITouch状态的改变都会回调UIResponder的处理方法,例如,两个点击,可能只有一个touchesBegan的回调,两个touchesEnded的回调同时,多个UIControl状态改变只有一次touchesBegan等方法回调的参数touches里touch的个数我测试的时候只有一个,不要以为所有状态改变的UITouch只有一次回调时都会放到touches参数里。关于多点触摸的处理个人不建议在UITouch的响应机制里去做处理,里面具体原理并不明朗,实际开发中的借鉴也不多,涉及多点触摸使用手势更好。
UIResponder是iOS中用于处理用户事件的API,可以处理触摸事件、按压事件(3D touch)、远程控制事件、硬件运动事件。可以通过touchesBegan、pressesBegan、motionBegan、remoteControlReceivedWithEvent等方法,获取到对应的回调消息。UIResponder不只用来接收事件,还可以处理和传递对应的事件,如果当前响应者不能处理,则转发给其他合适的响应者处理。
应用程序通过响应者来接收和处理事件,响应者可以是继承自UIResponder的任何子类,例如UIView、UIViewController、UIApplication等。当事件来到时,系统会将事件传递给合适的响应者,并且将其成为第一响应者。
第一响应者未处理的事件,将会在响应者链中进行传递,传递规则由UIResponder的nextResponder决定,可以通过重写该属性来决定传递规则。当一个事件到来时,第一响应者没有接收消息,则顺着响应者链向后传递。
Gesture Recognizer 是对底层事件处理的封装,是为了让使用者能够更简单处理事件。
手势分为离散型手势(discrete gestures)和持续型手势(continuous gesture)。
手势响应过程:
手势状态:
UIControl是系统提供的能够以target-action模式处理触摸事件的控件,iOS中UIButton、UISegmentedControl、UISwitch等控件都是UIControl的子类。
值得注意的是,UIConotrol是UIView的子类,因此本身也具备UIResponder应有的身份。
UIControl作为控件类的基类,它是一个抽象基类,我们不能直接使用UIControl类来实例化控件,它只是为控件子类定义一些通用的接口,并提供一些基础实现,以在事件发生时,预处理这些消息并将它们发送到指定目标对象上。
UIControl的触发过程:
四个重要识别方法是在touchesBegan、touchesMoved、touchedEnded、touchesCancelled里回调的。
推测是:endTrackingWithTouch调用后识别了行为,做标记,返回到touchesEnded后,判断本UIControl是否易识别行为,调用行为回调。
App接收到触摸事件后,会被放入当前应用程序的UIApplication维护的事件队列中
由于事件一次只有一个,但是能够响应的事件的响应者众多,所以这就存在一个寻找第一响应者的过程。
调用方法,获取到被点击的视图,也就是第一响应者。
- (UIView )hitTest:(CGPoint)point withEvent:(UIEvent )event;
hitTest:withEvent:方法内部会通过调用pointInside:这个方法,来判断点击区域是否在视图上,是则返回YES,不是则返回NO。
经过Hit-Testing的过程后,UIApplication已经知道了第一响应者是谁,接下来要做的事情就是:
自定义的view的touchesBegan、touchesMoved、touchesEnded、touchedCancelled四个方法重写,记录打印过程,该view上添加tapGestureRecognized手势,该tapGestureRecognized也覆写了这四个方法。
点击view调用打印过程输出:
调用栈:
结合上面的输出和调用栈,我们可能并不能明确的看出有手势的时候点击的过程,不过如果你自己调试,是能得出如下结论的:
UIGestureRecognizer和UITouch的关系可以由UIGestureRecognizer的三个属性影响:cancelsTouchesInView、delaysTouchesBegan、delaysTouchesEnded。
本身就是在UIResponder的UITouchesBegan、UITouchesMoved、UITouchedEnded、UITouchesCancel四个回调中调用的。
UIControl的响应处理并不会影响UIResponder的响应链的处理,但是UIControl会影响另一个UIControl,子视图的UIControl具有优先级。
UIGestureRecognizer和UIControl并没有决定的优先级。
从iOS6开始在控件的父视图上面添加相应的手势,控件就会控制阻止手势行为,比如:
tap 手势在 UIButton,UISwitch,UIStepper,UISegmentControl,UIPageControl;
swipe 手势在 UISlider;
pan 手势在 UISwitch;
其他可能是手势优于控件的行为。
UIResponder有touchesBegan等四个方法,默认向superview传递。
所有需要自定义点击处理逻辑的UIResponder子类要覆盖这四个方法。
点击事件由四个方法处理。
UIButton的处理也是需要经过这四个方法。
UIGestureRecognizer也有touchesBegan等四个方法。
手势不在响应链里,但是也会观察它的view和subView的点击。
UIGestureRecognizer会影响UIResponder的四个响应点击的方法。
默认点击事件响应关键步骤说明:
1)用户手指点击屏幕,经过系统传递到UIApplication, UIApplication通过hitTest:方法找到对应UITouch发生的第一响应者view
2)UIApplication更新手势状态,从第一响应者上的手势到其视图层上所有先辈视图上的手势都会接收这个UITouch来更新手势状态
3)UIApplication将UITouch交给找到的第一响应着view处理
4)UIApplication更新手势状态,识别成功后,会向UITouch的第一响应者发送cancel方法
加上UIControl会让过程变得复杂,关于UIControl的原理,不清楚,也不敢妄下结论,依据网上和实际测试大致推断:
1)它不会影响UITouch本身的响应流程,但是会影响其他UIControl和UIGestureRecognizer的响应
2)自定义的UIControl是和UITouch本身的响应过程是一样的
3)系统定义的UIControl和UIGestureRecognizer同一个优先级,谁先识别出来,另一个就out了,但是UIControl和UIGestureRecognizer有一点不同,它并不会cancel UITouch的流程。
关于UITouch、UIGestureRecognizer、UIControl之间影响说明:
1)UITouch和UIGestureRecognizer:UIGestureRecognizer优先级高于UITouch,由UIGestureRecognizer的三个参数cancelsTouchesInView、delaysTouchesBegan、delaysTouchesEnded决定对UITouch的影响,默认情况下,UIGestureRecognizer识别成功后,会向UITouch发送cancel
避免:
1)尽量不要覆盖重写UIResponder的touchesBegin、touchesMoved、touchesCancelled、touchesEnded这四个方法,如果需要覆盖重写,逻辑应该尽量简单,不宜做复杂的处理,
2)不要自定义UIControl,直接使用系统定义的UIControl
3)UIControl上不要添加UIControl子视图
4)不要依赖UIGestureRecognizer的delayTouchBegin和delayTouchEnded
5)不要自定义UIGestureRecognizer
参考文章:
1) iOS 事件(UITouch、UIControl、UIGestureRecognizer)传递机制
2) Touch Event Handing 教学 — part 1
以上就是关于UIControl的触摸事件全部的内容,包括:UIControl的触摸事件、iOS全解14:事件的传递和响应机制、iOS 中的事件响应与处理等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)