【iOS】变乱转达与相应机制

打印 上一主题 下一主题

主题 782|帖子 782|积分 2346


前言

提到相应者链与变乱转达,如果看过其他人的博客,常常能看到这经典的三张图



本文会对变乱的转达与相应机制举行具体的解说
变乱UIEvent

在开讲之前,我们先来明白一下UIEvent
变乱指的是 UIEvent : NSObject,它的API文档很简单:
  1. typedef NS_ENUM(NSInteger, UIEventType) {
  2.     UIEventTypeTouches,
  3.     UIEventTypeMotion,
  4.     UIEventTypeRemoteControl,
  5.     UIEventTypePresses API_AVAILABLE(ios(9.0)),
  6.     UIEventTypeScroll      API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos) = 10,
  7.     UIEventTypeHover       API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos) = 11,
  8.     UIEventTypeTransform   API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos) = 14,
  9. };
  10. typedef NS_ENUM(NSInteger, UIEventSubtype) {
  11.     // available in iPhone OS 3.0
  12.     UIEventSubtypeNone                              = 0,
  13.     // for UIEventTypeMotion, available in iPhone OS 3.0
  14.     UIEventSubtypeMotionShake                       = 1,
  15.     // for UIEventTypeRemoteControl, available in iOS 4.0
  16.     UIEventSubtypeRemoteControlPlay                 = 100,
  17.     UIEventSubtypeRemoteControlPause                = 101,
  18.     UIEventSubtypeRemoteControlStop                 = 102,
  19.     UIEventSubtypeRemoteControlTogglePlayPause      = 103,
  20.     UIEventSubtypeRemoteControlNextTrack            = 104,
  21.     UIEventSubtypeRemoteControlPreviousTrack        = 105,
  22.     UIEventSubtypeRemoteControlBeginSeekingBackward = 106,
  23.     UIEventSubtypeRemoteControlEndSeekingBackward   = 107,
  24.     UIEventSubtypeRemoteControlBeginSeekingForward  = 108,
  25.     UIEventSubtypeRemoteControlEndSeekingForward    = 109,
  26. };
  27. /// Set of buttons pressed for the current event
  28. /// Raw format of: 1 << (buttonNumber - 1)
  29. /// UIEventButtonMaskPrimary = 1 << 0
  30. typedef NS_OPTIONS(NSInteger, UIEventButtonMask) {
  31.     UIEventButtonMaskPrimary    = 1 << 0,
  32.     UIEventButtonMaskSecondary  = 1 << 1
  33. } NS_SWIFT_NAME(UIEvent.ButtonMask) API_AVAILABLE(ios(13.4)) API_UNAVAILABLE(tvos, watchos);
  34. /// Convenience initializer for a button mask where `buttonNumber` is a one-based index of the button on the input device
  35. /// .button(1) == .primary
  36. /// .button(2) == .secondary
  37. UIKIT_EXTERN UIEventButtonMask UIEventButtonMaskForButtonNumber(NSInteger buttonNumber) NS_SWIFT_NAME(UIEventButtonMask.button(_:)) API_AVAILABLE(ios(13.4)) API_UNAVAILABLE(tvos, watchos);
  38. UIKIT_EXTERN API_AVAILABLE(ios(2.0)) @interface UIEvent : NSObject
  39. @property(nonatomic,readonly) UIEventType     type API_AVAILABLE(ios(3.0));
  40. @property(nonatomic,readonly) UIEventSubtype  subtype API_AVAILABLE(ios(3.0));
  41. @property(nonatomic,readonly) NSTimeInterval  timestamp;
  42. @property (nonatomic, readonly) UIKeyModifierFlags modifierFlags API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos);
  43. @property (nonatomic, readonly) UIEventButtonMask buttonMask API_AVAILABLE(ios(13.4)) API_UNAVAILABLE(tvos, watchos);
  44. @property(nonatomic, readonly, nullable) NSSet <UITouch *> *allTouches;
  45. - (nullable NSSet <UITouch *> *)touchesForWindow:(UIWindow *)window;
  46. - (nullable NSSet <UITouch *> *)touchesForView:(UIView *)view;
  47. - (nullable NSSet <UITouch *> *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture API_AVAILABLE(ios(3.2));
  48. // An array of auxiliary UITouch’s for the touch events that did not get delivered for a given main touch. This also includes an auxiliary version of the main touch itself.
  49. - (nullable NSArray <UITouch *> *)coalescedTouchesForTouch:(UITouch *)touch API_AVAILABLE(ios(9.0));
  50. // An array of auxiliary UITouch’s for touch events that are predicted to occur for a given main touch. These predictions may not exactly match the real behavior of the touch as it moves, so they should be interpreted as an estimate.
  51. - (nullable NSArray <UITouch *> *)predictedTouchesForTouch:(UITouch *)touch API_AVAILABLE(ios(9.0));
  52. @end
  53. NS_ASSUME_NONNULL_END
  54. #else
  55. #import <UIKitCore/UIEvent.h>
  56. #endif
复制代码
我们以 UIEventType 作为突破口
  1. typedef NS_ENUM(NSInteger, UIEventType) {
  2.     UIEventTypeTouches,
  3.     UIEventTypeMotion,
  4.     UIEventTypeRemoteControl,
  5.     UIEventTypePresses API_AVAILABLE(ios(9.0)),
  6.     UIEventTypeScroll      API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos) = 10,
  7.     UIEventTypeHover       API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos) = 11,
  8.     UIEventTypeTransform   API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos) = 14,
  9. };
