iOS之事件的传递和响应机制

事件也有自己的生命周期,按照时间的顺序来看,事件的声明是下面这样子的:

事件的产生和传递(事件如何从父控件传递到子控件并寻找最合适的view,寻找最合适的view的底层实现,拦截事件的处理)---->找到最合适的view后事件的处理(touch方法的重写,也就是事件的响应)。

重点:1.如何找到最合适的view。2.寻找最合适的view的底层实现。

iOS中的触摸事件

想了解iOS中的触摸事件就需要学习一个重要的知识点:响应者对象(UIResponder)

iOS中不是所有的控件都能处理事件,只有继承UIResponder的对象才可以处理事件。比如UIApplication,UIViewController,UIView

那么为什么继承自UIResponder的对象才能处理事件呢?因为UIResponder提供了以下几个方法来接收处理事件。

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

iOS中的触摸事件处理

// UIView是UIResponder的子类,可以覆盖下列4个方法处理不同的触摸事件
// 一根或者多根手指开始触摸view,系统会自动调用view的下面方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
// 一根或者多根手指在view上移动,系统会自动调用view的下面方法(随着手指的移动,会持续调用该方法)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
// 一根或者多根手指离开view,系统会自动调用view的下面方法
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
// 触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,系统会自动调用view的下面方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
// 提示:touches中存放的都是UITouch对象

以上四个方法是系统自动调用的,可以通过重写这几个方法来处理一些事件。
1.如果两根手指同时触摸一个view,那么系统只会调用一次touchesBegan方法,touches参数中装着两个UITouch对象。
2.如果一前一后分别触摸一个view,则会调用两次touchesBegan方法,touches参数中分别装有一个UITouch对象。
3.重写以上四个方法,如果是重写UIView的触摸事件,必须要自定义UIView并继承自UIView
4.如果重写UIViewController的触摸事件,直接在控制器内重写这四个方法即可。

代码示例:
RespondsView.h

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface RespondsView : UIView

@end

NS_ASSUME_NONNULL_END

RespondsView.m

#import "RespondsView.h"

@implementation RespondsView

// 开始触摸时就会调用一次这个方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    NSLog(@"摸我干啥!");
}
// 手指移动就会调用这个方法
// 这个方法调用非常频繁
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
    NSLog(@"哎呀,不要拽人家!");
}
// 手指离开屏幕时就会调用一次这个方法
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
    NSLog(@"手放开还能继续玩耍!");
}

@end

ViewController.m

#import "ViewController.h"
#import "RespondsView.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    RespondsView *touchView = [[RespondsView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    touchView.backgroundColor = [UIColor redColor];
    [self.view addSubview:touchView];

}

UIView的拖拽

实现UIView的拖拽就需要重写touchesMoved方法,此时则需要用到参数touches,我们看一下UITouch的各种属性和方法。

NS_CLASS_AVAILABLE_IOS(2_0) @interface UITouch : NSObject

触摸产生时所处的窗口
@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;

- (CGPoint)locationInView:(UIView *)view;
//返回值表示触摸在view上的位置
//这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0))
//调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置

- (CGPoint)previousLocationInView:(UIView *)view;
//记录了前一个触摸点的位置

控件拖拽的实现代码

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
    NSLog(@"哎呀,不要拽人家!");
    UITouch *touch = [touches anyObject];
    
    CGPoint currentPoint = [touch locationInView:self];
    // 获取上一个点
    CGPoint prePoint = [touch previousLocationInView:self];
    CGFloat offsetX = currentPoint.x - prePoint.x;
    CGFloat offsetY = currentPoint.y - prePoint.y;
    
    NSLog(@"FlyElephant----当前位置:%@---之前的位置:%@",NSStringFromCGPoint(currentPoint),NSStringFromCGPoint(prePoint));
    self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
}

UITouch的作用

1.保存着跟手指相关的信息,比如触摸的时间,位置,阶段。
2.当手指移动时,系统会更新同一个UITouch对象,使之能一直保存该手指所在的位置。
3.手指离开屏幕时,系统会销毁相应的UITouch对象。

