iOS事件的分发机制和响应者链(Swift)

当我们在设计自己的APP时,可能会想动态的响应事件.例如:屏幕上许多对象都能够发生触摸,我们必须决定哪一个对象来响应给定的事件并且知道对象是如何接受事件的。当用户事件产生的时候,UIKit会创建一个事件对象(event
object),该对象包含了事件整个过程中所拥有的信息。并且该事件对象将处于APP活跃事件列队中。对于触摸事件,对象将包含一系列UIEvent对象。对于运动事件,取决于你使用的框架和你感兴趣的运动事件的类型。

事件沿着具体的路径进行传递,一直到发现对象能够处理该事件,首先,UIApplication单例对象会将该事件将被传递给window对象,window对象会使用hitTest:withEvent:方法来递归的寻找操作初始点所在的view,该视图就称之为hit-test
view,寻找hit-test view过程称为hit-testing.

分发机制(Hit-Testing)

iOS使用hit-testing来找到触摸点所在的视图。hit-testing将会检测是否触摸事件在相关视图的显示区域之内。如果在,将递归检测当前视图的所有子视图。视图层级中最底层的view如果包含触摸点将成为hit-test
view.在iOS确认了hit-test
view之后,将传递触摸世界给对应的视图进行处理。

为了解释上面,下面看一下官方例子:用户触摸视图view E,iOS将有序查找子视图,找到hit-test
view

1:由图可知,触摸位于视图A的区域之内,所以会对B,C进行检测

2:如果触摸事件不在视图B区域中,但是位于视图区域C,将对视图C的子视图D,E进行检测

3:如果触摸事件不在视图D区域中,但是在视图区域E中,又因为视图E是视图层级结构中最底层的视图,所以视图E将成为hit-test
view。

Hit-testing returns the subview that was touched

hitTest:withEvent:方法会根据给定触摸点(CGPoint)和事件对象(UIEvent)两个参数,返回点击的视图(hit
test view)。该方法首先会调用pointInside:withEvent:方法,如果hitTest:withEvent:方法中所传递的参数point点是位于视图之内,pointInside:withEvent:方法将返回true,然后,在返回true的所有子视图上将递归调用hitTest:withEvent:方法。

如果hitTest:withEvent:方法中传递的point点不在视图显示区域之内,第一次调用pointInside:withEvent:方法将返回false,那么该点将被忽略,hitTest:withEvent:方法将返回nil。如果一个子视图返回false,那么整个视图层级都将被忽略,因为触摸并不在子视图当中,所以子视图的子视图同样也不会发生触摸事件。

简单一点理解: hitTest:withEvent:方法的处理流程如下:调用当前view的pointInside:withEvent:方法来判定触摸点是否在当前view内部,如果返回false,则hitTest:withEvent:返回nil;如果返回true,则向当前view内的subViews发送hitTest:withEvent:消息,所有subView的遍历顺序是从数组的末尾向前遍历,直到有subView返回非空对象或遍历完成。如果有subView返回非空对象,hitTest方法会返回这个对象,如果每个subView返回都是nil,则返回自己。

hitTest:withEvent:方法忽略隐藏(hidden=YES)的视图,禁止用户操作(userInteractionEnabled=YES)的视图,以及alpha级别小于0.01(alpha<0.01)的视图。如果一个子视图的区域超过父视图的bound区域(父视图的clipsToBounds属性为NO,这样超过父视图bound区域的子视图内容也会显示),那么正常情况下对子视图在父视图之外区域的触摸操作不会被识别,因为父视图的pointInside:withEvent:方法会返回NO,这样就不会继续向下遍历子视图了。当然,也可以重写pointInside:withEvent:方法来处理这种情况。

hit-test view将首先处理触摸事件,如果hit-test view并不能够处理事件,那么该事件将由视图的响应者链进行查找,一直到系统找到能够处理事件的对象。

响应者链由响应者组成

许多类型的事件都依赖于响应者链进行事件的传递。响应者链关联着一系列的响应者对象,由第一个响应者对象开始一直到application对象结束,如果第一个响应者不能够处理事件,事件将会被传递到响应者链中的下一个响应者对象。

一个响应者对象是能够处理和响应事件的对象,UIResponder类是所有响应者对象的基类,它不仅仅定义了事件处理的接口而且还有共有的响应者行为。UIApplication,
UIViewController, UIView等类的实例都是响应者对象,这意味着所有的视图(all views)和大多数的关键视图控制器对象都是响应者。但是要注意核心动画中的层(layer)不是响应者。

第一响应者首先接收事件。代表性的就是:视图是第一响应者对象。一个对象要成为第一响应者需要做两件事:

重写canBecomeFirstResponder方法,返回true。

