<iOS底层原理探究> 第一探. 事件传递和响应者链

一. 声明: 

  本文意在探讨, 也参考了几位大神的文章, 在最后我会把链接发出来, 如果有理解错误的地方, 请大神们指正哈!

二. 前言: 

  最近自己做项目的时候, 用到了UITabbarController的UITabbar, 我们的设计是这样的, 如图: 

很常见的一种标签选择, 一个功能模块: 首页, 本地, 扫一扫, 入驻, 个人中心. 但是中间的扫一扫功能上半部分点击没有响应, 只有在UITabbar上的下半部分扫一扫点击才有响应, 那么我们如何来解决它呢? 下面就是今天咱们要探讨的内容: 事件传递和响应者链. 

三. 正文: 

1. 概念(知识点):

首先, 咱们在开始探讨之前, 先储备下几个概念或者说是知识点:

(1). 响应者对象: 可以响应事件的对象, 就是响应者对象. 继承自UIResponder的对象, 才可以响应事件. UIWindow, UIViewController, UIView, UIButton都是继承自UIResponder对象, 所以都可以响应事件

(2). 上一响应者: 视图的nextResponder属性, 得到的对象就是当前视图的上一响应者. 原则: 判断当前View是否是控制器的View (a). 如果是, 则上一响应者就是控制器 (b). 如果不是, 则上一响应者就是父控件

(3). 触摸对象: 一根手指触摸屏幕时就会产生一个UITouch对象, 也就是触摸对象, 所以我们可以根据UITouch对象的数量, 来判断用户有几根手指在操作屏幕

(4). 事件对象: 手指触摸屏幕时也会随之产生一个UIEvent对象, 也就是事件对象

2. 事件的生命周期

一个事件的生命周期: 从下往上找合适的视图, 从上往下找可响应的视图, 如果找不到则废弃该事件, 如图: 

从图中我们可以看到: 左边为事件的产生和传递的过程, 右边为事件的响应过程(响应者链)

首先是事件传递过程: 从下到上 从父控件到子控件

AppDelegate -> UIWindow -> UIViewController -> UIView

事件传递原则: 1. 自己是否能接收事件; 2. 触摸点是否在自己身上
当一个触摸事件发生后, UIApplication会将事件加入到一个事件队列中, 因为队列遵从FIFO原则(先进先出), 先发生的事件, 先处理. 

(a). UIApplication从队列中取出一个事件后, 将该事件传递给UIWindow, UIWindow检查自己是否符合事件传递原则

(b). 如果UIWindow符合, 则将该事件传递给UIViewController, UIViewController检查自己是否符合事件传递原则

(c). 如果UIViewContrller符合, 则将该事件传递给UIView, UIView检查自己是否符合事件传递原则

(d). 如果UIView符合, 则继续传递给自己的子控件, 如果没有子控件, 则事件传递过程结束. UIView就是合适的视图.

其次是事件响应过程: 从上到下 从子控件到父控件

重写touchesBegan:withEvent:方法就可以响应事件

UIView -> UIViewController -> UIWindow -> AppDelegate

(a). 如果UIView是合适的视图, 如果UIView不能响应事件, 则找自己的下一响应者, 也就是自己的父控件, 自己的父控件是UIViewController的View

(b). 如果View不能响应事件, 则找自己的下一响应者, 因为View是控制器的View, 所以上一响应者是UIViewController

(c). 如果UIViewController不能响应事件, 则找自己的下一响应者: UIWindow 

(d). 如果UIWindow不能响应事件, 则找自己的下一响应者: AppDelegate

(e). 如果AppDelegate不能响应事件, 则事件忽略, 废弃

3. 注意事项: 

(1). 视图不能接收事件的三种情况: (a). 当不允许用户交互时, 也就是视图的userInterActionEnabled属性值为NO时 (b). 当视图隐藏时, 也就是视图的hidden属性值为YES时 (c). 当视图的透明度在0.00 - 0.01范围时, 也就是视图的alpha属性值在0.00 - 0.01范围时
(2). 父控件对子控件的三种影响: (a). 当父控件不能接收事件时, 那么它所有的子控件也不能接收触摸事件 (b). 父控件的hidden会直接影响子控件的hiden (c). 父控件的alpha会直接影响子控件的hidden

(3). 事件传递如何寻找最合适的视图: (a). 判断自己是否能接收触摸事件 (b). 判断触摸点在不在自己身上 (c). 如果以上a, b两种情况都符合, 就从后往前遍历自己的子控件, 然后再重复a, b两个步骤 (d). 如果没有符合条件的子控件, 那么自己就是最合适的视图

四. Talk is cheap. Show me the code.

代码最终呈现出来的结果是这样的, 代码的目录结构是这样的, 

应用的根视图是ViewController, 自定义的红, 橙, 黄, 绿, 青五个视图都添加到ViewController的View上

ViewController里的代码是这样的: 

    // 第一层View
    LCLRedView * redView = [LCLRedView new];
    redView.frame = CGRectMake(20, 64 + 20, 300, 150);
    [self.view addSubview:redView];
    
    // 第一层View
    LCLOrangeView * orangeView = [LCLOrangeView new];
    orangeView.frame = CGRectMake(20, CGRectGetMaxY(redView.frame) + 20, 300, 300);
    [self.view addSubview:orangeView];
    
    // 第二层View
    LCLYellowView * yellowView = [LCLYellowView new];
    yellowView.frame = CGRectMake(0, 0, 200, 200);
    [orangeView addSubview:yellowView];
    
    // 第二层View
    LCLGreenView * greenView = [LCLGreenView new];
    greenView.frame = CGRectMake(150, 150, 150, 150);
    [orangeView addSubview:greenView];
    
    // 第三层View
    LCLCyanView * cyanView = [LCLCyanView new];
    cyanView.frame = CGRectMake(0, 0, 100, 100);
    [yellowView addSubview:cyanView];

