chenzhao

  • java
  • iOS
  • IT
知识积累
不积跬步无以至千里
  1. 首页
  2. iOS
  3. 正文

iOS 事件处理(2)

2017年 12月 1日 81点热度 0人点赞 0条评论

接上面

具体例子

用自定义触摸事件来实现相关 UIGestureRecognizer。这一小节的详细代码可见Multitouch EventsListing 3-1至Listing 3-7。

处理点击手势

用 UITouch 的 tapCount 属性来判断是单击还是双击还是三击。最好是在 touchesEnded:withEvent: 方法里面做判断处理,因为它是用户手指离开 App 时才响应的,要确保它真的是个 tap 手势,而不是拖动啥的。


- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { 

} 

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { 

} 

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { 

for (UITouch *aTouch in touches) { 

if (aTouch.tapCount >= 2) { 

// The view responds to the tap 

[self respondToDoubleTapGesture:aTouch]; 

} 

} 

} 

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { 

} 

处理滑动和拖动手势

滑动手势

从三个角度判断它是否是一个滑动手势

  • Did the user’s finger move far enough?
  • Did the finger move in a relatively straight line?
  • Did the finger move quickly enough to call it a swipe?

具体代码如下:


#define HORIZ_SWIPE_DRAG_MIN 12 

