iOS 事件处理总结与思考

UIControl

UIControl继承自UIView。
UIControl 依赖于Target-Action设计模式。即当发生一个事件时,UIControl会调用sendAction:to:forEvent:方法来将行为消息发送到UIApplication对象,再由UIApplication对象调用其sendAction:to:fromSender:forEvent:方法来将消息分发到指定的target上。如果没有指定target,则会将事件分发到响应链上第一个想处理该消息的对象上。

UIControl有不同的状态

typedef NS_OPTIONS(NSUInteger, UIControlState) {
    UIControlStateNormal       = 0,
    UIControlStateHighlighted  = 1 << 0,                  // used when UIControl isHighlighted is set
    UIControlStateDisabled     = 1 << 1,
    UIControlStateSelected     = 1 << 2,                  // flag usable by app (see below)
    UIControlStateFocused NS_ENUM_AVAILABLE_IOS(9_0) = 1 << 3, // Applicable only when the screen supports focus
    UIControlStateApplication  = 0x00FF0000,              // additional flags available for application use
    UIControlStateReserved     = 0xFF000000               // flags reserved for internal framework use
};

通过继承UIControl类,就可以使用OC内建的 target-action 机制以及简化版的 event-handling。主要有以下两类方法来实现UIControl。

  1. 重写 sendAction:to:forEvent:方法。这样就可以观察或者改写OC的分发机制,从而达到监听某个特定的对象(object)对于特定的事件(event)做了什么特定的处理(selector)。进一步的可以拦截到这些对象的事件,把它们发送到其他对象,或者让本对象执行其他的方法。

  2. 重写

    beginTrackingWithTouch:withEvent:, 

    continueTrackingWithTouch:withEvent:, 

    endTrackingWithTouch:withEvent:, 

    cancelTrackingWithEvent:

等方法。这样就可以追踪并获取到control对象的状态。进一步的,可以依据这些状态去更新页面上控件的状态;或者调用某些方法,执行其他命令。

此处需要注意,苹果文档上有一句:Always use these methods to track touch events instead of the methods defined by the UIResponder class.不知为何,苹果要这样写。

UIResponder

UIResponder对象及其子类的对象都叫做响应者。继承关系如下图:

UIResponder.png

也就是说UIApplication,UIViewCOntroller,UIView都是响应者,都可以接收并处理事件。
响应者是响应事件的。在iOS中,事件分为三种。即触摸事件,加速计事件,远程控制事件。
一般开发中触摸事件使用最频繁,而且其他两种事件处理方式与触摸事件大同小异,所以只介绍触摸事件。

触摸事件包括:

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

可以看出,触摸事件包括用户交互的整个过程。包括触摸开始,用户滑动,触摸结束,以及触摸因为其他事件(比如来电话)被取消。

以上方法中都包含两个参数:touches和event。
touches是一个包含UITouch对象的集合。

UITouch对象记录着某个事件中手指的相关信息,比如位置,大小,运动状况,手指在屏幕上的压力(限于有3D Touch的手机)等。
主要属性有:

  1. window 触摸所在的窗口
  2. view 触摸所在的视图
  3. tapCount 点击屏幕的次数
  4. majorRadius 触摸范围半径。锤子科技的Big-Bang就是根据触摸半径的大小来判断是否“炸开”文字的。
  5. gestureRecognizers 手势数组。如果触摸事件是发生在view对象上的,给这个view对象添加的手势UIGestureRecognizer都会在这个数组中。如果view对象没有添加过手势,这个数组中也有一个系统手势:_UISystemGestureGateGestureRecognizer。

UIGestureRecognizer

手势识别。苹果文档中有这样一段描述值得注意:

A window delivers touch events to a gesture recognizer before it delivers them to the hit-tested view attached to the gesture recognizer. Generally, if a gesture recognizer analyzes the stream of touches in a multi-touch sequence and doesn’t recognize its gesture, the view receives the full complement of touches. If a gesture recognizer recognizes its gesture, the remaining touches for the view are cancelled. The usual sequence of actions in gesture recognition follows a path determined by default values of the cancelsTouchesInView, delaysTouchesBegan, delaysTouchesEnded properties:

即,当触摸事件发生时,如果,手势对象会先于view对象获取到触摸事件。如果这个手势对象可以处理该事件,那么view对象就不会接收到触摸事件。如果还想让view也接收到事件,就要把手势的cancelsTouchesInView属性设置为NO。
具体参见以下代码:

//给一个view对象添加UIButton子控件。
UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(10, 20, 200, 20)];
[btn setTitle:@"button" forState:UIControlStateNormal];
//btn对象添加 target-action
[btn addTarget:self action:@selector(buttonAction) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:btn];
UITapGestureRecognizer *btnTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(buttonTapAction)];
//设置cancelsTouchesInView属性。
btnTap.cancelsTouchesInView = NO;
[btn addGestureRecognizer:btnTap];

以上代码,当cancelsTouchesInView为YES时。只响应buttonTapAction方法;当为NO时,事件可以继续传递,buttonTapActionbuttonAction方法均响应。

UIGestureRecognizer 响应始终在主线程。测试代码中曾把添加手势的代码放在子线程中,结果发现手势的响应仍然是在主线程。我猜测是这样的,手势的添加不在乎在哪个线程,只要把手势添加到view上即可。触摸事件的发生以及传递是在主线程的。所以,我们的响应方法最终在主线程被执行。

但是,文档中有一句我不是很明白A gesture recognizer doesn’t participate in the view’s responder chain.查了一些资料,还是没有头绪,这个问题先记着,后续处理。//TODO:find the answer.

事件传递

问题来了,如果UIResponder,UIGestureRecognizer,UIControl各自的对象同时出现一个或者多个;又或者他们三个中不同的对象同时出现,那响应顺序是什么样子的呢?

  1. 上面分析过,UIGestureRecognizer和UIControl同时存在时。会优先处理UIGestureRecognizer,如果事件能够响应,则不再处理UIControl。
  2. 如果view对象在添加了UIGestureRecognizer手势的同时,也实现了UIResponder的方法,比如touchBegin。那响应顺序如何?以下是我的测试:
    如下图的结构:
UITestView.png

UITestViewB和UITestViewA都是UIView的子类。并且都添加了单击手势
UITapGestureRecognizer * tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction)];
同时,实现了- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event方法
我测试的结果是先响应UIResponder方法,再响应UIGestureRecognizer。

另外,如果想要UIResponder继续传递,那就直接调用super方法,触摸事件就可以接着传递给父控件;
如果想要UIGestureRecognizer继续传递,那就重写可以同时响应的代理方法–gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer。虽然这个方法的说法是可以让一个对象同时响应多个手势,但经过测试发现,当这个方法返回YES时,父子控件可以同时响应一个触摸事件。如果父子控件的这个代理方法都返回NO(或者不写,默认是NO),那么只有子控件响应触摸事件。

这里我还有一个问题,没有解决。就是UIControl了类的点击事件如何继续传递。情景是这样的。一个view中添加一个button子控件。butoon通过addTarget添加target-action。如何在点击不同后,让该点击事件继续传递到父控件view上。view实现了toucheBegin并且也添加了tap手势。

我能想到的方法是,给button再次添加target-action。即在点击button是发出给两个target发送message,其中一个message是view。即把触摸事件发送给view。但是这样只能够实现相同的效果,并不是把同一个触摸事件传递给view。虽然应该不会有这样的需求,但我只是好奇这能够实现吗?

参考:

UIControl

UIControl补充

Target-Action

UIGestureRecognizer

EventHandlingiPhoneOS

IT文库 » iOS 事件处理总结与思考
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址