接收成为第一响应者信息,及becomeFirstResponder方法,如果有必要,对象能够自己给自己发送信息。

注意:在对象被赋值成为第一响应者之前,确保APP已经建立的对象图形(object
graph)。例如:我们可以在viewDidAppear:方法中调用becomeFirstResponder方法,但是,如果我们尝试viewWillAppear:中赋值第一响应者,我们的对象图形可能还没有建立,所以becomeFirstResponder方法将返回false。

响应者链遵守事件传递的具体路径

如果最初的对象,hit-test视图或者第一响应者(first
responder)不能够处理事件,UIKit将传递事件到响应者链中的下一个响应者。每一个响应者都会决定是否处理事件还是调用nextResponder方法将事件传递给下级响应者。该过程一直到有一个响应者对象能够处理事件或者没有下级响应者为止。

下图显示了两个APP配置下2种不同事件类型的路径传递,事件传递路径取决于具体的结构,所有的事件传递都遵守相同的起始。

Figure 2-2  The
responder chain on iOS

左边App事件所传递的路径:

1:初始视图(initial view)将尝试着处理事件或消息。如果它不能处理事件,将传递事件到自己的父视图(superview),因为初始视图并不是它所在视图控制器中视图层级的最顶部视图。

2:父视图(superview)将尝试处理所传递的事件,如果父视图不能够处理事件,该事件将传递到它自己的父视图,因为它仍然不是视图层级的最顶部视图.

3:视图控制器视图层级中最顶部视图(topmost view)将尝试处理所传递的事件,如果最顶部视图不能够处理事件,它将传递事件给它的视图控制器.

4:视图控制器(view controller)将尝试处理所传递事件,如果它不能够处理事件,该事件将被传递到window.

5:如果window对象不能够处理事件,它将传递事件到APP全局单例对象(singleton
app object).

6:如果app对象不能够处理事件,该事件将被放弃.

右边App事件的传递流程:

1:视图传递事件到它所在的视图控制器的视图层级中,一直到最顶部视图。

2:最顶部视图将传递事件到它的视图控制器

3:视图控制器将传递事件到它的最顶部视图的父视图。1~3步重复,直到找到根控制器

4:根视图控制器将传递事件到window对象

5:window对象将传递事件到app对象

唯一不同就在于,如果当前的ViewController是由层级关系的,那么当子ViewController不能处理事件时,它会将事件继续往上传递,直到传递到其Root
ViewController,其他流程是一样的。

开发中覆盖hitTest:withEvent:的一些用途:

1:增加视图的触摸区域

比如:按钮本身大小为20*20,但是太小不便操作,我们可以通过自定义UIButton,重写hitTest方法,增加点击区域:下面是添加了一个20*20的按钮,然后通过操作hitTest方法,实现100*100区域内可点击,即在每个方向增加40,具体实现代码:

class MyButton: UIButton {
    override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
        if !self.userInteractionEnabled || self.hidden || self.alpha == 0 {
            return nil
        }

        if CGRectContainsPoint(CGRectInset(self.bounds, -40, -40), point){
            for subview in self.subviews.reverse() {
                let convertPoint = subview.convertPoint(point, fromView:self)
                if let sview = subview.hitTest(convertPoint, withEvent: event) {
                    return sview
                }
            }
            return self
        }
        return nil
    }
}

hitTest:withEvent:方法首先检查视图是否允许接收触摸事件。视图允许接收触摸事件的条件是:

视图不是隐藏的:self.hidden == NO

视图是允许交互的:self.userInteractionEnabled ==true

视图透明度大于0.01:self.alpha >
0.01

视图包含这个点: pointInside:withEvent: ==true

然后,如果视图允许接收触摸事件,这个方法通过从后往前发送hitTest:withEvent:消息给每一个子视图来穿过接收者的子树,直到子视图中的一个返回nil。这些子视图中的第一个返回的非nil就是在触摸点下面的最前面的视图,被接收者返回。如果所有的子视图都返回nil或者接收者没有子视图返回接收者自己。否则,如果视图不允许接收触摸事件,这个方法返回nil而根本不会传递到接收者的子树。因此,hit-test可能不会访问所有的视图体系结构中的视图。