#define VERT_SWIPE_DRAG_MAX 4 

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { 

UITouch *aTouch = [touches anyObject]; 

// startTouchPosition is a property 

self.startTouchPosition = [aTouch locationInView:self]; 

} 

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { 

} 

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { 

UITouch *aTouch = [touches anyObject]; 

CGPoint currentTouchPosition = [aTouch locationInView:self]; 

// Check if direction of touch is horizontal and long enough 

if (fabsf(self.startTouchPosition.x - currentTouchPosition.x) >= HORIZ_SWIPE_DRAG_MIN &&

fabsf(self.startTouchPosition.y - currentTouchPosition.y) <= VERT_SWIPE_DRAG_MAX) 

{ 

// If touch appears to be a swipe 

if (self.startTouchPosition.x < currentTouchPosition.x) { 

[self myProcessRightSwipe:touches withEvent:event]; 

} else { 

[self myProcessLeftSwipe:touches withEvent:event]; 

} 

self.startTouchPosition = CGPointZero; 

} 

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { 

self.startTouchPosition = CGPointZero; 

} 

拖动手势

简单的一根手指拖动的相关代码。

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { 

} 

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { 

UITouch *aTouch = [touches anyObject]; 

CGPoint loc = [aTouch locationInView:self]; 

CGPoint prevloc = [aTouch previousLocationInView:self]; 

CGRect myFrame = self.frame; 

float deltaX = loc.x - prevloc.x; 

float deltaY = loc.y - prevloc.y; 

myFrame.origin.x += deltaX; 

myFrame.origin.y += deltaY; 

[self setFrame:myFrame]; 

} 

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { 

} 

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { 

} 

多点触摸

taps,drags,swipes 通常都只涉及了一个 touch,比较简单去跟踪。但是处理由多个 touches 组成的触摸事件时,比较有挑战性。需要去记录 touch 的所有相关属性,并且改变它的 state 等等。需要做到两点:

  • Set the view’s multipleTouchEnabled property to YES;将多点触摸属性置为 YES;
  • Use a Core Foundation dictionary object (CFDictionaryRef) to track the mutations of touches through their phases during the event;用 CFDictionaryRef 来跟着 UITouch,这里用 CFDictionaryRef 而不是 NSDictionary,因为 NSDictionary 会 copy 它的 key。而 UITouch 没有采取 NSCopying 协议。

Determining when the last touch in a multitouch sequence has ended,判断 multitouch sequence 里的最后一个 touch 是否结束,可以用下面的代码


- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { 

if ([touches count] == [[event touchesForView:self] count]) { 

// Last finger has lifted 

} 

} 

指定自定义的触摸事件行为

通过改变一些属性去改变事件流的处理。

多个 touch 的分发传递(multipleTouchEnabled)

Turn on delivery of multiple touches. 默认值为NO,意味着只会接受触摸队列里面的第一个 touch,其他的会忽略掉。所以,[touches anyObject]方法就只会返回一个对象。将属性 multipleTouchEnabled 设为 YES,则可以处理多个 touches。

限制事件只分发给一个 view(exclusiveTouch)

Restrict event delivery to a single view. 即只有一个 view 响应事件。默认情况下,view 的 exclusiveTouch 属性为 NO,这就意味着,一个 view 不会阻塞 window里的其他 view 去接受事件。如果将某个 view 的 exclusiveTouch 设为 YES,那么当它接受 touches 时,只会有它一个接收 touches 。这里举例说明了 exclusiveTouch 属性,A、B、C 3个 view 多点触摸的例子。

Apple Restricting event delivery with an exclusive-touch view

If the user touches inside A, it recognizes the touch. But if a user holds one finger inside view B and also touches inside view A, then view A does not receive the touch because it was not the only view tracking touches. Similarly, if a user holds one finger inside view A and also touches inside view B, then view B does not receive the touch because view A is the only view tracking touches. At any time, the user can still touch both B and C, and those views can track their touches simultaneously.

exclusiveTouch 这个属性比较傲娇,只有当设置它的为 YES 的 view 首先收到触摸事件时,它才能响应。

限制事件分发到 subviews 上 (hitTest:withEvent:)

Restrict event delivery to subviews. 重载 hitTest:withEvent: 方法返回自己 self。

关闭事件的分发

  • userInteractionEnabled 属性置为 NO;
  • hidden 属性置为 NO;
  • alpha 属性值 <= 0.01;

阶段性的关闭事件的分发

beginIgnoringInteractionEvents 方法关闭,endIgnoringInteractionEvents 方法开启。这个方法是 UIApplication 的,所以能做一些全局性的事情。

触摸事件的转发

你可以将一个事件转发给另外一个响应对象(响应链就是这样玩的嘛),当你使用这个技术的时候得小心,因为 UIKit 没有设计去接受不属于它们的事件。所以,你不要转发给 UIKit 框架的对象。如果你想要有条件的去转发事件给其他响应对象时,那么这些对象应该是 UIView 的实例,并且这些对象关心事件的转发,并且能够处理这些事件。原因如下:

For a responder object to handle a touch, the touch’s view property must hold a reference to the responder. 一个 responder 对象想要处理一个 touch ,那么 touch 的 view 属性必须持有这个 responder。

事件的转发经常需要去分析 touch 对象觉得它是否应该转发事件。这里有一些方法你可以采取去分析:

  • With an “overlay” view, such as a common superview, use hit-testing to intercept events for analysis prior to forwarding them to subviews.(使用 overlay view,例如公用的父视图,在转发到 subviews 之前拦截事件去分析)
  • Override sendEvent: in a custom subclass of UIWindow, analyze touches, and forward them to the appropriate responders.(UIWindow 的子类重载 sendEvent: 方法,将事件转发到合适的 responders)

重载 sendEvent: 方法可以监听 App 事件的接收。UIApplication 和 UIWindow 都是用这个方法来分发事件的,所以它就是事件进入 App 的管道一样。当你重载的时候,务必调用父类的实现,[super sendEvent:event]。在 control 和 gesture recognizer 的响应事件里面打断点,可以看到,事件走的 UIKit 开始传递都是先走的,[UIApplication sendEvent:]、[UIWindow sendEvent:],最终都是走的 [UIApplication sendAction:to:from:forEvent:]。

gesture recognizer handle flow

control handle flow

处理多点触摸事件的最佳实践

当处理 touch 和 motion 事件时,这里有一些值得推荐的技巧和模式:

  • 记得实现事件的取消方法。
  • 如果自定义的是 UIView、UIViewController、UIResponder的子类时,你应该实现所有的事件方法,即使里面没有做任何实现。但是不要在里面调用父类的实现。
  • 如果是其他 UIKit 的子类时,你没有必须实现所有的事件方法。但是,你必须得调用父类的实现。即 [super touchesBegan:touches withEvent:event] 。
  • 只转发事件给 UIView 的子类。并确保这些转发后的对象能够知道并且处理这些不属于它的事件。
  • 不要显示的通过 nextResponder 方法在响应链上发送事件。相反的,调用父类的实现,并且让 UIKit 去遍历处理。
  • 不要使用 round-to-integer 代码(即不要使用 integer 来处理 float),这样会丢失精度。
  • 如果在事件处理的时候需要创建持久对象,记得在 touchesCancelled:withEvent 和 touchesEnded:withEvent: 里面销毁它们。
  • 如果你阻止它接受某个状态的 touch 事件时,可能导致结果不确定。不过,我们在实际中应该不会这样做。

需求

事件传递的最终目标是找到一个能够处理响应这个事件的对象(UIResponder 的子类)。如果找不到就丢弃它。

前提条件

能够处理事件的对象需要完成下面3个条件:

  • 实现这四个方法

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; 

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; 

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; 

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event; 

  * 可以交互的。即 userInteractionEnabled 属性为 YES。 

  * 是可见的。即 hidden = NO & alpha > 0.01。

UIKit 已有的轮子

换汤不要药,跟前面的前提条件一样,只不过是另外一种形式来完成而已。

gesture recognizer

通过实现跟 UIResponder 相同签名的方法来完成。参考例子,上面有提到,官方文档 Listing 1-8 Implementation of a checkmark gesture recognizer 和 YYGestureRecognizer。

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; 

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; 

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; 

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event; 

UIControl

也是内部实现跟 UIResponder 相同功能的方法来完成,里面通过一个 _targetActions 数组来存储各种 UIControlEvents 状态的事件。可以参考Chameleon UIControl 和 SVSegmentedControl。

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event; 

- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event; 

- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event; 

- (void)cancelTrackingWithEvent:(UIEvent *)event; 

过程

手指触摸屏幕就会生成 UIEvent 对象,然后放在 application 的队列里面,application 会从系统队列的顶层取出一个事件并分发它。application(sendEvent:) -> window(sendEvent:) -> initial object(hit-test view or frist responder)。
而 application 和 window 则是通过 Hit-Testing 和响应链来找到 initial object。一般情况下,都不需要我们去干涉 UIKit 的这个分发过程。但是,我们可以在这个过程去干涉达到自己的需求。

用途

这个章节的相关代码参考自

  • smnh Hit-Testing in iOS
  • ZhoonChen 深入浅出iOS事件机制
  • iOS事件响应链中Hit-Test View的应用

扩大触摸区域

我们绘制 UIButton 的时候,想要扩大它的响应区域。我们可以在 UIButton 里面处理 Hit-Testing 的那两个方法其中一个里面做处理。

hitTest:withEvent:


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

CGRect touchRect = CGRectInset(self.bounds, -10, -10); 

if (CGRectContainsPoint(touchRect, point)) { 

return self; 

} 

return [super hitTest:point withEvent:event]; 

} 

