ios事件-触摸事件1(寻找点击的view)

本文深入探讨了iOS中事件响应链的工作原理,包括事件传递和响应机制,详细分析了hitTest、pointInside及touchesBegan等方法的调用流程,通过具体代码示例,解释了不同视图层级下事件如何被正确分发。

先看效果图

在这里插入图片描述

本文中,凡是看到xxx(),即表示xxx是一个函数或者方法!!!事件分为事件传递和事件响应,其中,事件响应又称事件处理。

具体代码

FindViewViewController的代码如下:

@interface FindViewViewController : UIViewController
@end
//--------分隔符,分隔.h文件和.m文件-------------
@interface FindViewViewController ()

@end

@implementation FindViewViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.view.backgroundColor = UIColor.whiteColor;
    
    //创建view层次
    [self createViewHierachy];
}

- (void)viewDidAppear:(BOOL)animated {
    [self logResponderChain];
}
//通过UIResponder来连接组成响应链,打印响应链:
- (void)logResponderChain {
    
    NSLog(@"响应链");
    
}

- (void)createViewHierachy {
    GrayView *grayView = [[GrayView alloc] initWithFrame:CGRectMake(50.f, 100.f, 260.f, 200.f)];
    RedView *redView = [[RedView alloc] initWithFrame:CGRectMake(0.f, 0.f, 120.f, 100.f)];
    BlueView *blueView = [[BlueView alloc] initWithFrame:CGRectMake(140.f, 100.f, 100.f, 100.f)];
    YellowView *yellowView = [[YellowView alloc] initWithFrame:CGRectMake(50.f, 360.f, 200.f, 200.f)];
    
    [self.view addSubview:grayView];
    [grayView addSubview:redView];
    [grayView addSubview:blueView];
    [self.view addSubview:yellowView];
}

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

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

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

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

UIView+Color的代码如下:

@interface UIView (Color)
- (NSString *)bgColorString;
@end

//--------分隔符,分隔.h文件和.m文件-------------
@implementation UIView (Color)
- (NSString *)bgColorString {
    if (self.backgroundColor == [UIColor redColor]) {
        return @"redColorView";
    } else if (self.backgroundColor == UIColor.blueColor) {
        return @"blueColorView";
    } else if (self.backgroundColor == UIColor.yellowColor) {
        return @"yellowColorView";
    } else if (self.backgroundColor == UIColor.lightGrayColor) {
        return @"lightGrayColorView";
    }
    return nil;
}
@end

GrayView的代码如下:

@interface GrayView : UIView
@end

//--------分隔符,分隔.h文件和.m文件-------------
#import "UIView+Color.h"

@implementation GrayView
- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.backgroundColor = UIColor.lightGrayColor;
    }
    return self;
}
//返回的是 点击区域是否在该view的范围内。
//返回YES表示点击区域在当前view中,NO表示点击区域不在当前view中
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"%@, %s", self.bgColorString, __func__);
    return [super pointInside:point withEvent:event];
}
//返回的是处理事件的view的实例。
//如果返回自己,表示处理事件的是当前view。返回nil表示当前view及其子view都不处理事件。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"%@, %s", self.bgColorString, __func__);
    return [super hitTest:point withEvent:event];
}

//scrollView 系统会重写它的super touch:导致不能往上传递,
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"%@, %s", self.bgColorString, __func__);
    [super touchesBegan:touches withEvent:event];
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"%@, %s", self.bgColorString, __func__);
    [super touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"%@, %s", self.bgColorString, __func__);
    [super touchesEnded:touches withEvent:event];
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"%@, %s", self.bgColorString, __func__);
    [super touchesCancelled:touches withEvent:event];
}
@end

BlueView、RedView和YellowView的代码和上面的GrayView的代码相同,只是类名不一样,这里就不贴出来了。

操作场景

  1. 点击蓝色的按钮,输出结果如下:
