官方文档说明:《Event Handling Guide for iOS》,本文参考转载文章,并参照官方文档补充说明。html
本篇内容将围绕iOS中事件及其传递机制进行学习和分析。在iOS中,事件分为三类:ios
这三类事件共同构成了iOS设备丰富的操做方式和使用体验,本次就首先来针对第一类事件:触控事件,进行学习和分析。设计模式
Gesture Recognizers是一类手势识别器对象,它能够附属在你指定的View上,而且为其设定指定的手势操做,例如是点击、滑动或者是拖拽。当触控事件 发生时,设置了Gesture Recognizers的View会先经过识别器去拦截触控事件,若是该触控事件是事先为View设定的触控监听事件,那么Gesture Recognizers将会发送动做消息给目标处理对象,目标处理对象则对此次触控事件进行处理,先看看以下流程图。app
在iOS中,View就是咱们在屏幕上看到的各类UI控件,当一个触控事件发生时,Gesture Recognizers会先获取到指定的事件,而后发送动做消息(action message)给目标对象(target),目标对象就是ViewController,在ViewController中经过事件方法完成对该事件的处理。Gesture Recognizers能设置诸如单击、滑动、拖拽等事件,经过Action-Target这种设计模式,好处是能动态为View添加各类事件监听,而不用去实现一个View的子类去完成这些功能。框架
以上过程就是咱们在开发中在方法中常见的设置action和设置target,例如为UIButton设置监听事件等。ide
在UIKit框架中,系统为咱们事先定义好了一些经常使用的手势识别器,包括点击、双指缩放、拖拽、滑动、旋转以及长按。经过这些手势识别器咱们能够构造丰富的操做方式。布局
在上表中能够看到,UIKit框架中已经提供了诸如UITapGestureRecognizer在内的六种手势识别器,若是你须要实现自定义的手势识别器,也能够经过继承UIGestureRecognizer类并重写其中的方法来完成,这里咱们就不详细讨论了。学习
每个Gesture Recognizer关联一个View,可是一个View能够关联多个Gesture Recognizer,由于一个View可能还能响应多种触控操做方式。为了使gesture recognizer识别发生在view上面的手势,你必须attach gesture recognizer to that view。当一个触控事件发生时,Gesture Recognizer接收一个touch发生的消息要先于View自己,结果就是Gesture Recognizer能够表明view回应视图上的touches事件。ui
触控动做同时分为连续动做(continuous)和不连续动做(discrete),连续动做例如滑动和拖拽,它会持续一小段时间,而不连续动做例如单击,它瞬间就会完成,在这两类事件的处理上又稍有不一样。对于不连续动做,Gesture Recognizer只会给ViewContoller发送一个单一的动做消息(action message),而对于连续动做,Gesture Recognizer会发送多条动做消息给ViewController,直到全部的事件都结束。 spa
为一个View添加GestureRecognizer有两种方式,一种是经过InterfaceBuilder实现,另外一种就是经过代码实现,咱们看看经过代码来如何实现。
1 - (void)viewDidLoad { 2 [super viewDidLoad]; 3 4 // 建立并初始化手势对象 5 UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] 6 initWithTarget:self action:@selector(respondToTapGesture:)]; 7 8 // 指定操做为单击一次 9 tapRecognizer.numberOfTapsRequired = 1; 10 11 // 为当前View添加GestureRecognizer 12 [self.view addGestureRecognizer:tapRecognizer]; 13 14 // ... 15 }
经过上述代码,咱们实现了为当前MyViewController的View添加一个单击事件,首先构造了UITapGestureRecognizer对象,指定了target为当前ViewController自己,action就是后面本身实现的处理方法,这里就呼应了前文提到的Action-Target模式。
在事件处理过程当中,这两种方式所处的状态又各有不一样,首先,全部的触控事件最开始都是处于可用状态(Possible),对应UIKit里面的UIGestureRecognizerStatePossible类,若是是不连续动做事件,则状态只会从Possible转变为已识别状态(Recognized,UIGestureRecognizerStateRecognized)或者是失败状态(Failed,UIGestureRecognizerStateFailed)。例如一次成功的单击动做,就对应了Possible-Recognized这个过程。
若是是连续动做事件,若是事件没有失败而且连续动做的第一个动做被成功识别(Recognized),则从Possible状态转移到Began(UIGestureRecognizerStateBegan)状态,这里表示连续动做的开始,接着会转变为Changed(UIGestureRecognizerStateChanged)状态,在这个状态下会不断循环的处理连续动做,直到动做执行完成变转变为Recognized已识别状态,最终该动做会处于完成状态(UIGestureRecognizerStateEnded),另外,连续动做事件的处理状态会从Changed状态转变为Canceled(UIGestureRecognizerStateCancelled)状态,缘由是识别器认为当前的动做已经不匹配当初对事件的设定了。每一个动做状态的变化,Gesture Recognizer都会发送消息(action message)给Target,也就是ViewController,它能够根据这些动做消息进行相应的处理。例如一次成功的滑动手势动做就包括按下、移动、抬起的过程,分别对应了Possible-Began-Changed-Recognized这个过程。
在屏幕上的每一次动做事件都是一次Touch,在iOS中用UITouch对象表示每一次的触控,多个Touch组成一次Event,用UIEvent来表示一次事件对象。
在上述过程当中,完成了一次双指缩放的事件动做,每一次手指状态的变化都对应事件动做处理过程当中得一个阶段。经过Began-Moved-Ended这几个阶段的动做(Touch)共同构成了一次事件(Event)。在事件响应对象UIResponder中有对应的方法来分别处理这几个阶段的事件。
后面的参数分别对应UITouchPhaseBegan、UITouchPhaseMoved、UITouchPhaseEnded、UITouchPhaseCancelled这几个类。用来表示不一样阶段的状态。
如上图,iOS中事件传递首先从App(UIApplication)开始,接着传递到Window(UIWindow),在接着往下传递到View以前,Window会将事件交给GestureRecognizer,若是在此期间,GestureRecognizer识别了传递过来的事件,则该事件将不会继续传递到View去,而是像咱们以前说的那样交给Target(ViewController)进行处理。
一般,一个iOS应用中,在一块屏幕上一般有不少的UI控件,也就是有不少的View,那么当一个事件发生时,如何来肯定是哪一个View响应了这个事件呢,接下来咱们就一块儿来看看。
什么是hit-test view呢?简单来讲就是你触发事件所在的那个View,寻找hit-test view的过程就叫作Hit-Testing。那么,系统是如何来执行Hit-Testing呢,首先假设如今有以下这么一个UI布局,一种有ABCDE五个View。
假设一个单击事件发生在了View D里面,系统首先会从最顶层的View A开始寻找,发现事件是在View A或者其子类里面,那么接着从B和C找,发现事件是在C或者其子类里面,那么接着到C里面找,这时发现事件是在D里面,而且D已经没有子类了,那么hit-test view就是View D啦。
响应者对象是可以响应而且处理事件的对象,UIResponder是全部响应者对象的父类,包括UIApplication、UIView和UIViewController都是UIResponder的子类。也就意味着全部的View和ViewController都是响应者对象。
第一响应者是第一个接收事件的View对象,咱们在Xcode的Interface Builder画视图时,能够看到视图结构中就有First Responder。
这里的First Responder就是UIApplication了。另外,咱们能够控制一个View让其成为First Responder,经过实现 canBecomeFirstResponder方法并返回YES可使当前View成为第一响应者,或者调用View的becomeFirstResponder方法也能够,例如当UITextField调用该方法时会弹出键盘进行输入,此时输入框控件就是第一响应者。
如上所说,,若是hit-test view不能处理当前事件,那么事件将会沿着响应者链(Responder Chain)进行传递,知道遇到能处理该事件的响应者(Responsder Object)。经过下图,咱们来看看两种不一样状况下得事件传递机制。
左边的状况,接收事件的initial view若是不能处理该事件而且她不是顶层的View,则事件会往它的父View进行传递。initial view的父View获取事件后若是仍不能处理,则继续往上传递,循环这个过程。若是顶层的View仍是不能处理这个事件的话,则会将事件传递给它们的ViewController,若是ViewController也不能处理,则传递给Window(UIWindow),此时Window不能处理的话就将事件传递给Application(UIApplication),最后若是连Application也不能处理,则废弃该事件。
右边图的流程惟一不一样就在于,若是当前的ViewController是由层级关系的,那么当子ViewController不能处理事件时,它会将事件继续往上传递,直到传递到其Root ViewController,后面的流程就跟以前分析的同样了。
这就是事件响应者链的传递机制,经过这些内容,咱们能够更深刻的了解事件在iOS中得传递机制,对咱们在实际开发中更好的理解事件操做的原理有很大的帮助,也对咱们实现复杂布局进行事件处理时增添了多一份的理解。
UIResponder中定义了一系列对事件的处理方法,他们分别是:
从方法名字能够知道,他们分别对应了屏幕事件的开始、移动、结束和取消几个阶段,前三个阶段理解都没问题,最后一个取消事件的触发时机是在诸如忽然来电话或是系统杀进程时调用。这些方法的第一个参数定义了UITouch对象的一个集合(NSSet),它的数量表示了此次事件是几个手指的操做,目前iOS设备支持的多点操做手指数最可能是5。第二个参数是当前的UIEvent对象。下图展现了一个UIEvent对象与多个UITouch对象之间的关系。
首先,新建一个自定义的View继承于UIView,并实现上述提到的事件处理方法,咱们能够经过判断UITouch的tapCount属性来决定响应单击、双击或是屡次点击事件。
1 #import "MyView.h" 2 @implementation MyView 3 4 -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 5 { 6 7 } 8 9 -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event 10 { 11 12 } 13 14 -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event 15 { 16 for (UITouch *aTouch in touches) { 17 if (aTouch.tapCount == 2) { 18 // 处理双击事件 19 [self respondToDoubleTapGesture]; 20 } 21 } 22 } 23 24 -(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event 25 { 26 27 } 28 29 -(void)respondToDoubleTapGesture 30 { 31 NSLog(@"respondToDoubleTapGesture"); 32 } 33 34 @end
滑动事件通常包括上下滑动和左右滑动,判断是不是一次成功的滑动事件须要考虑一些问题,好比大部分状况下,用户进行一次滑动操做,此次滑动是不是在一条直线上?或者是不是基本能保持一条直线的滑动轨迹。或者判断是上下滑动仍是左右滑动等。另外,滑动手势通常有一个起点和一个终点,期间是在屏幕上画出的一个轨迹,因此须要对这两个点进行判断。咱们修改上述的MyView.m的代码来实现一次左右滑动的事件响应操做。
1 #import "MyView.h" 2 3 #define HORIZ_SWIPE_DRAG_MIN 12 //水平滑动最小间距 4 #define VERT_SWIPE_DRAG_MAX 4 //垂直方向最大偏移量 5 6 @implementation MyView 7 8 -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 9 { 10 UITouch *aTouch = [touches anyObject]; 11 // startTouchPosition是一个CGPoint类型的属性,用来存储当前touch事件的位置 12 self.startTouchPosition = [aTouch locationInView:self]; 13 } 14 15 -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event 16 { 17 18 } 19 20 -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event 21 { 22 UITouch *aTouch = [touches anyObject]; 23 CGPoint currentTouchPosition = [aTouch locationInView:self]; 24 25 // 判断水平滑动的距离是否达到了设置的最小距离,而且是不是在接近直线的路线上滑动(y轴偏移量) 26 if (fabsf(self.startTouchPosition.x - currentTouchPosition.x) >= HORIZ_SWIPE_DRAG_MIN && 27 fabsf(self.startTouchPosition.y - currentTouchPosition.y) <= VERT_SWIPE_DRAG_MAX) 28 { 29 // 知足if条件则认为是一次成功的滑动事件,根据x坐标变化判断是左滑仍是右滑 30 if (self.startTouchPosition.x < currentTouchPosition.x) { 31 [self rightSwipe];//右滑响应方法 32 } else { 33 [self leftSwipe];//左滑响应方法 34 } 35 //重置开始点坐标值 36 self.startTouchPosition = CGPointZero; 37 } 38 } 39 40 -(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event 41 { 42 //当事件因某些缘由取消时,重置开始点坐标值 43 self.startTouchPosition = CGPointZero; 44 } 45 46 -(void)rightSwipe 47 { 48 NSLog(@"rightSwipe"); 49 } 50 51 -(void)leftSwipe 52 { 53 NSLog(@"leftSwipe"); 54 } 55 56 @end
在屏幕上咱们能够拖动某一个控件(View)进行移动,这种事件成为拖拽事件,其实现原理就是在不改变View的大小尺寸的前提下改变View的显示坐标值,为了达到动态移动的效果,咱们能够在move阶段的方法中进行坐标值的动态更改,仍是重写MyView.m的事件处理方法,此次在touchesMove方法中进行处理。
1 #import "MyView.h" 2 @implementation MyView 3 4 -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 5 { 6 7 } 8 9 -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event 10 { 11 UITouch *aTouch = [touches anyObject]; 12 //获取当前触摸操做的位置坐标 13 CGPoint loc = [aTouch locationInView:self]; 14 //获取上一个触摸点的位置坐标 15 CGPoint prevloc = [aTouch previousLocationInView:self]; 16 17 CGRect myFrame = self.frame; 18 //改变View的x、y坐标值 19 float deltaX = loc.x - prevloc.x; 20 float deltaY = loc.y - prevloc.y; 21 myFrame.origin.x += deltaX; 22 myFrame.origin.y += deltaY; 23 //从新设置View的显示位置 24 [self setFrame:myFrame]; 25 } 26 27 -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event 28 { 29 30 } 31 32 -(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event 33 { 34 35 } 36 37 @end
以前提到过UIEvent包含了一系列的UITouch对象构成一次事件,当设计多点触控操做时,可与对UIEvent对象内的UITouch对象进行处理,好比实现一个双指缩放的功能。
1 #import "MyView.h" 2 @implementation MyView 3 { 4 BOOL pinchZoom; 5 CGFloat previousDistance; 6 CGFloat zoomFactor; 7 } 8 9 -(id)init 10 { 11 self = [super init]; 12 if (self) { 13 pinchZoom = NO; 14 //缩放前两个触摸点间的距离 15 previousDistance = 0.0f; 16 zoomFactor = 1.0f; 17 } 18 return self; 19 } 20 21 -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 22 { 23 if(event.allTouches.count == 2) { 24 pinchZoom = YES; 25 NSArray *touches = [event.allTouches allObjects]; 26 //接收两个手指的触摸操做 27 CGPoint pointOne = [[touches objectAtIndex:0] locationInView:self]; 28 CGPoint pointTwo = [[touches objectAtIndex:1] locationInView:self]; 29 //计算出缩放先后两个手指间的距离绝对值(勾股定理) 30 previousDistance = sqrt(pow(pointOne.x - pointTwo.x, 2.0f) + 31 pow(pointOne.y - pointTwo.y, 2.0f)); 32 } else { 33 pinchZoom = NO; 34 } 35 } 36 37 -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event 38 { 39 if(YES == pinchZoom && event.allTouches.count == 2) { 40 NSArray *touches = [event.allTouches allObjects]; 41 CGPoint pointOne = [[touches objectAtIndex:0] locationInView:self]; 42 CGPoint pointTwo = [[touches objectAtIndex:1] locationInView:self]; 43 //两个手指移动过程当中,两点之间距离 44 CGFloat distance = sqrt(pow(pointOne.x - pointTwo.x, 2.0f) + 45 pow(pointOne.y - pointTwo.y, 2.0f)); 46 //换算出缩放比例 47 zoomFactor += (distance - previousDistance) / previousDistance; 48 zoomFactor = fabs(zoomFactor); 49 previousDistance = distance; 50 51 //缩放 52 self.layer.transform = CATransform3DMakeScale(zoomFactor, zoomFactor, 1.0f); 53 } 54 } 55 56 -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event 57 { 58 if(event.allTouches.count != 2) { 59 pinchZoom = NO; 60 previousDistance = 0.0f; 61 } 62 } 63 64 -(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event 65 { 66 67 } 68 69 @end
上面实现的方式有一点不足之处就是必须两个手指同时触摸按下才能达到缩放的效果,并不能达到相册里面那样一个手指触摸后,另外一个手指按下也能够缩放。若是须要达到和相册照片缩放的效果,须要同时控制begin、move、end几个阶段的事件处理。这个不足就留给感兴趣的同窗本身去实现了。
参考文章: