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。
重写
sendAction:to:forEvent:
方法。这样就可以观察或者改写OC的分发机制,从而达到监听某个特定的对象(object)对于特定的事件(event)做了什么特定的处理(selector)。进一步的可以拦截到这些对象的事件,把它们发送到其他对象,或者让本对象执行其他的方法。重写
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对象及其子类的对象都叫做响应者。继承关系如下图:
也就是说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的手机)等。
主要属性有:
- window 触摸所在的窗口
- view 触摸所在的视图
- tapCount 点击屏幕的次数
- majorRadius 触摸范围半径。锤子科技的Big-Bang就是根据触摸半径的大小来判断是否“炸开”文字的。
- 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各自的对象同时出现一个或者多个;又或者他们三个中不同的对象同时出现,那响应顺序是什么样子的呢?
- 上面分析过,UIGestureRecognizer和UIControl同时存在时。会优先处理UIGestureRecognizer,如果事件能够响应,则不再处理UIControl。
- 如果view对象在添加了UIGestureRecognizer手势的同时,也实现了UIResponder的方法,比如
touchBegin
。那响应顺序如何?以下是我的测试:
如下图的结构:
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。如何在点击button后,让该点击事件继续传递到父控件view上。view实现了toucheBegin
并且也添加了tap手势。
我能想到的方法是,给button再次添加target-action。即在点击button是发出给两个target发送message,其中一个message是view。即把触摸事件发送给view。但是这样只能够实现相同的效果,并不是把同一个触摸事件传递给view。虽然应该不会有这样的需求,但我只是好奇这能够实现吗?