pointInside:withEvent:


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

return CGRectContainsPoint(HitTestingBounds(self.bounds, self.minimumHitTestWidth, self.minimumHitTestHeight), point); 

} 

CGRect HitTestingBounds(CGRect bounds, CGFloat minimumHitTestWidth, CGFloat minimumHitTestHeight) { 

CGRect hitTestingBounds = bounds; 

if (minimumHitTestWidth > bounds.size.width) { 

hitTestingBounds.size.width = minimumHitTestWidth; 

hitTestingBounds.origin.x -= (hitTestingBounds.size.width - bounds.size.width)/2; 

} 

if (minimumHitTestHeight > bounds.size.height) { 

hitTestingBounds.size.height = minimumHitTestHeight; 

hitTestingBounds.origin.y -= (hitTestingBounds.size.height - bounds.size.height)/2; 

} 

return hitTestingBounds; 

} 

superview 响应 subview 的事件

这个在限制事件分发到 subviews 上小节里面就有说过。重载 hitTest:withEvent: 方法返回自己 self。

  • Apple UIApplication sendEvent:
  • Apple UIApplication sendAction:to:from:forEvent:
  • Apple UIWindow sendEvent:
  • Apple UIResponder
  • Apple UIControl
  • Apple UIGesturerecognizer
  • Apple TargetAction
  • iOS-Runtime-Headers _targetActions数组
  • Chameleon 自定义控件里面
  • Chameleon hitTest:withEvent:
  • Chameleon pointInside:withEvent:
  • 南峰子 UIKit: UIControl
  • 南峰子 UIKit: UIResponder
标签: 暂无
最后更新:2022年 11月 11日

陈昭

IT 程序员

打赏 点赞
< 上一篇
下一篇 >

文章评论

取消回复

COPYRIGHT © 2022 chenzhao. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang