iOS UITouch事件解决-原理篇

作者 : 开心源码 本文共6525个字,预计阅读时间需要17分钟 发布时间: 2022-05-12 共88人阅读

前言:

事件 是界面交互(或者人机交互)的最基本组成之一。没有它手机上的App就失去了存在的意义。
一个事件的周期:事件的产生——事件的传递——事件的响应
事件传递到响应实现原理:hitTest:withEvent: 和pointInside:withEvent 和touches方法

事件传递和事件响应区别:

事件的传递是自上而下(父控件到子控件);事件的响应是自下而上(顺着响应者链条向上传递:子控件到父控件。

iOS中的事件类型

  • 触摸事件(touch events)
  • 按压事件(press events)
  • 摇晃事件(shake – motions events)
  • 远程控制事件 (remote – controls events)
  • 编辑菜单消息事件(editing menu messages)

一、事件的产生和传递

  • 发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中为什么是队列而不是栈?由于队列的特点是先进先出,先产生的事件先解决才符合常理,所以把事件增加到队列。
  • UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便解决,通常,先发送事件给应使用程序的主窗口(keyWindow)。
  • 主窗口会在视图层次结构中找到一个最合适的视图来解决触摸事件,这也是整个事件解决过程的第一步。
    找到合适的视图控件后,就会调使用视图控件的touches方法来作具体的事件解决。

1.1 事件传递原理

事件传递是通过下面两个方法完成的:

  1. hitTest:withEvent: (UIView 的实例方法)
  2. pointInside:withEvent (UIView 的事例方法)
传递过程:
  1. 主窗口接收到应使用传过来的事件后,首先判断能不能接收事件。假如能,就会判> 断触摸点在不在自身范围内。(通过pointInside:withEvent判断)假如不在,就不解决。假如在就进行第二步。
  2. 假如触摸点在自己的坐标范围内,那么窗口会从后往前遍历自己的子控件,来寻觅最合适的view。(通过hitTest:withEvent:方法递归寻觅)
  3. 遍历到每一个子控件后,又会重复上面的两个步骤(传递事件给子控件,1.判断子控件是否接受事件,2.点在不在子控件上)
  4. 按照上面步骤循环遍历子控件,直到找到最合适的view,假如没有更合适的子控件,那么自己就是最合适的view。
UIView不能接收事件的三种情况:1. 不允许交互 `userInteractionEnabled` = NO2. 透明度 `alpha` < 0.013. 父视图或者者子视图的 `hidden` = YES

1.2 hitTest:withEvent:代码具体实现

// 什么时候调使用:只需事件一传递给一个控件,那么这个控件就会调使用自己的这个方法// 作使用:寻觅并返回最合适的view// UIApplication -> [UIWindow hitTest:withEvent:]寻觅最合适的view告诉系统// point:当前手指触摸的点// event:触摸事件- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{   // 判断自己是否接收事件    if (self.userInteractionEnabled == NO || self.alpha <= 0.01 || self.hidden == YES) {        return nil;    }        // 判断点是不是在当前视图上    if (![self pointInside:point withEvent:event]) {        return nil;    }        // 从后往前遍历自己的子控件,寻觅更合适的View    for (long i = self.subviews.count - 1; i >= 0; i--) {        // 获取子控件        UIView *childView = self.subviews[i];                // 将自己坐标系的点转化成子控件坐标系的点        CGPoint childPoint = [self convertPoint:point toView:childView];                // 递归调使用hitTest方法,寻觅更加合适的View        UIView *fitView = [childView hitTest:childPoint withEvent:event];        if (fitView) {            return fitView;        }    }        // 没有找到比自己更适合的View    return self;}// 作使用:判断下传入过来的点在不在方法调使用者的坐标系上// point:是方法调使用者坐标系上的点- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{     return NO;}

1.3 重写 hitTest:withEvent: 方法实使用之处

1.3.1 阻拦事件的解决
  • 正由于hitTest:withEvent:方法可以返回最合适的view,所以可以通过重写hitTest:withEvent:方法,返回指定的view作为最合适的view。
  • 不论点击哪里,最合适的view都是hitTest:withEvent:方法中返回的那个view。
  • 通过重写hitTest:withEvent:,即可以阻拦事件的传递过程,想让谁解决事件谁就解决事件。
栗子:
//BView是一个view的子视图//BView是UIButton的父视图,但是BView的userInteractionEnable= NO//通过hitTest可以阻拦事件的功能 即可以指定UIButton作为响应者。- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{    for (UIView *v  in self.subviews) {        if ([v isKindOfClass:NSClassFromString(@"BView")]) {            for (UIView *vv  in v.subviews) {                if ([vv isKindOfClass:NSClassFromString(@"UIButton")]) {                    CGPoint insideP = [self convertPoint:point toView:vv];                    BOOL isInsidePoint =  [vv pointInside:insideP withEvent:event];                    if (isInsidePoint) {                        return vv;                        break;                    }                }            }            break;        }    }    return self;}
1.3.2 return nil作使用
return nil的含义:

hitTest:withEvent:中return nil的意思是调使用当前hitTest:withEvent:方法的view不是合适的view,子控件也不是合适的view。

特殊使用法

谁都不能解决事件,窗口也不能解决。
重写window的hitTest:withEvent:方法return nil
只能有窗口解决事件。

控制器的view的hitTest:withEvent:方法return nil或者者window的hitTest:withEvent:方法return self

二、事件的响应

2.1响应者对象(UIResponder)

iOS中,我们最常接触的事件是触摸事件,什么样的对象才能解决触摸事件呢?

只有继承UIResponder的类或者者子类才可以响应和解决事件。由于UIResponder:是使用于响应和解决事件的笼统对象。

具体可以解决触摸事件的类如下:

  • UIApplication
  • UIViewController (包括它的子类)
  • UIView (包括它的子类)

2.2 下面是UIResponder类提供的响应和解决触摸事件的4方法:

//开始接触屏幕,就会调使用一次- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;//手指开始移动就会调使用(这个方法会频繁的调使用,其实一接触屏幕就会屡次调使用)- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;//手指离开屏幕时,调使用一次- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;//触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,或者者view上面增加手势时,系统会自动调使用view的下面方法- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

通过这4个方法即可以知道触摸事件的整个过程。四个方法中的两个参数UITouch 和 UIEvent 解释下:

UITouch对象
  • 当使用户使用一根手指触摸屏幕时,会创立一个与手指相关联的UITouch对象
  • 一根手指对应一个UITouch对象
UITouch作使用
  • 保存着跟手指相关的信息,比方触摸的位置、时间、阶段
  • 当手指移动时,系统会升级同一个UITouch对象,使之能够一直保存该手指在的触 摸位置
  • 当手指离开屏幕时,系统会销毁相应的UITouch对象
UITouch的主要属性和方法
//触摸产生时所处的窗口@property(nonatomic,readonly,retain) UIWindow *window; //触摸产生时所处的视图@property(nonatomic,readonly,retain) UIView *view; //短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或者更多的点击@property(nonatomic,readonly) NSUInteger tapCount; //记录了触摸事件产生或者变化时的时间,单位是秒@property(nonatomic,readonly) NSTimeInterval timestamp; //当前触摸事件所处的状态@property(nonatomic,readonly) UITouchPhase phase;//获取手指与屏幕的接触半径 IOS8以后可使用 只读@property(nonatomic,readonly) CGFloat majorRadius NS_AVAILABLE_IOS(8_0);//获取手指与屏幕的接触半径的误差 IOS8以后可使用 只读@property(nonatomic,readonly) CGFloat majorRadiusTolerance NS_AVAILABLE_IOS(8_0);//触摸时所在的窗口 只读@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(3_2);//获取触摸压力值,一般的压力感应值为1.0 IOS9 只读@property(nonatomic,readonly) CGFloat force NS_AVAILABLE_IOS(9_0);//获取最大触摸压力值@property(nonatomic,readonly) CGFloat maximumPossibleForce NS_AVAILABLE_IOS(9_0);//获得在指定视图的位置// 返回值表示触摸在view上的位置// 这里返回的位置是针对view的坐标系的(以view的左上角为原点(0,0))// 调使用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置- (CGPoint)locationInView:(nullable UIView *)view;//该方法记录了前一个触摸点的位置- (CGPoint)previousLocationInView:(nullable UIView *)view;
UIEvent 对象

每产生一个事件,就会产生一个UIEvent对象
UIEvent:称为事件对象,记录事件产生的时刻和类型

主要属性和方法

 //事件类型 @property(nonatomic,readonly) UIEventType    type;//事件子类型 @property(nonatomic,readonly) UIEventSubtype  subtype;//事件产生的时间@property(nonatomic,readonly) NSTimeInterval  timestamp;//返回值:返回与接收器相关联的所有触摸对象。- (nullable NSSet <UITouch *> *)allTouches; // 返回值:返回属于一个给定视图的触摸对象,使用于表示由接收器所表示的事件。- (nullable NSSet <UITouch *> *)touchesForView:(UIView *)view; //返回值:返回属于一个给定窗口的接收器的事件响应的触摸对象。- (nullable NSSet <UITouch *> *)touchesForWindow:(UIWindow *)window;//返回值:返回触摸对象被传递到特殊手势识别- (nullable NSSet <UITouch *> *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture 

终于说完了UITouch 和 UIEvent ,其实还有很多细节,这里就不开展说了,回到touches的四个方法上来说事情:

  1. touches 里包含了一个或者多UITouch对象,也即一个或者多个手指同时触摸view,因而touches.count就是触摸的点数,是1就是单点触摸,大于1就是多点触摸。
  2. 一次触摸过程中,只会产生一个事件对象,4个触摸方法都是同一个event参数
    假如两根手指同时触摸一个view,那就是一个事件,view只会调使用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象
  3. 假如这两根手指一前一后分开触摸同一个view,那就是两个事件,view会分别调使用2次touchesBegan:withEvent:方法,并且每次调使用时的touches参数中只包含一个UITouch对象

响应者链传递过程示用意:

响应者链视图.png

以上四个方法就是view使用来响应事件而后做出解决的过程。

注意事项:
  • 在view上用这四个方法,必需自己设置view而后重写父类方法。
  • 在viewController里,重写这四个方法,是响应viewController的事件,viewController上的子视图需要响应和解决事件需要自己设置,不要搞混淆了,以为在viewController里面写,就是重写了view的这四个方法。
view重写touches方法,解决事件的缺陷
1. 必需自己设置View2. 因为是View内部的touches方法中监听触摸事件,因而默认情况下无法让其余外界对象监听View的触摸事件3. 不容易区分使用户的具体手势行为。
用技巧:

如何做到一个事件多个对象解决?
由于系统默认做法是把事件上抛给父控件,所以可以通过重写自己的touches方法和父控件的touches方法来达到一个事件多个对象解决的目的

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{     // 1.自己先解决事件...    NSLog(@"do somthing...");   // 2.再调使用系统的默认做法,再把事件交给上一个响应者解决   [super touchesBegan:touches withEvent:event]; }

本文借鉴了少量前辈的文章,假如有不对的地方请指正,欢迎大家一起交流学习。

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » iOS UITouch事件解决-原理篇

发表回复