如何决定哪个Responder响应事件?
UIKit使用基于视图的点击测试来确定触摸事件发生的位置。具体地说,UIKit会将触摸位置与视图层次中的视图对象边界进行比较。UIView的hitTest(_:with:)
方法遍历视图层次结构,查找包含指定触摸的最深的子视图,这将成为触摸事件的first responder。
注意⚠️,如果触摸的发生位置在某个view
之外,那么该view
的所有subviews
都会被忽略,即使有的subview
因clipsToBounds
属性设置为false
而导致超出superView
。
发生触摸时,UIKit会创建一个UITouch对象并将其与view
相关联。随着触摸位置或其他参数的变化,UIKit会使用新信息更新相同的UITouch对象。唯一不变的属性是view
。(即使触摸位置移动到原始视图之外,触摸的视图属性中的值也不会更改。).当触摸结束时,UIKit释放UITouch对象。
改变响应链
可以通过修改UIResponder
的next
属性来修改某个responder的响应链。
比如,当一个view
是某个view controller
的根视图时,下一个响应者是view controller
;否则是视图的SuperView
。
pointInside和hitTest区别:
hitTest和pointInside是UIView提供的触摸事件处理方法。
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event: 用来判断触摸点是否在控件上
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event: 用来判断控件是否接受事件以及找到最合适的view
事件处理流程:
(1)当用户点击屏幕时,会产生一个触摸事件,系统会将该事件加入到一个由UIApplication管理的事件队列中
(2)UIApplication会从事件队列中取出最前面的事件进行分发以便处理,通常,先发送事件给应用程序的主窗口(UIWindow)
(3)主窗口会调用hitTest:withEvent:方法在视图(UIView)层次结构中找到一个最合适的UIView来处理触摸事件 (hitTest:withEvent:其实是UIView的一个方法,UIWindow继承自UIView,因此主窗口UIWindow也是属于视图的一种)
hitTest:withEvent:方法处理流程:
(1)首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内:若pointInside:withEvent:方法返回NO,说明触摸点不在当前视图内,则当前视图的hitTest:withEvent:返回nil 若pointInside:withEvent:方法返回YES,说明触摸点在当前视图内,则遍历当前视图的所有子视图(subviews),调用子视图的hitTest:withEvent:方法重复前面的步骤,子视图的遍历顺序是从top到bottom,即从subviews数组的末尾向前遍历,直到有子视图的hitTest:withEvent:方法返回非空对象或者全部子视图遍历完毕:
(2)若第一次有子视图的hitTest:withEvent:方法返回非空对象,则当前视图的hitTest:withEvent:方法就返回此对象,处理结束 若所有子视图的hitTest:withEvent:方法都返回nil,则当前视图的hitTest:withEvent:方法返回当前视图自身(self) (4)最终,这个触摸事件交给主窗口的hitTest:withEvent:方法返回的视图对象去处理
hitTest实现原理
- (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.从后往前遍历自己的子控件
NSInteger count = self.subviews.count;
for (NSInteger 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;
}
}
// 循环结束,表示没有比自己更合适的view
return self;
}
手势识别
窗口将触摸事件递送到手势识别器,然后将它们递送到附加到手势识别器的命中测试的视图。
手势识别器是处理触摸或按下事件的最简单方法。
手势识别器使用Target-Action设计模式发送通知。当UITapGestureRecognizer
对象在视图中检测到单指轻触时,它将调用view controller
的相关方法。
Gesture recognizers
有两种类型:离散的和连续的。离散的Gesture recognizers
在识别手势后只调用一次操作方法,而连续的则会多次调用,比如每次触摸位置更改时,UIPanGestureRecognizer对象都会调用操作方法。
state
属性记录了Gesture recognizers
的当前状态:对于连续Gesture recognizers
来说,状态变化是: UIGestureRecognizer.State.began
to UIGestureRecognizer.State.changed
to UIGestureRecognizer.State.ended
, or to UIGestureRecognizer.State.cancelled
.
Handling Tap Gestures
Tap gestures
检测一个或多个手指短暂触摸屏幕。
在执行任何操作之前,始终检查Gesture recognizers
的状态属性,即使是对于离散的Gesture recognizers
也是如此。
1 |
|
在使用Gesture recognizer
之前,确保下列三条:
- 确保
isUserInteractionEnabled
属性被置为true
. Image views 和 label默认是false
。 - 确保点击次数和
numberOfTapsRequired
属性相同。 - 确保触摸时手指的数量和
numberOfTouchesRequired
属性相同。