美文网首页ios进阶
iOS 触摸事件相关知识总结

iOS 触摸事件相关知识总结

作者: 恋空K | 来源:发表于2019-11-05 19:05 被阅读0次

    1.触摸事件和手势相关知识?

    iOS 的事件分为三种,触摸事件(Touch Event)、加速器事件(Motion Events)、远程遥控事件(Remote Events)。这些事件对应的类为UIResponder

    事件传递步骤:

    (有个有趣的地方,UIApplication和AppDelegate也继承于UIResponder)

    简单地说,自下而上。AppDelegate -> UIApplication -> UIWindow -> UIViewController -> UIView(父view一直遍历到子view,同层的view按后添加的view先遍历)。其遵循的规则如下:

    自己是否能接收触摸事件?

    不能接收的情况有三种

    一、userInteractionEnabled = NO

    二、 hidden = YES

    三、 alpha = 0.0 ~ 0.01

    触摸点是否在自己身上?

    1.从后往前遍历子控件,重复前两个步骤。

    2.若父控件不能接收触摸事件,不会传递给子控件。

    3.如果没有符合条件的子控件,那么就自己最适合处理。

    当事件传递给当前view时,当前view会调用- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event方法。寻找最适合的view。

    返回谁,谁就是最合适的view。

    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

        // 如果在这里直接返回yellowView

        // 区域A的事件也会由B响应

        // 所以这里还是直接调用父类方法

        return [super hitTest:point withEvent:event];

        // 本Demo中,甚至控制器view的点击事件也会被B响应,因为控制器view会遍历子控件最后一个(红色view),红色view调用这方法返回yellowView

    }

    事件响应步骤:

    UIResponder -----> - (nullable UIResponder*)nextResponder;通过这个方法可以获取到当前view的控制器

    @interface UIResponder : NSObject <UIResponderStandardEditActions>

    @property(nonatomic, readonly, nullable) UIResponder *nextResponder;

    - (nullable UIResponder*)nextResponder;

    @property(nonatomic, readonly) BOOL canBecomeFirstResponder;    // default is NO

    - (BOOL)canBecomeFirstResponder;    // default is NO

    - (BOOL)becomeFirstResponder;

    @property(nonatomic, readonly) BOOL canResignFirstResponder;    // default is YES

    - (BOOL)canResignFirstResponder;    // default is YES

    - (BOOL)resignFirstResponder;

    @property(nonatomic, readonly) BOOL isFirstResponder;

    - (BOOL)isFirstResponder;

    // 触摸事件方法

    // 手指触摸

    - (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;

    - (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);

    @end

    UITouch

    UITouch对象记录 触摸的位置、时间、阶段。

    一根手指对应一个UITouch对象。

    手指移动时,系统会更新同一个UITouch对象。

    手指离开屏幕时,UITouch对象被销毁。

    @interface UITouch : NSObject

    // 触摸产生时所处的窗口

    @property (nonatomic, readonly, retain) UIWindow *window;

    // 触摸产生时所处的视图

    @property (nonatomic, readonly, retain) UIView *view;

    // 短时间内点按屏幕的次数

    @property (nonatomic, readonly) NSUInteger tapCount;

    // 记录了触摸事件产生或变化的时间,单位:秒

    @property (nonatomic, readonly) NSTimeInterval timestamp;

    // 当前触摸事件所处的状态

    @property (nonatomic, readonly) UITouchPhase phase;

    typedef NS_ENUM(NSInteger, UITouchPhase) {

        UITouchPhaseBegan,            //(触摸开始)

        UITouchPhaseMoved,            // (接触点移动)

        UITouchPhaseStationary,        // (接触点无移动)

        UITouchPhaseEnded,            // (触摸结束)

        UITouchPhaseCancelled,        // (触摸取消)

    };

    // 返回触摸在view上的位置

    // 相对view的坐标系

    // 如果参数为nil,返回的是在UIWindow的位置

    - (CGPoint)locationInView:(nullable UIView *)view;

    // 返回上一个触摸点的位置

    - (CGPoint)previousLocationInView:(nullable UIView *)view;

    @end

    UIEvent

    每产生一个事件,就会产生一个UIEvent对象。记录事件产生的时刻和类型。本文探究的都是触摸事件。

    响应链 --->简单地说,传递到最合适的view后,如果有实现touches方法那么就由此 View 响应,如果没有实现,那么就会自下而上,传递给他的下一个响应者【子view -> 父view,控制器view -> 控制器-> UIWindow -> UIApplication -> AppDelegate】。

    由这两张图,我们就可以知道每个UIResponder对象的nextResponder指向谁。

    手势

    手势识别和触摸事件是两个独立的概念。

    UIResponder

    UIResponder是iOS中用于处理用户事件的API,可以处理触摸事件、按压事件(3D touch)、远程控制事件、硬件运动事件。可以通过touchesBegan、pressesBegan、motionBegan、remoteControlReceivedWithEvent等方法,获取到对应的回调消息。UIResponder不只用来接收事件,还可以处理和传递对应的事件,如果当前响应者不能处理,则转发给其他合适的响应者处理。

    应用程序通过响应者来接收和处理事件,响应者可以是继承自UIResponder的任何子类,例如UIView、UIViewController、UIApplication等。当事件来到时,系统会将事件传递给合适的响应者,并且将其成为第一响应者。

    第一响应者未处理的事件,将会在响应者链中进行传递,传递规则由UIResponder的nextResponder决定,可以通过重写该属性来决定传递规则。当一个事件到来时,第一响应者没有接收消息,则顺着响应者链向后传递。

    查找第一响应者

    基础API

    查找第一响应者时,有两个非常关键的API,查找第一响应者就是通过不断调用子视图的这两个API完成的。

    调用方法,获取到被点击的视图,也就是第一响应者。

    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;

    hitTest:withEvent:方法内部会通过调用这个方法,来判断点击区域是否在视图上,是则返回YES,不是则返回NO。

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

    查找第一响应者

    应用程序接收到事件后,将事件交给keyWindow并转发给根视图,根视图按照视图层级逐级遍历子视图,并且遍历的过程中不断判断视图范围,并最终找到第一响应者。

    视图的hidden等于YES。

    视图的alpha小于等于0.01。

    视图的userInteractionEnabled为NO。

    如果点击事件是发生在视图外,但在其子视图内部,子视图也不能接收事件并成为第一响应者。这是因为在其父视图进行hitTest:withEvent:的过程中,就会将其忽略掉。

    事件传递

    传递过程

    UIApplication接收到事件,将事件传递给keyWindow。

    keyWindow遍历subViews的hitTest:withEvent:方法,找到点击区域内合适的视图来处理事件。

    UIView的子视图也会遍历其subViews的hitTest:withEvent:方法,以此类推。

    直到找到点击区域内,且处于最上方的视图,将视图逐步返回给UIApplication。

    在查找第一响应者的过程中,已经形成了一个响应者链。

    应用程序会先调用第一响应者处理事件。

    如果第一响应者不能处理事件,则调用其nextResponder方法,一直找响应者链中能处理该事件的对象。

    最后到UIApplication后仍然没有能处理该事件的对象,则该事件被废弃。

    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

        if (self.alpha <= 0.01 || self.userInteractionEnabled == NO || self.hidden) {

            return nil;

        }

        BOOL inside = [self pointInside:point withEvent:event];

        if (inside) {

            NSArray *subViews = self.subviews;

            // 对子视图从上向下找

            for (NSInteger i = subViews.count - 1; i >= 0; i--) {

                UIView *subView = subViews[i];

                CGPoint insidePoint = [self convertPoint:point toView:subView];

                UIView *hitView = [subView hitTest:insidePoint withEvent:event];

                if (hitView) {

                    return hitView;

                }

            }

            return self;

        }

        return nil;

    }

    如上图所示,响应者链如下:

    如果点击UITextField后其会成为第一响应者。

    如果textField未处理事件,则会将事件传递给下一级响应者链,也就是其父视图。

    父视图未处理事件则继续向下传递,也就是UIViewController的View。

    如果控制器的View未处理事件,则会交给控制器处理。

    控制器未处理则会交给UIWindow。

    然后会交给UIApplication。

    最后交给UIApplicationDelegate,如果其未处理则丢弃事件。

    事件通过UITouch进行传递,在事件到来时,第一响应者会分配对应的UITouch,UITouch会一直跟随着第一响应者,并且根据当前事件的变化UITouch也会变化,当事件结束后则UITouch被释放。

    UIViewController没有hitTest:withEvent:方法,所以控制器不参与查找响应视图的过程。但是控制器在响应者

    注意

    在执行hitTest:withEvent:方法时,如果该视图是hidden等于NO的那三种被忽略的情况,则改视图返回nil。

    如果当前视图在响应者链中,但其没有处理事件,则不考虑其兄弟视图,即使其兄弟视图和其都在点击范围内。

    UIImageView的userInteractionEnabled默认为NO,如果想要UIImageView响应交互事件,将属性设置为YES即可响应事件。

    事件控制

    事件拦截 --- > 有时候想让指定视图来响应事件,不再向其子视图继续传递事件,可以通过重写hitTest:withEvent:方法。在执行到方法后,直接将该视图返回,而不再继续遍历子视图,这样响应者链的终端就是当前视图。

    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

        return self;

    }

    事件转发

    在开发过程中,经常会遇到子视图显示范围超出父视图的情况,这时候可以重写该视图的pointInside:withEvent:方法,将点击区域扩大到能够覆盖所有子视图。

    事件逐级传递

    如果想让响应者链中,每一级UIResponder都可以响应事件,可以在每级UIResponder中都实现touches并调用super方法,即可实现响应者链事件逐级传递。

    只不过这并不包含UIControl子类以及UIGestureRecognizer的子类,这两类会直接打断响应者链。

    Gesture Recognizer

    如果有事件到来时,视图有附加的手势识别器,则手势识别器优先处理事件。如果手势识别器没有处理事件,则将事件交给视图处理,视图如果未处理则顺着响应者链继续向后传递。

    当响应者链和手势同时出现时,也就是既实现了touches方法又添加了手势,会发现touches方法有时会失效,这是因为手势的执行优先级是高于响应者链的。

    事件到来后先会执行hitTest和pointInside操作,通过这两个方法找到第一响应者,这个在上面已经详细讲过了。当找到第一响应者并将其返回给UIApplication后,UIApplication会向第一响应者派发事件,并且遍历整个响应者链。如果响应者链中能够处理当前事件的手势,则将事件交给手势处理,并调用touches的calcelled方法将响应者链取消。

    在UIApplication向第一响应者派发事件,并且遍历响应者链查找手势时,会开始执行响应者链中的touches系列方法。会先执行touchesBegan和touchesMoved方法,如果响应者链能够继续响应事件,则执行touchesEnded方法表示事件完成,如果将事件交给手势处理则调用touchesCancelled方法将响应者链打断。

    根据苹果的官方文档,手势不参与响应者链传递事件,但是也通过hitTest的方式查找响应的视图,手势和响应者链一样都需要通过hitTest方法来确定响应者链的。在UIApplication向响应者链派发消息时,只要响应者链中存在能够处理事件的手势,则手势响应事件,如果手势不在响应者链中则不能处理事件。

    UIControl

    根据上面的手势和响应者链的处理规则,我们会发现UIButton或者UISlider等控件,并不符合这个处理规则。UIButton可以在其父视图已经添加tapGestureRecognizer的情况下,依然正常响应事件,并且tap手势不响应。

    以UIButton为例,UIButton也是通过hitTest的方式查找第一响应者的。区别在于,如果UIButton是第一响应者,则直接由UIApplication派发事件,不通过Responder Chain派发。如果其不能处理事件,则交给手势处理或响应者链传递。

    不只UIButton是直接由UIApplication派发事件的,所有继承自UIControl的类,都是由UIApplication直接派发事件的。

    小技巧

    在开发中,有时会有找到当前View对应的控制器的需求,这时候就可以利用我们上面所学,根据响应者链来找到最近的控制器。

    在UIResponder中提供了nextResponder方法,通过这个方法可以找到当前响应环节的上一级响应对象。可以从当前UIView开始不断调用nextResponder,查找上一级响应者链的对象,就可以找到离自己最近的UIViewController。

    示例代码:

    - (UIViewController *)parentController {

      UIResponder *responder = [self nextResponder];

      while (responder) {

          if ([responder isKindOfClass:[UIViewController class]]) {

              return (UIViewController *)responder;

          }

          responder = [responder nextResponder];

      }

      return nil;

    }

    相关文章

      网友评论

        本文标题:iOS 触摸事件相关知识总结

        本文链接:https://www.haomeiwen.com/subject/gdtluctx.html