复制代码
现在iOS主流使用的变乱有三种


  • touch events(触摸变乱)
  • motion events(运动变乱)
  • remote-control events(远程控制变乱)

我们这里着重讲一下触摸变乱
   触摸变乱就是我们的手指或者苹果的
Pencil(触笔)在屏幕中所引发的互动,比如轻点、长按、滑动等操纵,是我们最常接触到的变乱类型。触摸变乱对象可以包罗一个或多个触摸,并且每个触摸由
UITouch 对象表示。当触摸变乱发生时,系统会将其沿着线路转达,找到得当的相应者并调用得当的方法,例如
touchedBegan:withEvent:。相应者对象会根据触摸来确定得当的方法。 触摸变乱分为以下几类:
  手势变乱


  • 长按手势(UILongPressGestureRecognizer)
  • 拖动手势(UIPanGestureRecognizer)
  • 捏合手势(UIPinchGestureRecognizer)
  • 相应屏幕边沿手势(UIScreenEdgePanGestureRecognizer)
  • 轻扫手势(UISwipeGestureRecognizer)
  • 旋转手势(UIRotationGestureRecognizer)
  • 点击手势(UITapGestureRecognizer)
  • 自界说手势
  • 点击 button 相关

触摸变乱对应的对象为 UITouch,UITouch现实上就对应着我们的手指,有几根手指就有几个UITouch对象
一、变乱转达

变乱转达机制(Event Handling)
iOS的变乱转达系统将触摸和其他变乱(如动作、手势)发送到视图层次结构中的得当对象。在变乱转达过程中,系统通常从根视图开始查找,并递归向下查找以找到最得当处置惩罚该变乱的视图。
转达流程


  • 变乱的产生
    用户通过手势或是触摸等其他操纵与设备交互,天生变乱,系统将变乱转达给应用的UIApplication实例,以开始变乱分发
  • UIApplication变乱分发
    UIApplication负责顶层管理所有效户输入变乱。
    它将变乱转达给当前活动的UIWindow对象,以进一步查找得当的相应者。
  • UIWindow变乱分发
    当前活动的UIWindow对象接收变乱并通过hitTest:withEvent:方法开始寻找得当的视图
    UIWindow遍历整个视图层次结构,以找到最合适的视图来相应变乱。
  • 掷中测试(Hit-Testing)
    hitTest:withEvent:是寻找第一相应者的核心方法。它通过以下步骤工作:

  • 查抄当前视图的userInteractionEnabled、hidden和alpha属性以确保视图可交互。当视图隐蔽属性hidden=NO、交互userInteractionEnabled=YES、透明度alpha>0.01三者同时满意才拥有相应能力。
  • 调用pointInside:withEvent:,确定触摸点是否在当前视图的界限范围内。
  • 从后往前遍历子视图,递归调用子视图的hitTest:withEvent:方法。
  • 如果找到合适的子视图,它将返回该子视图作为第一相应者;否则返回当前视图自身。