iOS中事件的产生和传递

1.事件的产生

(1)发生触摸事件时,系统会将事件加入到一个由UIApplication管理的队列中,为什么是队列而不是栈,因为队列是FIFO,先产生的事件先处理才对。
(2)UIApplication会从队列中取出最前面的事件,然后分发下去,通常会先分发给事件的主窗口(keyWindow)
(3)主窗口会在视图层次中找出一个最适合的视图来处理触摸事件,这也是整个事件处理过程的第一步。找到合适的视图后,就会调用视图的touches方法来做具体的事件处理。

2.事件的处理

(1)事件的传递是从父控件传递到子控件
(2)UIApplication->keyWindow->寻找最合适的view

ps.如果父控件无法处理事件,子控件是不可能处理的。
pps.view的透明度小于等于0.01时,view就将无法处理事件。

应用如何找到最合适的视图来处理事件

1.首先判断主窗口是否可以处理事件
2.判断触摸点是否在自己身上
3.子控件在数组中从后往前遍历重复前两个步骤
4.遍历到的view,比如叫做fitview,然后再遍历fitView的子控件,直到没有更合适的view
5.如果没有更合适的子控件,那么就认为自己是最符合的。

事件传递示意图

如何打破事件的传递链呢?可以设置userInteractionEnabledno,比如设置蓝色的userInteractionEnabledno,那么蓝色以及黄色的点击事件只能由橙色来处理了,所以不管视图能不能处理事件,只要点击了视图,就会产生事件,关键是最终由谁来处理!如果蓝色不能处理事件,即使点击了蓝色,产生的事件也不会由蓝色来处理!

如何寻找最合适的View

1.主窗口收到点击事件,首先判断自己能否成为点击事件的处理者,如果能再判断触摸点是否在自己身上。
2.如果触摸点在自己身上,则会从后往前遍历子控件。
3.遍历到每一个子控件后,则会重复前两个步骤(判断自己是否能处理事件,触摸点是否在自己上面)
4.如此循环,知道找不到最合适的子控件,则自己就是最合适的View

找到最合适的view之后就会调用touches方法来进行相应处理,找不到合适的view就不会调用touches方法。

找到合适的View底层方法。

两个重要的方法:

hitTest:withEvent:方法
pointInside方法
什么时候调用?

hitTest:withEvent:方法:只要事件传递到一个控件,这个控件就会调用自己的hitTest:withEvent:方法。

作用

找到最合适的view

ps.不管这个控件是否能处理事件,也不管触摸点是否在这个控件上,都会调用它的hitTest:withEvent:方法。

事件的拦截

1.因为hitTest:withEvent:方法可以返回最合适的view,所以可以通过重写hitTest:withEvent:方法来返回指定的view作为最合适的view
2.不管点击那里,处理事件的view都是hitTest:withEvent:方法返回的view
3.通过重写hitTest:withEvent:方法可以指定处理事件的view,想让谁处理就可以让谁处理。

如果hitTest:withEvent:方法返回nil,那么调用该方法的控件和子控件都不是最合适的view,那么最合适的控件就是它的父控件。

所以事件传递的顺序是这样的:产生触摸事件->UIApplication事件队列->[UIWindow hitTest:withEvent:]->返回更合适的view->[子控件 hitTest:withEvent:]->返回最合适的view

技巧:想让谁成为最合适的view就重写谁自己的父控件hitTest:withEvent:方法返回指定的子控件,或者重写自己的hitTest:withEvent:方法 return self。但是,建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view

hitTest:withEvent:底层实现