yellowColorView, -[YellowView hitTest:withEvent:]
yellowColorView, -[YellowView pointInside:withEvent:]
lightGrayColorView, -[GrayView hitTest:withEvent:]
lightGrayColorView, -[GrayView pointInside:withEvent:]
blueColorView, -[BlueView hitTest:withEvent:]
blueColorView, -[BlueView pointInside:withEvent:]
blueColorView, -[BlueView touchesBegan:withEvent:]
lightGrayColorView, -[GrayView touchesBegan:withEvent:]
vc, -[FindViewViewController touchesBegan:withEvent:]
blueColorView, -[BlueView touchesEnded:withEvent:]
lightGrayColorView, -[GrayView touchesEnded:withEvent:]
vc, -[FindViewViewController touchesEnded:withEvent:]
  • 事件传递:hitTest()和pointInSide()的调用顺序是:YellowView->GrayView->BlueView,因为BlueView包含了你所点击的区域,并且BlueView没有subviews,所以BlueView的hitTest()返回的是self,即返回自身到GrayView,GrayView的hitTest()返回的是BlueView实例。
  • 事件响应:touchesBegan()的调用顺序是:BlueView->GrayView。 touchesEnded()的调用顺序是:BlueView->GrayView;

点击红色的按钮,输出结果如下:

yellowColorView, -[YellowView hitTest:withEvent:]
yellowColorView, -[YellowView pointInside:withEvent:]
lightGrayColorView, -[GrayView hitTest:withEvent:]
lightGrayColorView, -[GrayView pointInside:withEvent:]
blueColorView, -[BlueView hitTest:withEvent:]
blueColorView, -[BlueView pointInside:withEvent:]
redColorView, -[RedView hitTest:withEvent:]
redColorView, -[RedView pointInside:withEvent:]
redColorView, -[RedView touchesBegan:withEvent:]
lightGrayColorView, -[GrayView touchesBegan:withEvent:]
redColorView, -[RedView touchesEnded:withEvent:]
lightGrayColorView, -[GrayView touchesEnded:withEvent:]
  • 事件传递::hitTest()和pointInSide()的调用顺序是:YellowView->GrayView->BlueView->RedView,因为RedView包含了你所点击的区域,并且BlueView没有subviews,所以BlueView的hitTest()返回的是self,即返回自身到GrayView,GrayView的hitTest()返回的是RedView实例。

  • 事件响应:touchesBegan()的调用顺序是:RedView->GrayView->FindViewViewController。 touchesEnded()的调用顺序是:BlueView->GrayView->FindViewViewController;

    总结: hitTest()和pointInSide()的传递机制:假设父view是A,而view B和view C都是A的子view,B先被A 添加,然后C才被A添加。当点击C时,先调用A的hitTest(),A的hitTest()里面调用[super hitTest],[super hitTest]接着调用A的pointInSide(),如果pointInSide()返回YES,说明点击位置在A里面。这里假设A的pointInSide()返回YES,A的[super hitTest]里面会逆序遍历A的子view,即先调用C的hitTest()和pointInSide()。此时的情况1:如果C的hitTest()返回了一个view实例(比如C自己的实例),那么[super hitTest]里面就会停止遍历,然后返回这个view实例,所以此时是不会调用B的hitTest()和pointInSide(),即说明该view实例作为事件的处理对象。情况2:如果C的hitTest()返回了nil,则A的[super hitTest]继续遍历到,然后调用B的hitTest()和pointInSide()。如果B返回一个实例(比如B自己的实例或者B的一个子孙实例),接着A的[super hitTest]会返回该实例,即说明该实例作为事件的处理对象。

    上面的总结用伪代码表示如下:

