Skip to content

Latest commit

 

History

History
189 lines (109 loc) · 9.65 KB

事件响应机制.md

File metadata and controls

189 lines (109 loc) · 9.65 KB

事件响应机制

iOS中的事件

iOS 的事件可以分为三种

  • Touch Events(触摸事件)
  • Motion Events(运动事件,比如重力感应和摇一摇等)
  • Remote Events(远程事件,比如用耳机上得按键来控制手机)

我们主要是研究Touch Events(触摸事件),Touch Events事件的整个过程可以分为 传递和响应 2 个阶段,

  • 传递: 是当我们触摸屏幕时,为我们找出最适合的 View,
  • 响应: 当我们找出最适合的 View 后,此时只是找到了最合适的 View,但未必 此 View 可以响应此事件,所以需要继续找出能响应此事件的 View

传递过程

查找第一响应者时,有两个非常关键的API,查找第一响应者就是通过不断调用子视图的这两个API完成的

调用方法,获取到被点击的视图,也就是第一响应者。

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

hitTest:withEvent:方法内部会通过调用这个方法,来判断点击区域是否在视图上,是则返回YES,不是则返回NO

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

每当手指接触屏幕,操作系统会把事件传递给当前的 App, 在 UIApplication接收到手指的事件之后,就会去调用UIWindow的hitTest:withEvent:,看看当前点击的点是不是在window内,如果是则继续依次调用其 subViewhitTest:withEvent:方法,直到找到最后需要的view。调用结束并且hit-test view确定之后,便可以确定最合适的 View

传递过程

传递过程1

传递过程2

传递过程3

应用如何找到最合适的控件来处理事件?

  • 1.首先判断主窗口(keyWindow)自己是否能接受触摸事件
  • 2.判断触摸点是否在自己身上
  • 3.子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤)
  • 4.view,比如叫做fitView,那么会把这个事件交给这个fitView,再遍历这个fitView的子控件,直至没有更合适的view为止。
  • 5.如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view。
  • 6、UIViewController没有hitTest:withEvent:方法,所以控制器不参与查找响应视图的过程。但是控制器在响应者链中,如果控制器的View不处理事件,会交给控制器来处理。控制器不处理的话,再交给View的下一级响应者处理。

UIView不能接收触摸事件的三种情况:

  • 不允许交互:userInteractionEnabled = NO
  • 隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
  • 透明度:如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明

下面是 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event 方法的内部实现

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

hitTest:withEvent:方法

  • 什么时候调用?
    • 只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法
  • 作用
    • 寻找并返回最合适的view(能够响应事件的那个最合适的view)

拦截事件的处理

  • 正因为hitTest:withEvent:方法可以返回最合适的view,所以可以通过重写hitTest:withEvent:方法,返回指定的view作为最合适的view。
  • 不管点击哪里,最合适的view都是hitTest:withEvent:方法中返回的那个view
  • 通过重写hitTest:withEvent:,就可以拦截事件的传递过程,想让谁处理事件谁就处理事件

事件传递给谁,就会调用谁的hitTest:withEvent:方法。 注意:如果hitTest:withEvent:方法中返回nil,那么调用该方法的控件本身和其子控件都不是最合适的view,也就是在自己身上没有找到更合适的view。那么最合适的view就是该控件的父控件。

响应过程

在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接受并处理事件,我们称之为“响应者对象”。以下都是继承自UIResponder的,所以都能接收并处理事件。

  • UIApplication
  • UIViewController
  • UIView

那么为什么继承自UIResponder的类就能够接收并处理事件呢? 因为UIResponder中提供了以下4个对象方法来处理触摸事件。

UIResponder内部提供了以下方法来处理事件触摸事件
- (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;
加速计事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
远程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;

当我们知道最合适的 View 后,事件会 【子view -> 父view,控制器view -> 控制器】来找出合适响应事件的 View,来响应相关的事件。如果当前的 View 有添加手势,那么直接响应相应的事件,不会继续向下寻找了,如果没有手势事件,那么会看其是否实现了4个触摸方法

响应者链的事件传递过程:

  • 1、如果当前view是控制器的view,那么控制器就是上一个响应者,事件就传递给控制器;如果当前view不是控制器的view,那么父视图就是当前view的上一个响应者,事件就传递给它的父视图
  • 2、在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理
  • 3、如果window对象也不处理,则其将事件或消息传递给UIApplication对象
  • 4、如果UIApplication也不能处理该事件或消息,则将其丢弃

响应者链

实践

实践

示意图说明:白色 view 是蓝色 view 的父视图;蓝色 view 是橙色 view 的父视图。

点击重叠区,只有蓝色 view(既父视图)响应事件。

一个最简单的办法是将子视图的 isUserInteractionEnabled 设置为 false ;也可以在子视图的 hitTest(_:with:) 方法里面返回 nil 或 superview ,可以达到同样的效果。

需求二:点击屏幕上的任意地方;只有蓝色 view 响应事件。

一个最简单的办法是在蓝色 view 的 hitTest(_:with:) 方法里返回 self 。当事件传递到蓝色 view 时,返回自己做为最适合触发事件的控件。

为什么 父View 关闭了事件响应时,子View 就无法响应事件?

因为在事件传递的时,先到父view,当父view无法响应事,直接就跳过了遍历其子view,故只要父类关闭了事件,子 view 就已经没有机会响应事件了。

如何扩大 Button 的点击范围?

扩大点击范围,无非就是想本来没有点击 btn 但想让 btn 响应事件,那么可以在 hitTest 方法中做适当的操作,当满足xxx条件时,强行返回 btn 来达到最佳点击范围的效果,相关的实现可以自行 Google ,有一些较优雅而简洁的方式。

如何让 父View 和 子View 同时响应同一事件?

父View 和 子View同时响应同一事件,默认当点击子view时,如果ziview可以处理事件,那么其他父view 是不会响应的,但是在 父view 传到 子view 时我们在 hitTest 方法中是清楚知道的,使用可以在这里做相关的操作便实现了子view 和父view 同时响应事件的效果。

为什么子View 关闭了事件,但其 父View 开启事件的情况下,点击 子View 时,父View 可以响应事件

子view关闭了事件,事件的传递是 父view 到子view,在 父view时,父view可以响应,那么会继续访问其 子view是否可以响应,如果此时子view不可以响应,那么他会直接返回 父view,所以 子View 关闭了事件 父View 正常执行事件是必然的。

浅谈 iOS 事件的传递和响应过程

史上最详细的iOS之事件的传递和响应机制-原理篇

史上最详细的iOS之事件的传递和响应机制-实践篇

深入理解 iOS 事件机制