zoukankan      html  css  js  c++  java
  • 深入浅出iOS事件机制

    深入浅出iOS事件机制

    2015年 04月 12日

    本文章将讲解有关iOS事件的传递机制,如有错误或者不同的见解,欢迎留言指出。转载自:http://zhoon.github.io/ios/2015/04/12/ios-event.html

    iOS的事件有好几种:Touch Events(触摸事件)、Motion Events(运动事件,比如重力感应和摇一摇等)、Remote Events(远程事件,比如用耳机上得按键来控制手机),其中最常用的应该就是Touch Events了,基本存在于每个app的每个地方,今天我们主要就讲讲它,至于其他两个事件有兴趣的可以自行查阅资料。

    在网页上当我们讲到事件,我们会讲到事件响应链,我们会讲到事件的响应者和事件的传递方式(冒泡),那么在app上,其实也离不开这几个问题,今天我们也重这几个方面来介绍iOS的事件机制: 1、响应链是什么时候怎样构建的? 2、事件第一个响应者是怎么确定的? 3、事件第一个响应者确定后,系统是怎样传递事件的?

    响应链的构建

    无论是哪种事件,其传递和响应都与响应链息息相关,那么响应链到底是一个什么样的东西呢? 在UIKit中有一个类:UIResponder,我们可以看看头文件的几个属性和方法:

    UIResponder是所有可以响应事件的类的基类(从名字应该就可以看出来了),其中包括最常见的UIView和UIViewController甚至是UIApplication,所以我们的UIView和UIViewController都是作为响应事件的载体。

    那么响应链跟这个UIResponder有什么关系呢?事实事件响应链的形成和事件的响应和传递,UIResponder都帮我们做了很多事。我们的app中,所有的视图都是按照一定的结构组织起来的,即树状层次结构,每个view都有自己的superView,包括controller的topmost view(controller的self.view)。当一个view被add到superView上的时候,他的nextResponder属性就会被指向它的superView,当controller被初始化的时候,self.view(topmost view)的nextResponder会被指向所在的controller,而controller的nextResponder会被指向self.view的superView,这样,整个app就通过nextResponder串成了一条链,也就是我们所说的响应链。所以响应链就是一条虚拟的链,并没有一个对象来专门存储这样的一条链,而是通过UIResponder的属性串连起来的。如下图:

    Hit-Testing View

    文章开头说到有iOS三种event类型,事件传递中UIWindow会根据不同的event,用不同的方式寻找initial object,initial object决定于当前的事件类型。比如Touch Event,UIWindow会首先试着把事件传递给事件发生的那个view,就是下文要说的hit-testview。对于Motion和Remote Event,UIWindow会把例如震动或者远程控制的事件传递给当前的firstResponder,有关firstResponder的相关信息请看这里。下面主要讲Touch Event的hit-testview。

    有了事件响应链,接下来的事情就是寻找响应事件的具体响应者了,我们称着为:Hit-Testing View,寻找这个View的过程我们称着为Hit-Test。

    那么什么是Hit-Test呢,我们可以把它理解为一个探测器,通过这个探测器我们可以找到并判断手指是否点击在某个视图上面,换句话说就是通过Hit-Test可以找到手指点击到的处于屏幕最前面的那个UIView。

    在解释Hit-Test是怎么工作之前,先来看看它是什么时候被调用的。前面说Hit-Test是一个探测器,那么在代码里面其实就是一个函数,UIView有如下两个方法:

    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event; 
    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;

    每当手指接触屏幕,UIApplication接收到手指的事件之后,就会去调用UIWindow的hitTest:withEvent:,看看当前点击的点是不是在window内,如果是则继续依次调用subView的hitTest:withEvent:方法,直到找到最后需要的view。调用结束并且hit-test view确定之后,这个view和view上面依附的手势,都会和一个UITouch的对象关联起来,这个UITouch会作为事件传递的参数之一,我们可以看到UITouch头文件里面有一个view和gestureRecognizers的属性,就是hitTest view和它的手势。

    现在知道Hit-Test是什么时候调用了,那么接下来看看它是怎么工作的。Hit-Test是采用递归的方法从view层级的根节点开始遍历,看看下面这张图:

    UIWindow有一个MianVIew,MainView里面有三个subView:view A、view B、view C,他们各自有两个subView,他们层级关系是:view A在最下面,view B中间,view C最上(也就是addSubview的顺序,越晚add进去越在上面),其中view A和view B有一部分重叠。如果手指在view B.1和view A.2重叠的上面点击,按照上面说的递归方式,顺序如下图所示:

    递归是向界面的根节点UIWindow发送hitTest:withEvent:消息开始的,从这个消息返回的是一个UIView,也就是手指当前位置最前面的那个 hittest view。 当向UIWindow发送hitTest:withEvent:消息时,hitTest:withEvent:里面所做的事,就是判断当前的点击位置是否在window里面,如果在则遍历window的subview然后依次对subview发送hitTest:withEvent:消息(注意这里给subview发送消息是根据当前subview的index顺序,index越大就越先被访问)。如果当前的point没有在view上面,那么这个view的subview也就不会被遍历了。当事件遍历到了view B.1,发现point在view B.1里面,并且view B.1没有subview,那么他就是我们要找的hittest view了,找到之后就会一路返回直到根节点,而view B之后的view A也不会被遍历了。

    一图胜千言:

    注意hitTest里面是有判断当前的view是否支持点击事件,比如userInteractionEnabled、hidden、alpha等属性,都会影响一个view是否可以相应事件,如果不响应则直接返回nil。 我们留意到还有一个pointInside:withEvent:方法,这个方法跟hittest:withEvent:一样都是UIView的一个方法,通过他开判断point是否在view的frame范围内。如果这些条件都满足了,那么遍历就可以继续往下走了,代码表现大概如下:

    Hit-Test的应用

    一、扩大view的点击区域

    一个按钮尺寸是10pt*10pt,如果要扩大按钮的点击区域(按钮四周之外的10pt也可以响应按钮的事件),可以怎么做呢?或许重写hittest:withEvent:是个好办法,hitest就是返回可以响应事件的view,如果我们在button的子类里面重写它,在方法里面判断如果point在button的frame之外的10pt内,就返回button自己。

    二、将事件传递给兄弟view

    如上面第一个图,如果需要是需要view A响应事件而不是B(即使点在重叠的部分),什么都不做的话,当点击在重叠的时候,A是不能响应事件的,除非B的userInteractionEnabled为NO并且者B没有任何事件的响应函数。这个时候通过重写B的hittest可以解决这个问题,在B的hittest里面直接返回nil就行了。

    三、将事件传递给subview

    如下图,蓝色的scrollView设置pagingEnabled使得image停止滚动后都会固定在居中的位置,如果在scrollView的左边或者右边活动,发现scrollView是无法滚动的,原因就是hittest里面没有满足pointInSide这个条件,scrollView的bound只有蓝色的区域。这个时候重写UIView的hittest:withEvent:,然后返回scrollView即可解决问题。

    事件的传递

    有了响应链,并且找到了第一个响应事件的对象,接下来就是把事件发送个这个响应者了。 UIApplication中有个sendEvent:的方法,在UIWindow中同样也可以发现一个同样的方法。UIApplication是通过这个方法把事件发送给UIWindow,然后UIWindow通过同样的接口,把事件发送给hit-testview。这个我们可以从Time Profiler里面得到证实:

    当我点击了WRBuyBookButton之后,UIWindow会通过一个私有方法,在里面会去调用按钮的touchesBegan和touchesEnded方法,touchesBegan里面有设置按钮的高亮等之类的动作,这样就实现了事件的传递。而事件的响应,也就是按钮上绑定的action,是在touchEnded里面通过调用UIApplication的sendAction:to:from:forEvent:方法来实现的,至于这个方法里面是怎么去响应action,就只能猜测了(可能是通过oc底层消息机制的相关接口 objc_msgSend 来发送消息实现的,可以参考message.h文件)。如果第一响应者没有响应这个事件,那么就会根据响应链,把事件冒泡传递给nextResponder来响应。

    注意这里是怎么把事件传递给nextResponder的呢?拿touch事件来说,UIResponder里面touch四个阶段的方法里面,实际上是什么事都没有做的,UIView继承了它进行重写,重写的内容也是没有什么东西,就是把事件传递给nextResponder,比如:[self.nextResponder touchesBegan:touches withEvent:event]。所以当一个view或者controller里面没有重写touch事件,那么这个事件就会一直传递下去,直到UIApplication,这也就是事件往上冒泡的原理。如果view重写了touch方法,我们一般会看到的效果是,这个view响应了事件之后,事件就被截断了(就像JavaScript里面调用e.stopPropagation()),它的nextResponder不会收到这个事件,即使重写了nextResponder的touch方法。这个时候如果想事件继续传递下去,可以调用[super touchesBegan:touches withEvent:event],不建议直接调[self.nextResponder touchesBegan:touches withEvent:event]。

    关于UIScrollView的事件

    先说一个现象,我们平时加到UIScrollView(或者UITableView和UICollection)上面的UIButton,即使有设置highLighted的样式,点击的时候却发现这个样式老是不出来,但是按钮的事件明明可以响应的,很诡异。

    后来才知道,UIScrollView因为要滚动,所以对事件做了特殊的处理: 当UIScrollView接收到事件之后,会暂时劫持当前的事件300毫秒,如果300毫秒之后手指还没有滚动,则认为你放弃滚动,放弃对事件的劫持并往下传递,但是从Time Profiler看到此时按钮并不是调用自身的touch方法,而是调用自身绑定的手势的touch事件,由于按钮的highLighted样式是写在按钮的touch方法上的,所以这个这个时候就看不到高亮了。但是长按按钮缺可以让按钮有高亮的状态,这个就不太清楚为什么了,因为从Time Profiler里面看按钮的touchesBegan好像还是没有被调。 如果300毫秒之内手指滚动了,则响应滚动的事件,事件就不会继续传给subView了,也就是不会继续调用按钮上手势的touch方法了。

    可以通过UIScrollView的一个属性来解决这个问题:delaysContentTouches,意思是是否需要延迟处理事件的传递,默认是NO。把delaysContentTouches设置为YES之后,一切看起来挺好的,按钮终于有高亮样式了哈哈哈,但是发现另一个问题:如果手指点击在按钮上面并滚动UIScrollView,发现怎么也滚动不了。原因是当手指点击UIScrollView并在滚动之前,如果subView接收并且可以响应事件(delaysContentTouches设置为YES),则事件响应链会在subView响应事件之后就截断,即UIScrollView本身不会响应到此事件,不会发生滚动。可以设置canCancelContentTouches为YES来让UIScrollView可以滚动,与之类似的还有一个touchesShouldCancelInContentView:接口,可以根据参数view来更方便的判断是否需要cancel,如果有需要可以在UIScrollView的子类里面重写这个接口。

    这一块里面的具体实现原理我们都不知道,水太深了,只能通过Time Profiler来看到一些大概的实现,我们也没必要去深究,大方向理解就好了。真的有兴趣的同学也可以去研究研究,期待你的分享。

    参考资料:

    1、http://smnh.me/hit-testing-in-ios/

    2、http://southpeak.github.io/blog/2015/03/07/uiresponder/

    3、https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIScrollView_Class/index.html#//apple_ref/doc/uid/TP40006922

    4、https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/event_delivery_responder_chain/event_delivery_responder_chain.html

    5、https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/GestureRecognizer_basics/GestureRecognizer_basics.html#//apple_ref/doc/uid/TP40009541-CH2-SW2

    6、https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/GestureRecognizer_basics/GestureRecognizer_basics.html#//apple_ref/doc/uid/TP40009541-CH2-SW2

     
  • 相关阅读:
    arcgis api 3.x for js 入门开发系列八聚合效果(附源码下载)
    arcgis api 3.x for js 入门开发系列七图层控制(附源码下载)
    arcgis api 3.x for js 入门开发系列六地图分屏对比(附源码下载)
    arcgis api 3.x for js 入门开发系列五地图态势标绘(附源码下载)
    arcgis api 3.x for js 入门开发系列四地图查询(附源码下载)
    Java里面获取当前服务器的IP地址
    Flutter at Google I/O 2018
    Modbus RTU 协议使用汇总
    plsql 创建表空间、用户、赋予权限
    Oracle:ODP.NET Managed 小试牛刀
  • 原文地址:https://www.cnblogs.com/HypeCheng/p/4775264.html
Copyright © 2011-2022 走看看