我们可以写一个其简单实现实例
  1. - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
  2.     // 视图不能交互、隐藏或不可见时,直接返回nil
  3.     if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha < 0.01) {
  4.         return nil;
  5.     }
  6.     // 判断触摸点是否在当前视图范围内
  7.     if (![self pointInside:point withEvent:event]) {
  8.         return nil;
  9.     }
  10.     // 从后往前遍历子视图(子视图叠加次序),递归调用
  11.     for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
  12.         // 转换坐标到子视图的坐标系
  13.         CGPoint convertedPoint = [subview convertPoint:point fromView:self];
  14.         // 递归查找子视图
  15.         UIView *hitView = [subview hitTest:convertedPoint withEvent:event];
  16.         if (hitView != nil) {
  17.             return hitView;
  18.         }
  19.     }
  20.     // 没有合适的子视图时,当前视图自己成为第一响应者
  21.     return self;
  22. }
复制代码


  • 第一相应者确定
    如果确定了当前触摸点在当前视图上,同时当前视图没有任何子视图,那么当前视图就成为第一相应者并开始处置惩罚触摸变乱
    touchesBegan:withEvent:、touchesMoved:withEvent:、touchesEnded:withEvent:、touchesCancelled:withEvent:方法由第一相应者接收并处置惩罚,这些都是触摸变乱
讲到这里,其实我们的变乱转达就已经结束了,变乱转达的目的就是为了让我们找到第一相应者
总结一下第一相应者

  • 可以或许相应触摸变乱
  • 触摸点在自己身上
  • 没有任何子视图,或是所有子视图都不在触摸点上

遍历顺序

我们在上图中看到了在subViews中查找相应者的过程是倒序遍历,这是什么意思呢
也就是我们遍历当前视图的子视图时,起首hitTest:withEvent:方法会被子视图数组中的末了一个元素调用
怎样明白这句话呢?简单明白就是会从末了一个添加到当前视图的子视图举行遍历,也就是视图上最上层的子视图是第一个被遍历的,然后再继续去遍历其他子视图,我们来看一个demo:

可以看到touchView1先被添加到view中,随后再添加touchView2,我们来看一下subViews数组

可以看到touchView2在数组的尾部,这也就说明了倒序遍历就是从反面添加的视图向前遍历
二、手势辨认

找到了合适的View,也就是第一相应者,如果是触摸变乱,我们就要去辨认是何种手势
使用不同的手势会调用不同次数的变乱,这里我们不细讲,只要知道有这么一个过程即可
三、相应机制

在了解相应者链前,我们必要知道什么是相应者
UIResponder(相应者)

在 iOS 中,只有继续于 UIResponder 的对象、或者它本身才能成为相应者。很多常见的对象都可以相应变乱,比如 UIApplication 、UIViewController、所有的 UIView(包括 UIWindow)。
我们来看一张继续图

可以看到UIResponder提供了我们平常最常用的touchesBegan/touchesMoved/touchesEnded方法。此外尚有如下几个属性比力告急:


  • isFirstResponder:判断该View是否为第一相应者。
  • canBecomeFirstResponder:判断该View是否可以成为第一相应者。
  • becomeFirstResponder:使该View成为第一相应者。
  • resignFirstResponder:取消View的第一相应者。
如果我们将一个view_A先加在view_B上,然后又加到view_C上,那么view_A.nextResponder指的是view_B。
相应者链

找到第一相应者之后并且辨认脱手势后,我们就要确定由谁来相应这个变乱了,怎样明白这句话呢?
第一相应者不愿定能相应变乱,因为他可能并没有实现触摸变乱
我们来以一个Demo来明白


赤色的是V1,蓝色的是V2,V2是第一相应者
我们为我们的V1添加点击变乱

我们点击蓝色地区

可以看到相应触摸变乱的我们的V1,也就是赤色地区,这也说明了第一相应者不愿定能相应变乱
这里必要留意的一点是如果我们要给视图添加触摸变乱,肯定要新建一个子类View,不能再UIViewC中重写touches实例方法,因为这样变乱的相应者就是UIViewC而非你盼望中的View,我们也以一个Demo来树模
在VC中重写方法

在VC中重写方法

可以看到当我点击空白地区时候相应的是VC,点击蓝色或是赤色地区时相应的是V1,这是因为变乱的转达是沿相应者链转达的,由此引出我们对相应者链的讨论
相应者链示意图:




  • Response Chain,相应链,一般我们称之为相应者链。
  • 在我们的 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 的属性串联起来的。
    @property(nonatomic, readonly, nullable) UIResponder *nextResponder;
总结一下相应者链的相应流程
判断当前视图能否相应,再去判断当前视图的nextResponder,如果是VC的View,那么nextResponder就是VC
如果不是控制器的 View,上一个相应者就是SuperView
相应的大致的过程 第一相应者 –> super view –> ……–> view controller –> window –>Application
四、相关应用

扩大button点击范围

办理:给button加分类然后重写pointInside
实现步骤:


  • 自界说按钮:创建一个自界说按钮子类,继续自 UIButton。
  • 重写 point(inside:with:在自界说按钮类中重写此方法。该方法担当一个点,并判断该点是否在视图的范围内。你可以扩展点击地区,以便更宽泛的地区内点击时视图仍然会接收点击变乱。
  • 设定点击地区扩展的大小:可以设定必要扩大点击范围的值,在四个方向上(上、下、左、右)同时增大或仅针对特定方向。
  1. // CustomButton.h
  2. #import <UIKit/UIKit.h>
  3. @interface CustomButton : UIButton
  4. @property (nonatomic) UIEdgeInsets hitTestEdgeInsets;
  5. @end
  6. // CustomButton.m
  7. #import "CustomButton.h"
  8. @implementation CustomButton
  9. - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
  10.     // 计算新的扩大后的点击区域
  11.     CGRect largerFrame = CGRectMake(self.bounds.origin.x - self.hitTestEdgeInsets.left,
  12.                                     self.bounds.origin.y - self.hitTestEdgeInsets.top,
  13.                                     self.bounds.size.width + self.hitTestEdgeInsets.left + self.hitTestEdgeInsets.right,
  14.                                     self.bounds.size.height + self.hitTestEdgeInsets.top + self.hitTestEdgeInsets.bottom);
  15.     // 判断点是否在新的点击区域内
  16.     return CGRectContainsPoint(largerFrame, point);
  17. }
  18. @end
复制代码
在你的视图控制器中,将自界说按钮的 hitTestEdgeInsets 属性设置为所需的值,以扩大点击地区:
  1. // Example usage in a view controller
  2. CustomButton *button = [[CustomButton alloc] initWithFrame:CGRectMake(100, 100, 100, 40)];
  3. button.backgroundColor = [UIColor systemBlueColor];
  4. [button setTitle:@"Click Me" forState:UIControlStateNormal];
  5. // 将点击区域向四个方向各扩展10个点
  6. button.hitTestEdgeInsets = UIEdgeInsetsMake(-10, -10, -10, -10);
  7. [self.view addSubview:button];
复制代码
穿透变乱


例如我们想点击蓝色地区时相应变乱的是赤色地区,但是第一相应者是蓝色地区,那么就必要我们重写(UIView *)hitTestCGPoint)point withEventUIEvent *)event方法,让其无法成为相应者,这样就会让赤色成为相应者
  1. - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
  2.     self.userInteractionEnabled = NO;
  3.     return [super hitTest:point withEvent:event];
  4.    }
复制代码
总结



  • 当触摸变乱发生后,系统会主动天生一个UIEvent对象,记载变乱产生的时间和类型
  • 然后系统会将UIEvent变乱加入到一个由UIApplication管理的变乱队列中
  • 然后UIApplication将变乱分发给UIWindow,主窗口会在视图层次结构中找到一个最合适的视图来处置惩罚触摸变乱
  • 不停递归调用hitTest方法来找到第一相应者
  • 如果第一相应者无法相应变乱,那么按照相应者链往上转达,也就是转达给自己的父视图
  • 一直转达直到UIApplication,如果都无法相应则变乱被扬弃
    参考博客变乱转达与相应 详解(精通iOS系列)

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

千千梦丶琪

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表