iOS-触摸事件分发及响应详解

iOS-触摸事件分发及响应详解,第1张

原理认识 触摸事件 UIEvent

一个UIEvent事件的定义为:第一根手指触摸屏幕到最后一根手指离开屏幕。它记录了事件发生的类型,时间以及多个UITouch事件。

@property(nonatomic,readonly) UIEventType type NS_AVAILABLE_IOS(3_0); //事件的类型

@property(nonatomic,readonly) UIEventSubtype subtype NS_AVAILABLE_IOS(3_0);

@property(nonatomic,readonly) NSTimeInterval timestamp; //事件的时间

@property(nonatomic, readonly, nullable) NSSet  *allTouches; //事件包含的touch对象
UITouch

一个UITouch表示表示一根手指与屏幕的交互,它在一根手指触摸屏幕时被创建,这手指离开屏幕时被销毁,同时通过各种属性记录这跟手指交互的信息。

@property(nonatomic,readonly) NSTimeInterval timestamp; //时间

@property(nonatomic,readonly) UITouchPhase phase; //状态,例如begin,move,end,cancel

@property(nonatomic,readonly) NSUInteger tapCount; // 短时间内单击的次数

@property(nonatomic,readonly) UITouchType type NS_AVAILABLE_IOS(9_0); //类型

@property(nonatomic,readonly) CGFloat majorRadius NS_AVAILABLE_IOS(8_0); //触摸半径

@property(nonatomic,readonly) CGFloat majorRadiusTolerance NS_AVAILABLE_IOS(8_0);

@property(nullable,nonatomic,readonly,strong) UIWindow *window; //触摸所在窗口

@property(nullable,nonatomic,readonly,strong) UIView *view; //触摸所在视图

@property(nullable,nonatomic,readonly,copy) NSArray  *gestureRecognizers NS_AVAILABLE_IOS(3_2); //正在接收该触摸对象的手势识别器

@property(nonatomic,readonly) CGFloat force NS_AVAILABLE_IOS(9_0); //触摸的力度
UIEvent响应

系统的UIResponder类提供了一系列API用于响应触摸事件

