最好的输入方式:iOS中的触摸事件

乔帮主在发布会上提到,用户的手才是最好的输入设备,的确,iPhone之后,非触屏手机再已难觅。触摸是最基本的用户输入事件,理解iOS特有的触摸事件响应机制,能够良好管理程序中触摸响应方法,避免冲突的发生。

iOS中的事件

iOS中的事件主要分为三类:

  1. UIControl Actions: 使用target/action注册的SEL。
  2. User Events: 用户与应用之间的交互:触摸,输入文字,摇晃,远程控制等。
  3. System Events: 应用启动,切前后台,低内存等。
    cocoa和cocoa touch的程序启动后,,会首先初始化一些基本资源:在主线程创建一个main event loop;初始化主UIWindow
    应用启动过程-w500

    main event loop本质上是一个NSRunLoop,与其他辅助线程的run loop不同,其是自创建后自动开始运行的。主消息循环最大的特点是:它在创建时就与负责捕获用户事件的系统底层建立了连接,所以它的input source可以收到系统传递过来的用户事件。UIApplication对象会将当前要处理的用户事件封装成UIEvent,发送给UIWindow,在由UIWindow转发给对应的响应者。
    iOS响应用户事件
    iOS响应用户事件

    UIEvent表示用户与iOS产生交互的事件,UIWindow将触摸事件发送给hitTest View,其他事件发送给first responder,若它们不能处理该事件,事件在响应链向上传递,找到最终的响应者或丢弃。
    本文主要介绍触摸事件的响应机制。

iOS中能够捕获触摸事件的类

iOS程序中,有三种类可以接受用户的触摸事件并响应,分别是:UIControl, UIReponder, UIGestureRecognizer,这三个类在参与触摸响应机制的时机不同,在实际使用时要加以注意。

iOS中的触摸事件

iOS中使用UItouch来表示用户的一根手指在屏幕上的触摸行为。当用户触摸屏幕时,硬件会捕捉到触摸行为,将触摸点的半径、力度和坐标等发送给iOS,经过UIKit封装后,得到UITouch对象。通过UITouch对象,我们可以获得其关联的视图(hitTest View),在视图中的坐标,生命周期的当前阶段,点击数等信息。。一次用户点击多次的事件,其只包含一个UITouch
触摸类型的UIEvent包含至少一个UITouch,也就是用户在屏幕上的一次手势操作的手指运动,其会持有此次事件相关联的UITouches序列。,即在一次手势操作中,其中一个手指中途离开屏幕,它所对应的UITouch依然存在于该事件中。响应者会在touchesBegan:withEvent:等方法中获取UITouch对应的UIEvent
UITouches序列在用户第一根手指触摸屏幕时开始,最后一根手指离开时结束,当手指状态变化时,iOS会将序列中的UITouch对象发送给UIEvent对象。

一个UItouches序列和UItouch的不同生命周期
一个UItouches序列和UItouch的不同生命周期

iOS的触摸事件响应机制

当用户触摸屏幕时,对应的触摸事件会加入到UIApplication事件队列中,当下一个RunLoop来临时,UIApplication会将出列最前端的事件,发送给当前的UIWindow(key window)。
UIWindow会调用hitTest:withEvent:方法,开始hit-testing流程寻找包含触摸点的视图。该流程会返回包含触摸点的层级最低的视图。
每当用户触摸屏幕时,UIKit都会执行hit-testing,之后再从hitTest视图开始寻找事件的响应者。当hitTest视图决定后,它就关联了对应的触摸事件,会持续收到触摸事件生命周期的方法,(touchBegan, touchMove, touchCancel/touchEnd),即使是触摸点已经在touchMove阶段移出了hitTest视图,它依然能够收到后续的消息。

Note: A touch object is associated with its hit-test view for its lifetime, even if the touch later moves outside the view.

iOS的触摸事件响应机制

hit-testing流程

iOS中hit-testing使用逆前序的深度遍历算法来确定用户点按的最低层级(最靠近用户)的视图,该hitTest视图是触摸事件的响应链头结点。
逆前序的深度遍历算法:根节点-->右子树-->左子树。
当收到触摸事件后,UIApplication在当前视图层级中,从key window开始(最顶级),从上往下遍历子视图调用hitTest:withEvent:,若找到hitTest视图则停止遍历并返回。
当视图收到hitTest:withEvent:方法后,通过下列条件判断是否在该视图执行hit-testing。

  1. pointInside:withEvent:方法返回YES。pointInside:withEvent:方法用来判断触摸点是否在当前视图内。
  2. hidden == NO。
  3. userInteractionEnabled == YES。
  4. alpha >= 0.01。若view的content绘制为透明的,则不受影响。

需要注意的是,当clipsToBounds == NO时,视图的子视图可能会超出其bounds,这种情况如果触摸点在子视图超出父视图的范围,那么hit-tesing不会再此视图树上执行。

hit-testing

如图,当用户触摸viewB.1时,UIApplication对象收到触摸事件,从key window开始执行hit-testing,首先访问viewC,由于pointInside:withEvent:方法返回NO,取消执行并访问viewB,满足执行,则从右往左开始访问其子视图(视图层级从下往上),找到viewB.1,它没有子视图,则返回自己。最终UIWindow对象将viewB.1作为hitTest视图返回给UIApplication对象。
hit-testing流程图

可以看到,当某一视图收到hitTest:withEvent:方法后,它会向所有子视图发送hitTest:withEvent:方法,若它的没有子视图或所有子视图返回nil,那么就返回自己,所有hit-testing流程最终一定会找到一个对象UIView/UIWindow去接收触摸事件。
以下是hitTest:withEvent:可能的实现。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    if ([self pointInside:point withEvent:event]) {
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            CGPoint convertedPoint = [subview convertPoint:point fromView:self];
            UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
        return self;
    }
    return nil;
}

responder chain

responder chain是UIResponder对象组成的链形结构,它以first responder为头结点,UIApplication对象为尾节点,事件从头开始在响应链中向上传递。
UIResponder用来设计处理事件,UIApplication, UIViewController, UIView都是其子类,只要它们实现了UIResponder中的钩子方法,就可以响应对应的事件。

UIResponder的继承关系
UIResponder的继承关系

其中first responder用来第一个接触事件,可以使用becomeFirstResponder来设置它,主要要在视图层级已经完全建立之后再设置。

If you try to assign the first responder in viewWillAppear:, your object graph is not yet established, so the becomeFirstResponder method returns NO

默认情况下,fist responder是当前UIWindow中最有可能响应事件的UIView,这由UIkit决定。
iOS中大部分的事件都依赖响应链来找到最终的响应者,在UIResponder的头文件中可以看到,Touch events,Motion events,Remote events,UIControl Action,Text editing,press events等事件都可以在响应链中传递。

寻找响应对象

UIApplication在处理的事件时,触摸事件会交给hitTest view开始的响应链处理,其他的动作事件,远程事件,系统事件等,会交给first responder开始的响应链处理。
UIKit会将用户事件发送给理论上最合适的对象。所以当程序中的响应者要经过很长的查找路径时,这时就要考虑是否实现是否设计合理了。

UIKit first sends the event to the object that is best suited to handle the event. For touch events, that object is the hit-test view, and for other events, that object is the first responder

对于触摸事件,hit-test视图获得了最先接受触摸对象的机会,但如果它不能处理对应的触摸事件,那么UIKit会沿着以hit-test开头的响应链寻找能够最终的响应者。


The responder chain on iOS
The responder chain on iOS

当找到响应者或已经到链尾(UIApplication)仍不能处理,UIKit会停止查找,对于后者,对应的事件会被丢弃。

除了UIResponder对象,UIGestureRecognizerUIControl也可以响应触摸事件,但它们参与触摸事件响应的方式不同。

  1. UIGestureRecognizer在响应链中的位置取决于依附的视图。
  2. UIControl参与响应的方式决定于其关联的target。
    UIGestureRecognizer要先于视图收到触摸事件,但需要注意的是,若该视图也可以响应触摸事件(实现了UITouch生命周期函数),那么手势对象并不会阻碍视图的响应,双方是同时响应的,只不过存在先后顺序。
    UIGestureRecognizer与UIView的接触事件的次序
    UIGestureRecognizer与UIView的接触事件的次序

响应触摸事件

当确定了响应链后,UIWindow会向hitTest View发送以下方法:

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

这是UIResponder用于响应触摸事件的方法,这些钩子方法的默认实现是向nextResponder转发方法。
当触摸事件在响应链上传递时,判断当前UIResponder能否响应的条件是:其是否实现了touchesBegan方法。
在这些UITouches序列的生命周期方法中,我们可以获取对应UIEventUITouch,利用它们所提供的信息,进一步决定如何响应用户的触摸事件。

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

推荐阅读更多精彩内容