《Responders and the Responder Chain》

f17df5bc-d80b-4e17-81cf-4277b1e0f6e4

如何决定哪个Responder响应事件?

UIKit使用基于视图的点击测试来确定触摸事件发生的位置。具体地说,UIKit会将触摸位置与视图层次中的视图对象边界进行比较。UIView的hitTest(_:with:)方法遍历视图层次结构,查找包含指定触摸的最深的子视图,这将成为触摸事件的first responder

注意⚠️,如果触摸的发生位置在某个view之外,那么该view的所有subviews都会被忽略,即使有的subviewclipsToBounds属性设置为false而导致超出superView

发生触摸时,UIKit会创建一个UITouch对象并将其与view相关联。随着触摸位置或其他参数的变化,UIKit会使用新信息更新相同的UITouch对象。唯一不变的属性是view。(即使触摸位置移动到原始视图之外,触摸的视图属性中的值也不会更改。).当触摸结束时,UIKit释放UITouch对象。

改变响应链

可以通过修改UIRespondernext属性来修改某个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的相关方法。

0c8c5e29-c846-4a16-988b-3d809eafbb6b

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
2
3
4
5
6
7
8
9
10
11
@objc func tapPiece(_ gestureRecognizer : UITapGestureRecognizer ) {
   guard gestureRecognizer.view != nil else { return } // 这里有个疑惑,gestureRecognizer为什么还有view这个属性?
        
   if gestureRecognizer.state == .ended {      // Move the view down and to the right when tapped.
      let animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeInOut, animations: {
         gestureRecognizer.view!.center.x += 100
         gestureRecognizer.view!.center.y += 100
      })
      animator.startAnimation()
}}

在使用Gesture recognizer之前,确保下列三条: