iOS开发 -- 知识点杂记

iOS - 事件的传递与响应

2022-11-17  本文已影响0人  Lcr111

前言

最近新项目需要用到自定义Tabbar按钮样式,如下图所示的需求,就需要用到hitTest等相关的一些点击事件处理知识点。本文只进行浅浅的探究,记录下在实现过程中涉及到的东西。


事件处理001

传递链

在 UIKit 中我们使用响应者对象(Responder)接收和处理事件。一个响应者对象一般是 UIResponder 类的实例,它常见的子类包括 UIViewUIViewControllerUIApplication,这意味着几乎所有我们日常使用的控件都是响应者,如 UIButtonUILabel 等等。
UIResponder及其子类中,我们是通过重写相关触摸(UITouch)的方法来处理和传递事件(UIEvent):

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

当有人触摸屏幕时,我们需要找出最适合对此次触摸事件做出响应的控件,也就是寻找到第一响应者,最终的响应者追溯祖先路径中的每个类组成的链就是所谓的传递链
在触摸发生后,UIApplication 会触发 func sendEvent(_ event: UIEvent) 将一个封装好的 UIEvent 传给 UIWindow,也就是当前展示的 UIWindow,通常情况接下来会传给当前展示的 UIViewController,接下来传给 UIViewController 的根视图UIView,如果UIView还有子View的话,会循环遍历所有子View,直到找到最适合响应此次触摸事件的响应者(UIResponder实例)。

事件处理002

如上图对应的传递链:UIApplication-->UIWindow-->UIViewController-->UIViewController.view->SuperView-->View...

响应链

在找到合适的视图控件后,就会调用视图控件的 touches 方法来作事件的具体处理:
touchesBegin,touchesMoved,touchesEnded等。如果找到了最合适的响应者,但是如果其没有实现touches方法,就会调用其上一个响应者对象的touches方法,依次往前推。
如果父视图在不具备响应事件的条件下,子视图也不能响应当前事件:

事件的响应顺序刚好与事件的传递顺序 相反:View-->SuperView-->UIViewController.view-->UIViewController-->UIWindow->UIApplication-->事件丢失
也就是说如果最终UIApplication还处理不了,则该事件将会被丢弃。

事件处理003

hitTest

那么要找出最适合的view来响应触摸事件,我们就需要用到hitTest: withEvent:方法。
hitTest: withEvent:是UIView里面的一个方法,该方法的作用在于:在视图的层次结构中寻找一个最适合的view来响应触摸时间。该方法如果返回nil,即事件有可能被丢弃,否则返回最适合的那个view。
下面我们通过几个例子来帮助我们理解hitTest的追溯过程:

事件处理004

如上图,把蓝色layer添加到红色layer(RedView实例.layer)上,通过点击蓝色区域和红色区域,看看响应者是谁:

    self.blueLayer = [CALayer layer];
    self.blueLayer.frame = CGRectMake(0, 0, 100, 100);
    self.blueLayer.backgroundColor = [UIColor blueColor].CGColor;
    [self.redLayer.layer addSublayer:self.blueLayer];
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //1.get touchu position relative to main view(获取相对于主视图的触摸位置)
    CGPoint point = [[touches anyObject]locationInView:self.view];
    //2.get touched layer
    CALayer *layer = [self.layerView.layer hitTest:point];
    //3.get layer using using hitTest
    if(layer == self.blueLayer)
    {
        NSLog(@"Inside Blue layer");
    }else if(layer == self.layerView.layer)
    {
        NSLog(@"Inside Red layer");
    }
}
#import "RedView.h"
@implementation RedView
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // 如果交互未打开,或者透明度小于0.05 或者 视图被隐藏
    if (self.userInteractionEnabled == NO || self.alpha < 0.05 || self.hidden == YES)
    {
         return nil;
    }
    // 如果 touch 的point 在 self 的bounds 内
    if ([self pointInside:point withEvent:event])
    {
        for (UIView *subView in self.subviews)
        {
            //进行坐标转化
            CGPoint coverPoint = [subView convertPoint:point fromView:self];
            // 调用子视图的 hitTest 重复上面的步骤。找到了,返回hitTest view ,没找到返回有自身处理
            UIView *hitTestView = [subView hitTest:coverPoint withEvent:event];
            if (hitTestView)
            {
                return hitTestView;
            }
        }
        return self;
    }
    return nil;
}
运行,分别点击蓝色和红色区域,打印结果: 事件处理005

