IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    iOS 事件处理总结与思考

    shendao发表于 2017-05-30 17:01:06
    love 0

    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对象及其子类的对象都叫做响应者。继承关系如下图:

    iOS 事件处理总结与思考
    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时,事件可以继续传递,buttonTapAction和buttonAction方法均响应。

    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。那响应顺序如何?以下是我的测试:
      如下图的结构:
    iOS 事件处理总结与思考
    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



沪ICP备19023445号-2号
友情链接