#import "WSWindow.h"
@implementation WSWindow
// 什么时候调用:只要事件一传递给一个控件,那么这个控件就会调用自己的这个方法
// 作用:寻找并返回最合适的view
// UIApplication -> [UIWindow hitTest:withEvent:]寻找最合适的view告诉系统
// point:当前手指触摸的点
// point:是方法调用者坐标系上的点
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    // 1.判断下窗口能否接收事件
     if (self.userInteractionEnabled == NO || self.hidden == YES ||  self.alpha <= 0.01) return nil; 
    // 2.判断下点在不在窗口上 
    // 不在窗口上 
    if ([self pointInside:point withEvent:event] == NO) return nil; 
    // 3.从后往前遍历子控件数组 
    int count = (int)self.subviews.count; 
    for (int i = count - 1; i >= 0; i--)     { 
    // 获取子控件
    UIView *childView = self.subviews[i]; 
    // 坐标系的转换,把窗口上的点转换为子控件上的点 
    // 把自己控件上的点转换成子控件上的点 
    CGPoint childP = [self convertPoint:point toView:childView]; 
    UIView *fitView = [childView hitTest:childP withEvent:event]; 
    if (fitView) {
    // 如果能找到最合适的view 
    return fitView; 
    }
    } 
    // 4.没有找到更合适的view,也就是没有比自己更合适的view 
    return self;
    }
    // 作用:判断下传入过来的点在不在方法调用者的坐标系上
    // point:是方法调用者坐标系上的点
    //- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
    //{
    // return NO;
    //}
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ 
    NSLog(@"%s",__func__);
    }
    @end

hit:withEvent:方法底层会调用pointInside:withEvent:方法判断点在不在方法调用者的坐标系上。如果返回YES,代表点在方法调用者的坐标系上;返回NO代表点不在方法调用者的坐标系上,那么方法调用者也就不能处理事件。

事件的响应

(1)事件处理的过程

1.用户点击屏幕产生一个触摸事件,经过一系列的传递找到最合适的view来处理这个事件
2.找到最合适的视图控件后就会调用控件的touches来具体处理事件,touchesBegan…touchesMoved…touchedEnded…
3.这些touches默认是不处理事件,而是顺着响应者链条向上传递事件,将事件交给上一个响应者进行处理。

(2)响应者链条示意图

在我们开发的程序中视图是有层级的,我们经常会在一个视图控件上添加另一个视图控件,那么我们点击上面这个视图控件时,是上面的来处理事件还是下面的来处理事件呢?这种先后关系就形成一个链条,叫做"响应者链条"。

响应者链条

响应者对象:能处理事件的对象,也就是继承自UIResponder的对象。
作用:清晰的看到每个响应者之间的关系。

响应者链条事件传递过程

1>如果当前的view是控制器的view,那么控制器就是上一个响应者。如果不是控制器的view,那么其父控件就是上一个响应者。
2>在视图层次的最顶层视图也不能处理事件的话,那么它的事件将会交给window来处理。
3>如果window也不处理的话,将交给UIApplication来处理事件。
4>如果UIApplication也不处理事件,事件就会被废弃。

事件处理的流程总结

1.触摸屏产生触摸事件之后,会将事件放进一个由UIApplication管理的队列当中。
2.UIApplication会将最先进来的事件交给window去处理。
3.主窗口会在视图层次中找到一个最合适的view去处理事件。
4.最合适的view会调用自己的touches方法去处理事件。
5.默认情况会将事件向上传递,交给父控件。

#import "WSView.h"
@implementation WSView 
//只要点击控件,就会调用touchBegin,如果没有重写这个方法,自己处理不了触摸事件
// 上一个响应者可能是父控件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ 
// 默认会把事件传递给上一个响应者,上一个响应者是父控件,交给父控件处理
[super touchesBegan:touches withEvent:event]; 
// 注意不是调用父控件的touches方法,而是调用父类的touches方法
// super是父类 superview是父控件 
}
@end

注意:最合适的view将会先自己处理事件,如果不能处理才会交给上一个响应者处理。上级视图仍然无法处理则一直向上传递。在传递的过程中如果某个控件实现了touches方法,则事件由这个控件处理。

如何实现一个事件多个对象处理?

重写自己的和父控件的touches方法

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

事件的传递是由上到下(父控件->子控件),事件的响应是由下到上(子控件->父控件)。

原文地址:https://www.jianshu.com/p/2e074db792ba

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容