测试代码和效果如下:

  func testExpandButtonClickArea(){

        //为了便于观察,添加一个背景视图,大小正好为100*100
        let backgroundView = UIView(frame:  CGRect(x: 60, y: 160, width: 100, height: 100))
        backgroundView.backgroundColor = UIColor.purpleColor()
        view.addSubview(backgroundView)

        let btn = MyButton(type: .Custom)
        btn.frame = CGRect(x: 100, y: 200, width: 20, height: 20)
        btn.backgroundColor = UIColor.redColor()
        btn.setTitle("btn", forState: .Normal)
        btn.addTarget(self, action: #selector(UIButtonViewController.tapButton), forControlEvents: .TouchUpInside)
        view.addSubview(btn)
    }

    func tapButton(){
        print("button has been pressed!");
    }

点击紫色区域内容,同样可以响应点击事件,可以在console看到打印输出:button has been pressed!

2:实现传递事件到点击视图之下的视图

有的时候对于一个视图忽略触摸事件并传递给下面的视图是很重要的。例如,假设一个透明的视图覆盖在应用内所有视图的最上面。覆盖层有子视图应该相应触摸事件的一些控件和按钮。但是触摸覆盖层的其他区域应该传递给覆盖层下面的视图。为了完成这个行为,覆盖层需要覆盖hitTest:withEvent:方法来返回包含触摸点的子视图中的一个,然后其他情况返回nil,包括覆盖层包含触摸点的情况:

class SHView: UIView {
    override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
        var hitTestView = super.hitTest(point, withEvent:event)
        if hitTestView == self{
           hitTestView = nil
        }
        return hitTestView;
    }
}

测试部分代码:

func testCoverView(){
        let btn1 = UIButton(type: .Custom)
        btn1.frame = CGRect(x: 80, y: 200, width: 20, height: 20)
        btn1.backgroundColor = UIColor.redColor()
        btn1.setTitle("btn1", forState: .Normal)
        btn1.addTarget(self, action: #selector(OverSuperViewController.tapButton(_:)), forControlEvents: .TouchUpInside)
        view.addSubview(btn1)

        let btn2 = UIButton(type: .Custom)
        btn2.frame = CGRect(x: 120, y: 200, width: 20, height: 20)
        btn2.backgroundColor = UIColor.yellowColor()
        btn2.setTitle("btn2", forState: .Normal)
        btn2.addTarget(self, action: #selector(OverSuperViewController.tapButton(_:)), forControlEvents: .TouchUpInside)
        view.addSubview(btn2)

        //添加一个覆盖层
        let backgroundView = SHView(frame:  CGRect(x: 60, y: 160, width: 100, height: 100))
        backgroundView.backgroundColor = UIColor.purpleColor()
        backgroundView.alpha = 0.75;
        view.addSubview(backgroundView)
    }

    func tapButton(button:UIButton){
        print("button = %@,title = %@",button,button.currentTitle);
    }

当点击覆盖层的时候,如果点击的位置属于对应的按钮的区域,将响应对应的触发事件,点击btn1将打印按钮1的相关信息,点击按钮2将打印按钮2的相关信息。页面效果如下:

  

3:超出父视图区域部分响应事件

首先看一下页面效果:当前页面上有3个控件,紫色视图是红色视图的子视图,红色视图是灰色视图的子视图。最上面是一个按钮,方便我们进行测试:现在我们要实现点击红色视图之外的紫色区域能够响应事件。

实现代码:自定义TestView实现hitTest方法,并调用我们对UIView的扩展方法

class TestView: UIView {
    override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
        super.hitTest(point, withEvent: event)
        return overlapHitTest(point, withEvent: event)
    }
}

extension UIView{
    func overlapHitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
        // We should not send touch events for hidden or transparent views, or views with userInteractionEnabled set to NO;
        if !self.userInteractionEnabled || self.hidden || self.alpha == 0 {
            return nil
        }
        // If touch is inside self, self will be considered as potential result.
        var hitView: UIView? = self
        if !self.pointInside(point, withEvent: event) {
            if self.clipsToBounds {
                return nil
            } else {
                hitView = nil
            }
        }
        // Check recursively all subviews for hit. If any, return it.
        for subview in self.subviews.reverse() {
            let insideSubview = self.convertPoint(point, toView: subview)
            if let sview = subview.overlapHitTest(insideSubview, withEvent: event) {
                return sview
            }
        }
        // Else return self or nil depending on result from step 2.
        return hitView
    }
}