自定义View里的代码是这样的, 下面是红色View里的代码, 其它自定义View的代码只是更改了对应的背景色

- (instancetype)init {
    if (self = [super init]) {
        self.backgroundColor = [UIColor redColor];
    }
    return self;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"%s", __func__);
}

代码就是这些, 其实关于事件的传递和响应, 代码不是很多, 重要的在思想上的理解, 接下来我们要做得就是点点这几个视图, 大家就可以理解啦!

好, 下面大家来跟着我做做练习

1. 点击红色视图, 控制台打印的是什么?

2. 点击橙色视图, 控制台打印的是什么?

3. 点击黄色视图, 控制台打印的是什么?

4 点击绿色视图, 控制台打印的是什么?

5. 点击青色视图, 控制台打印的是什么?

6. 当我们把黄色视图的userInterActionEnabled属性设置为NO时, 点击青色视图, 打印的是什么?

下面我们来分析第6种情况:

首先两个原则: (a). 自己是否能接收触摸事件 (b). 触摸点是否在自己身上

(1). 当我们点击青色视图, 事件产生, UIApplication开始处理这个事件, UIApplication将这个事件传递给UIWindow, 也就是keyWindow

(2). keyWindow接收事件, 检查自己能接收触摸事件, 触摸点也在自己身上, keyWindow将这个事件传递给ViewControler

(3). ViewController接收事件, 检查自己能接收事件, 触摸点也在自己身上, ViewControler的View从后往前遍历自己的子控件

(4). ViewController的View有两个子控件, 红色视图和橙色视图, 橙色视图为后添加的, 所以将这个事件传递给橙色视图, 

(5). 橙色视图接收事件, 检查自己能接收事件, 触摸点也在自己身上, 橙色视图从后往前遍历自己的子控件

(6). 橙色视图有两个子控件, 黄色视图和绿色视图, 绿色视图为后添加的, 所以将事件传递给绿色视图

(7). 绿色视图接收事件, 检查自己能接收事件, 但是触摸点不在自己身上. 所以橙色视图又将事件传递给黄色视图

(8). 黄色视图接收事件, 检查自己不能接收事件. 所以橙色视图继续遍历自己的子控件

(9). 橙色视图遍历完自己的子控件, 发现没有合适的视图, 所以橙色视图就是最合适的视图, 事件传递过程停止.

其次, 找到最合适的视图后, 开始事件响应过程

(1). 橙色视图检查自己是否能响应事件, 如果能, 处理事件, 整个事件结束. 如果不能, 将事件给下一响应者处理.

(2). 因为我们在橙色视图里重写了touchesBegan:withEvent:方法, 所以橙色视图能响应事件, 事件结束.

到此, 练习结束, 如果分析得有错误的地方, 或者大家有不同的见解, 欢迎大家过来讨论! 共同进步!

回首开篇: 我们提到的UITabbar突出按钮的处理:

1. 首先我们要自定义一个UITabBar类

下面是我们的自定义UITabBar类LCLTabBar, 重写它的hitTest:withEvent:方法, 作用是: 返回可处理该触摸事件的视图

#import <UIKit/UIKit.h>

@interface LCLTabBar : UITabBar

/** 中间的凸起按钮 */
@property (nonatomic, strong) UIButton * centerButton;

@end
#import "LCLTabBar.h"

@implementation LCLTabBar

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    /**
     self.centerButton: UITabBar中间的凸起按钮
     point: 用户的触摸点
     判断用户的触摸点是否在UITabBar中间的凸起按钮上
     如果是, 让UITabBar中间的凸起按钮响应事件
     如果不是, 让UITabbar响应事件
     */
    BOOL isContains = CGRectContainsPoint(self.centerButton.frame, point);
    if (isContains) {
        return self.centerButton;
    } else {
        return [super hitTest:point withEvent:event];
    }
}

@end

2. 然后我们在根视图里面, 根视图是继承自UITabBarController的子类, 这样写:

    /** 将系统的tabBar换成我们自定义的tabBar */
    LCLTabBar * tabBar = [LCLTabBar new];
    [self setValue:tabBar forKey:@"tabBar"];

    /** 中间的按钮 */
    CGFloat centerButtonWidth = 50; // 中间按钮的大小
    UIButton * centerButton = [UIButton buttonWithType:UIButtonTypeCustom];
    [centerButton setBackgroundColor:[UIColor whiteColor]];
    [centerButton setImage:[UIImage imageNamed:@"图片名字"] forState:UIControlStateNormal];
    centerButton.frame = CGRectMake((self.view.frame.size.width- centerButtonWidth) / 2, -centerButtonWidth/2 + 5, centerButtonWidth, centerButtonWidth);
    centerButton.layer.cornerRadius = centerButtonWidth/2; // 圆形按钮
    centerButton.layer.masksToBounds = YES;
    [tabBar addSubview:centerButton];
    tabBar.centerButton = centerButton;
    [centerButton addTarget:self action:@selector(centerButtonAction) forControlEvents:UIControlEventTouchUpInside];

完成以上两步, 我们的问题, 也就解决啦!

五. 参考文章

1. iOS事件拦截和事件转发

2. iOS事件传递响应机制

至此为止, 我们的<iOS底层原理探究>第一探也就结束啦, 如果本文有描述不正确的地方, 或者大家有不同的意见, 欢迎来此讨论!

Talk is cheap. Show me the code.

猜你喜欢

转载自www.cnblogs.com/ZeroHour/p/9460611.html