思路讲解:

  1. 首先在当前视图的hitTest方法中调用pointInside方法判断触摸点是否在当前视图内
  2. 若pointInside方法返回NO,说明触摸点不在当前视图内,则当前视图的hitTest返回nil,该视图不处理该事件
  3. 若pointInside方法返回YES,说明触摸点在当前视图内,则从最上层子视图开始(即从subviews数组的末尾向前遍历),遍历当前视图的所有子视图,调用子视图的hitTest方法重复步骤1~3
  4. 直到有子视图的hitTest方法返回非空对象或者全部子视图遍历完毕
  5. 若第一次有子视图的hitTest方法返回非空对象,则当前视图的hitTest方法就返回此对象,处理结束
  6. 若所有子视图的hitTest方法都返回nil,则当前视图的hitTest方法返回当前视图本身,最终由该对象处理触摸事件

项目例子

我的项目中如前言部分的图片所示,需要自定义一个大按钮的tabbar,关键代码如下:

-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if (self.isHidden == NO) {
        CGPoint newA = [self convertPoint:point toView:self.middleView];
        if ([self.circleBtn pointInside:newA withEvent:event]) {
            return _circleBtn;
        }
    }
    return [super hitTest:point withEvent:event];
}

在自定义的UITabBar子类中,重写hitTest方法,然后将触摸点的坐标转换到大按钮上,如果pointInside方法返回YES,则当前点击区域为大按钮内。

项目中还用到了为一个按钮增大点击范围,比如有时候按钮很小,点击范围小,所以为了提升用户体验感,需要在按钮周围增加一个点击范围来触发。所以在UIButton的分类中添加增大方法setEnlargeEdge

static char topNameKey;
static char rightNameKey;
static char bottomNameKey;
static char leftNameKey;

- (void)setEnlargeEdge:(CGFloat)size
{
    objc_setAssociatedObject(self, &topNameKey, [NSNumber numberWithFloat:size], OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, &rightNameKey, [NSNumber numberWithFloat:size], OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, &bottomNameKey, [NSNumber numberWithFloat:size], OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, &leftNameKey, [NSNumber numberWithFloat:size], OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (CGRect)enlargedRect
{
    NSNumber* topEdge = objc_getAssociatedObject(self, &topNameKey);
    NSNumber* rightEdge = objc_getAssociatedObject(self, &rightNameKey);
    NSNumber* bottomEdge = objc_getAssociatedObject(self, &bottomNameKey);
    NSNumber* leftEdge = objc_getAssociatedObject(self, &leftNameKey);
    if (topEdge && rightEdge && bottomEdge && leftEdge)
    {
        return CGRectMake(self.bounds.origin.x - leftEdge.floatValue,
                          self.bounds.origin.y - topEdge.floatValue,
                          self.bounds.size.width + leftEdge.floatValue + rightEdge.floatValue,
                          self.bounds.size.height + topEdge.floatValue + bottomEdge.floatValue);
    }
    else
    {
        return self.bounds;
    }
}

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
    CGRect rect = [self enlargedRect];
    if (CGRectEqualToRect(rect, self.bounds))
    {
        return [super hitTest:point withEvent:event];
    }
    return CGRectContainsPoint(rect, point) ? self : nil;
}  

其中,topNameKeyrightNameKeybottomNameKeyleftNameKey为静态变量,外部调用setEnlargeEdge方法即可。

事件处理就写到这里,不足之处望海涵,也可在评论区与我分享你的观点。洗洗睡了~

上一篇 下一篇

猜你喜欢

热点阅读