测试部分代码:

 func testOverSuperview(){

        let view1 = TestView(frame:CGRect(x: 100, y: 100, width: 200, height: 200))
        view1.backgroundColor = UIColor.lightGrayColor()
        view.addSubview(view1)

        let view2 = UIView(frame: CGRect(x: 40, y: 40, width: 100, height: 100))
        view2.backgroundColor = UIColor.redColor()
        view1.addSubview(view2)

        let view3 = UIButton(type: .Custom)
        view3.frame = (frame: CGRect(x: 10, y: 10, width: 200, height: 80))
        view3.backgroundColor = UIColor.purpleColor()
        view3.addTarget(self, action: #selector(ThirdViewController.tapButton), forControlEvents: .TouchUpInside)
        view2.addSubview(view3)
    }

    func tapButton(){
        print("button has been pressed!");
    }

参考文章:

https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/Introduction/Introduction.html

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

http://blog.csdn.net/jiajiayouba/article/details/23447145

http://ios.jobbole.com/81864/

http://stackoverflow.com/questions/4961386/event-handling-for-ios-how-hittestwithevent-and-pointinsidewithevent-are-r

时间: 2024-11-06 21:55:09

iOS事件的分发机制和响应者链(Swift)的相关文章

测试安卓触摸事件的分发机制

概要: Activity |dispatchTouchEvent  ========================> ^onTouchEvent -- PhotoWindow.FragmentLayout ViewGroup |dispatchTouchEvent  = onInterceptTouchEvent > |onTouchEvent View |dispatchTouchEvent  ========================> |onTouchEvent 其中, 角

从ScrollView嵌套EditText的滑动事件冲突分析触摸事件的分发机制以及TextView的简要实现和冲突的解决办法

本篇文章假设读者没有任何的触摸事件基础知识,所以我们会从最基本的触摸事件分发处说起. ScrollView为什么会出现嵌套EditText出现滑动事件冲突呢?相信你会有这种疑问,我们来看这么一种情况: 有一个固定高度的EditText,假设它只能显示3行文本,但是,我们在其中输入的文本多余三行时,那么这时就需要可以在EditText内部进行小幅滚动了.那么将这个EditText放入了ScrollView当中, 并且ScrollView内容过多以致ScrollView也可以滑动,这时候就会出现Ed

Android 触摸事件 点击事件的分发机制 详解三---责任链模式

前面两节  我们讲述了 android 点击事件的分发流程.其实大家可以细细体会一下,这个分发的过程 始终是从顶层到下层.一层一层的按照顺序进行. 当然了,传到哪一层停止,我们可以通过重写某些方法来完成. 这个地方 android的开发人员很好的利用了 责任链模式来完成这边代码的编写. 下面我们就来讲一下 责任链模式到底是什么.以及如何运用. 大家知道 一个软件公司的基本架构就是 程序员----leader---project manager---boss 这种基础架构. 我们一般都会有team

事件的分发机制(View篇因此事件传递的顺序是先经过onTouch,再传递到onClick)

参考声明:感谢郭霖http://blog.csdn.net/guolin_blog/article/details/9097463和张鸿洋http://blog.csdn.net/lmj623565791/article/details/38960443 以一个简单的activity为例,该activity中只有一个button,如果我们为该按钮添加监听,只需要这样: 1 button.setOnClickListener(new OnClickListener() { 2 @Override

Android事件的分发机制

在分析Android事件分发机制前,明确android的两大基础控件类型:View和ViewGroup.View即普通的控件,没有子布局的,如Button.TextView. ViewGroup继承自View,表示可以有子控件,如Linearlayout.Listview这些.今天我们先来了解View的事件分发机制. 先看下代码,非常简单,只有一个Button,分别给它注册了OnClick和OnTouch的点击事件. 1 btn.setOnClickListener(new View.OnCli

Android 触摸事件 点击事件的分发机制 详解

最近发现团队里有些员工在做一些自定义控件的时候感觉比较吃力.尤其是做触摸事件这种东西的时候.很多人对机制并不理解.因为百度出来的东西都太理论化了.确实不好理解. 今天带大家坐几个小demo.帮助理解一下. 先从简单的view 的事件分发机制开始解释. 我们首先自定义一个工程 package com.example.testtouch; import android.app.Activity; import android.os.Bundle; import android.util.Log; i

Anroid View事件响应机制和ViewGroup的事件响应分发机制

注:低版本的源码内容比高版本的源码简单,分析起来方便,但是高版本源码更为严密. View的事件响应机制 涉及2个方法dispatchTouchEvent和onTouchEvent 1.View的dispatchTouchEvent方法(事件传递到View,View的这个方法就自动执行.) dispatchTouchEvent返回true,响应事件:返回false,不响应事件. public boolean dispatchTouchEvent(MotionEvent event) { ... L

iOS基础-事件处理、触摸、响应者链

事件处理的事件传递 简介: 发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件 队列中,UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow) UIView不接受触摸事件的三种情况: 不接收用户交互 userInteractionEnabled = NO 隐藏 hidden = YES 透明 alpha = 0.0 ~ 0.01 提示:UIImageView的userInteract

Android 触摸事件 点击事件的分发机制 详解二

现在我们来看看 事件分发的流程.view group 怎么传递给view的. 首先自定义一个layout 1 package com.example.testtouch; 2 3 import android.content.Context; 4 import android.util.AttributeSet; 5 import android.util.Log; 6 import android.view.MotionEvent; 7 import android.widget.Linear