- (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; //触摸被中断,例如触摸时电话呼入
事件的传递

当事件产生后系统就会从顶部视图开始递归向下找到最内部的子view,通过判断点击位置是否在视图frame内,寻找最合适响应事件的对象,如果没有合适对象该事件就会被丢弃。

如何寻找第一个可以响应事件的对象?

通过hitTest方法按照视图层级,Application接收到系统传来事件时调用 sendEvent: 发送事件给UIWindow,UIWindow开始传递事件,通过hitTest方法递归遍历返回最合适响应该事件的view。

hitTest判断是否返回视图有哪些条件?

判断当前视图的子视图中存在或自身适合处理该事件的对象条件有三个:

1、当前视图userInteractionEnabled属性为YES

2、当前视图hidden属性为NO且alpha值大于0.01

3、事件的坐标在当前视图热区内

满足三个条件后时会对所有子视图遍历调用hitTest,若遍历结束后返回nil或当前视图无子视图则返回自身,否则返回子视图

遍历子视图的顺序?

简单写一个demo,视图添加顺序、层级以及遍历顺序如下树状图所示。

同一层级从左到右为视图添加顺序,最右侧为最早添加视图。从上到下为视图层级顺序,上方为父视图。

图中可知,hitTest:返回视图是通过后序递归遍历方法,先左后右最后根。

事件的响应

找到合适响应当前事件的视图后,UIWindow将事件发送给视图并调用touchBegan:、touchesMoved:等一系列方法对事件做出相应的处理,进行事件响应。

响应链 通过UIResponder中的nextResponder属性,按视图层级将app串成一条(多条)虚拟链。当viewController初始化时,根视图的nextResponder会别指向改viewController,viewController的nextResponder为根视图的父视图。当一个view添加到另一个view上时,该view的nextResponder为父视图。在事件传递步骤中,递归查找第一个响应的视图时形成,根据视图添加层级,UIApplication在响应链最顶部,事件传递最终返回的视图为响应链最底部。 手势识别器

UIGestureRecognize为手势识别器的父类,该类没有继承UIResponder,也不参与响应链。

工作机制

事件传递到一个视图时,如果该视图绑定了手势识别器优先进行手势识别,再进行touchBegan: withEvent: 方法调用,并且在手势识别成功时取消事件传递,调用响应该事件对象的touchsCancelled:withEvent: 方法。

只有当手势识别失败的时候,才会调用touchEnd方法。手势识别过程中,UIRespnoder 及 UIGestureRecognize 的 touchBegan、touchMoved 等方法正常调用。调用 touchCancle 之后的 UITouch 对象失去第一响应视图的持有

工作流程

手势的传递不依赖响应链,事件通过记录并直接调用手势识别器的方法达到目的。hitTest:遍历过程中获取响应链上的所有手势,遍历结束后通过 shouldReceiveTouch 等方法确定手势是否可接受当前事件,并将满足条件的手势记录在 UITouch 的数组中。

GestureRecognize 在添加进数组后会首先调用 touchBegan (上边提到的优先识别,touchMove 等方法优先级高于 UIResponder 对象),之后通过 shouldRequireFailureOfGestureRecognizer 等方法进行手势优先级排序 (即添加手势识别器之间的依赖,如一个手势失败时该手势才能响应)

------------------

//每次尝试识别时调用一次,可以延迟手势识别的失败判断,并且可以跨视图层次结构在识别器之间设置

返回 YES 可以在gestureRecognizer 和otherGestureRecognizer 之间设置动态失败条件

手势类型识别成功后,响应链上的 UIRespnoder 对象依次调用 gestureRecognizerShouldBegin: shouldRecognizeSimultaneouslyWithGestureRecognizer: 方法确认最终可执行的识别器,并从数组中移除其余识别器 (非持续型识别器不会变换为 cancle 等状态,持续型识别器会更新为 Fail)。

--------------------

//当一个手势识别器尝试从 Possible 状态转换为其他状态时调用,返回NO会导致识别器的状态转换为 Failed

----------------

//当一个手势的识别被另一个手势阻塞时调用,返回YES可以让两个手势识别器同时识别,默认为NO

------------------

依次调用 UITouch 手势数组中剩余的手势识别器,更新手势状态并进行 target-action 调用,完成手势识别流程。

UIControl

重写了UIResponder中事件处理方法的特殊类。

1.UIControl 对象对触摸事件接收的优先级高于手势识别器

UIControl通过重写 gestureRecognizerShouldBegin: 拦截了点击类手势

当识别成功的手势为长按、拖动等持续型手势时,手势正常执行,不会被 UIControl 拦截。

2.重写了touchBegan: 等方法使事件不能继续传递,如图,UIResponder 的事件处理方法(touchBegan: 等)对应调用 beginTrackingWithTouch等方法。

3.通过 target-action 机制实现点击事件的处理,通过数组存储和管理多个 target-action 对象。

实践场景 场景1:一个事件,多个对象处理

由于UIResponder事件处理方法(touchBegan:withEvent等)的 *** 作是将事件上抛给nextResponder,当父视图与子视图均实现了事件处理方法并且调用父类处理方法时,即可实现一个事件多个对象处理。

场景2:多手势处理

手势代理方法 shouldRecognizeSimultaneouslyWithGestureRecognizer 返回YES即可使两个手势同时响应,当一个 GestureRecognizer 返回了YES之后即生效且在其他地方返回NO不会再使它失效。

该方法准确的理解应该为当一个手势识别时是否拦截另一个手势的识别,如果希望有三个手势同时执行,两两之间都应该返回YES。

场景3:多手势处理生效时使某一个手势失效

想要使手势失效通常可以使用 gestureRecognizerShouldBegin: 实现,当响应链下层某个手势实现了 shouldRecognizeSimultaneouslyWithGestureRecognizer: 时上层通过判断返回NO是没有用的,这个时候就可以使用 gestureRecognizerShouldBegin: 达到目的。

GestureRecognizer 在识别成功后会通过 gestureRecognizerShouldBegin: 返回的值判断是否应该执行,与第一响应视图绑定外的 GestureRecognizer ,该方法会执行两次,第一次调用第一响应视图的方法并将自身作为参数传入,返回NO则结束,否则再次调用与其绑定的视图的方法,两次都为YES时才能继续执行。

场景4:双重scrollView问题

当一个scrollViewA作为子视图添加到scrollViewB上时,会出现一些不符合预期的现象。

scrollViewA的 offset 为0或到达 contentSize 大小时,再次向内容外的方向拖动 scrollViewA 会导致 scrollViewB 滚动。 原因:scrollView中重写了部分方法封装出UIScrollViewPanGestureRecognizer,当 scrollView 的 offset 到达边界且父视图包含 scrollView 且满足可滚动等条件时,再次向边界外拖动时shouldBeRequiredToFailByGestureRecognizer: 将返回YES,导致实际生效手势为 scrollViewB 的拖动手势。开发中遇到过一个场景场景的ScrollView上添加 scrollView 时,拖动 scrollView 到内容边界不松手继续拖动也会导致内流视图滚动。

查看堆栈可以发现一个陌生的方法 [UIScrollView _attemptToDragParent:forNewBounds:oldBounds:],名字看起来很有嫌疑,汇编中可以看到方法中的判断条件与scrollEnable、pagingEnabled等有关,关闭后解决。

最后,手势识别器中有各种属性设置便于我们实现各种场景的使用,如取消延时识别等,有错误的地方欢迎大佬指出批评~

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/993363.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存