//A的[super hitTest]方法的伪代码, 即UIView里面的hitTest()的伪代码
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
	if (self.alpha <= 0.01 || !self.userInteractionEnabled || self.hidden) {
        return nil;
    }
    
    if ([self pointInside:point withEvent:event]) {
        NSArray<UIView *> *subviewss = [[[self subviews] reverseObjectEnumerator] allObjects];
        for (UIView *subview in subviewss) {
            CGPoint convertedPoint = [self convertPoint:point toView:subview];
            UIView *targetView = [subview hitTest:convertedPoint withEvent:event];
            if (targetView) {
                return targetView; //点击区域在子view中的某个view(可以是子view的子view的子view...),返回的是该view的实例
            }
        }
        return self; //执行到这里说明点击区域在自己这里,而不是在子view里,所以返回的是自己
    }
    return nil; //执行到这里说明点击区域不在自己范围内
}

总结:

  • pointInside: withEvent: 这个方法的作用只是判断点击区域是否在当前view的范围内。

  • hitTest: withEvent: 这个方法的返回值很重要,这个返回值表示处理事件的实例。即这个返回值及其父view、父view的父view(直到View的VC)的touchesBegan()、touchesMoved()、touchesEnded()、touchesCancelled()依次会被调用。举例,假设VC的根view有子view A,A有子view B,B有子view C,手机触摸屏幕、滑动、然后手指离开屏幕(一个事件序列)。当手指触摸屏幕时(任意位置,不一定是触摸View A的所在区域),先调用A的hitTest: withEvent:方法。 如果A的hitTest: withEvent:方法返回的是C的实例,则方法的调用顺序是C的touchesBegan:withEvent:方法->B的touchesBegan:withEvent:方法->A的touchesBegan:withEvent:方法->VC的根view的touchesBegan:withEvent:方法->VC的touchesBegan:withEvent:方法。此时手指在滑动时,方法的调用顺序是C的touchesMoved:withEvent:方法->B的touchesMoved:withEvent:方法->A的touchesMoved:withEvent:方法->VC的根view的touchesMoved:withEvent:方法->VC的touchesMoved:withEvent:方法。此时手指离开屏幕时,方法的调用顺序是C的touchesEnded:withEvent:方法->B的touchesEnded:withEvent:方法->A的touchesEnded:withEvent:方法->VC的根view的touchesEnded:withEvent:方法->VC的touchesEnded:withEvent:方法。

  • 事件响应传递: 你自定的view(直接继承自UIView)里面写的[super touchesBegan:touches withEvent:event];[super touchesMoved:touches withEvent:event];[super touchesEnded:touches withEvent:event];[super touchesCancelled:touches withEvent:event];里面都分别调用当前view对象的[self.nextResponder touchesBegan:touches withEvent:event[self.nextResponder touchesMoved:touches withEvent:event];[self.nextResponder touchesEnded:touches withEvent:event];[self.nextResponder touchesCancelled:touches withEvent:event];。但如果你的自定义view继承自UIButton,此时你自定的view里面写的[super touchesBegan:touches withEvent:event];[super touchesMoved:touches withEvent:event];[super touchesEnded:touches withEvent:event];[super touchesCancelled:touches withEvent:event];里面都不会调用当前view对象的[self.nextResponder touchesBegan:touches withEvent:event[self.nextResponder touchesMoved:touches withEvent:event];[self.nextResponder touchesEnded:touches withEvent:event];[self.nextResponder touchesCancelled:touches withEvent:event];。因为UIButton是UIControl(该类是UIView的子类)的子类,所以UIButton的事件不会在响应链中传递。

  • 事件响应传递中遇到UIControl: 先看下面的类图。在事件响应的传递过程中,如果中途遇到一个继承与UIControl的子view,那么该子view就会终止事件响应的传递。即该子view的[super touches等4个方法]里面不会调用self.nextResponder的对应的touches等4个方法。因为UIControl类重写了touchesBegan:touches withEvent:eventtouchesMoved:touches withEvent:eventtouchesEnded:touches withEvent:eventtouchesCancelled:touches withEvent:event4个方法,在重写的方法里面并不会调用self.nextResponder的对应的touches等4个方法。
    在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值