zoukankan      html  css  js  c++  java
  • 响应者链UIResponder-扩大UIButton的点击范围

    在开发中,我们经常看到有按钮等的点击,会出现响应事件。按钮->view->ViewController->UIWindow->UIApplication,这就形成了一个响应链。本篇将讲述响应链的具体底层实现,大约花费10-15分钟左右,欢迎点评!!!

    一、知识

    继承UIResponder的对象,我们称之为响应者对象。我们常用到的UIApplication、UIWindow、UIViewController以及所有继承UIView的UIKit都会根继承于UIResponder。

    UIResponder一般响应触摸事件,点按事件,加速事件以及远程控制事件:

    触摸事件(touch handling)
    - (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event;
    - (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event;
    - (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event;
    - (void)touchesCancelled:(NSSet *)touches withEvent:(nullable UIEvent *)event;
    - (void)touchesEstimatedPropertiesUpdated:(NSSet *)touches NS_AVAILABLE_IOS(9_1);
    点按事件(press handling) NS_AVAILABLE_IOS(9_0)
    - (void)pressesBegan:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
    - (void)pressesChanged:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
    - (void)pressesEnded:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
    - (void)pressesCancelled:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
    加速事件
    - (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
    - (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
    - (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
    远程控制事件
    - (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(4_0);

    响应者链:由多个响应者组合成的链条,我们就叫做响应者链,表示了响应者之间的联系。下面是响应者链关系图:

    假设我们触摸了initial View,

    1.第一响应者就是initial view 首先响应touchesBegan:withEvent:方法,然后传递给橘黄色的view

    2.橘黄色的view开始去响应touchesBegan:withEvent:方法,然后传递给蓝绿色View

    3.蓝绿色的view响应touchesBegan:withEvent:方法,然后传递给控制器的view

    4.控制器view开始响应touchesBegan:withEvent:方法,然后传递给窗口

    5.窗口再传递给application

    假如上述响应者都不处理该事件,那么事件就会被丢弃。

    事件的分发和传递

    1.当程序发生触摸事件后,系统会将事件加入到UIApplication管理的一个任务队列中,

    2.UIApplication将任务队列的最前端事件开始向下分发到UIWindow

    3.UIWindow将事件向下分发到UIView

    4.UIView首先看自己的是否处理事件,触摸点是否在自己的范围内,如果是,就开始继续找子视图

    5.遍历子控件,重复上述上两步

    6.如果没有找到,那么自己就是事件的处理者

    7.如果自己不能处理,不做任何的处理。

    >>>拓展

    UIView不会接受事件处理的情况:

    1)alpha < 0.01;

    2) userInteractionEnabled = NO

    3) hidden = YES

    从父控件到子控件找处理事件最合适的view的期间,如果父视图不接受处理,则不需要向下走了,则子视图也不会接收到事件。在事件的处理中,关键在于是否有最合适的View来处理和响应事件,如果遍历了最后都还没有找到最合适的view来接受事件,则会被丢弃。

    二、核心

    寻找最合适的View

    用到两个方法

    // 此方法返回的View是本次点击事件需要的最佳View
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
    
    // 判断一个点是否落在范围内
    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event

    事件传递给窗口或者控件的话,就会调用hitTest:withEvent:方法寻找最合适的view,如果子控件满足是合适的view,则在子控件会再次调用hitTest:withEvent:来查看子控件到底是不是最合适的子view,一直这样递归,找到最合适的view,如果最终还没有找到,就会废弃事件。

    思想可以用下面的代码表示:

    // 因为所有的视图类都是继承BaseView
    - (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;
       
    }

    判断触碰点是否在视图内

    判断一个点是不是在视图内,通过下面方法判断

    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event

    三、应用

    在我们实际的开发过程中,我们也会经常使用这个方法,但并不是很多人知道使用这个方法。

    • 使用这个方法增加按钮的点击范围
    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event {
        CGRect bounds = self.bounds;
        bounds = CGRectInset(bounds, -10, -10);
        // CGRectContainsPoint  判断点是否在矩形内
        return CGRectContainsPoint(bounds, point);
    }
    • 不规则的按钮点击区域(自定义按钮的点击范围)
    // // 改变图片的点击范围
    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
        
        // 控件范围宽度多40,高度20
        CGRect bounds = CGRectInset(self.bounds, -20, -20);
        NSLog(@"point = %@",NSStringFromCGPoint(point));
        UIBezierPath *path1 = [UIBezierPath bezierPathWithRect:CGRectMake(-20, 0, 40, 120)];
        UIBezierPath *path2 = [UIBezierPath bezierPathWithRect:CGRectMake(self.frame.size.width - 20, 0, 40, 120)];
        if (([path1 containsPoint:point] || [path2 containsPoint:point])&& CGRectContainsPoint(bounds, point)){
            //如果在path区域内,返回YES
            return YES;
        }
        return NO;
    }

    上面的增加UIButton的点击范围

    demo是以分类的方式的,利用runtime机制,核心代码如下: 

    我们来看看分类具体实现方式:
    (1).UIButton+UIButtonExpand.h

    #import <UIKit/UIKit.h>
    #import <objc/runtime.h>
    
    @interface UIButton (UIButtonExpand)
    - (void)expandButtonSize:(CGFloat)size;
    
    @end

    (2).UIButton+UIButtonExpand.m

    #import "UIButton+UIButtonExpand.h"
    
    static char expandSizeKey;
    
    
    @implementation UIButton (UIButtonExpand)
    - (void)expandButtonSize:(CGFloat)size{
        objc_setAssociatedObject(self, &expandSizeKey, [NSNumber numberWithFloat:size], OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    
    - (CGRect)expandRect{
        NSNumber *expandSize = objc_getAssociatedObject(self, &expandSizeKey);
        if (expandSize) {
            return CGRectMake(self.bounds.origin.x - expandSize.floatValue,
                              self.bounds.origin.y - expandSize.floatValue,
                              self.bounds.size.width + expandSize.floatValue + expandSize.floatValue,
                              self.bounds.size.height + expandSize.floatValue + expandSize.floatValue);
        } else {
            return self.bounds;
        }
    }
    
    
    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
        
        CGRect buttonRect = [self expandRect];
        if (CGRectEqualToRect(buttonRect, self.bounds)) {
            return [super pointInside:point withEvent:event];
        }
        return CGRectContainsPoint(buttonRect, point) ? YES : NO;
    }
    
    @end

    下面是demo的地址:https://github.com/zxy1829760/UIResponderDemo

    以上就是UIResponder和响应链的基本内容,欢迎大家指正。

  • 相关阅读:
    [POI2014]FarmCraft
    [POI2014]Solar Panels
    Luogu P2824 [HEOI2016/TJOI2016]排序
    CF903G Yet Another Maxflow Problem
    CF901C Bipartite Segments
    CF749E Inversions After Shuffle
    ARC068C Snuke Line
    BZOJ3747 [POI2015]Kinoman
    SA-IS
    简单字符串
  • 原文地址:https://www.cnblogs.com/guohai-stronger/p/9367366.html
Copyright © 2011-2022 走看看