值得注意的事,当一个view上面有多个手势时,touch是无序的
1事件是怎么样产生与传递的?(由上至下的过程)
当发生一个触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中.
UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理.
主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件
触摸事件的传递是从父控件传递到子控件的.
如果一个父控件不能接收事件,那么它里面的了子控件也不能够接收事件.
如何寻找最合适的View?
主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件.
那如何找到最合适的View呢?
1.先判断自己是否能够接收触摸事件,如果能再继续往下判断,
2.再判断触摸的当前点在不在自己的身上.
3.如果在自己身上,它会从后往前遍历子控件,遍历出每一个子控件后,重复前面的两个步骤.
4.如果没有符合条件的子控件,那么它自己就是最适合的View.
2-事件响应(由下至上)
用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件,
找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理
那这些touches方法的默认做法是将事件顺着响应者链条向上传递,将事件交给上一个响应者进行处理
什么是响应者链条?
是由多个响应者对象连接起来的链条.
什么是响应者对象?
继承了UIResponder对象我们称之为响应者对象,也就是能处理事件的对象
事件传递的完整过程?
在产生一个事件时,系统会将该事件加入到一个由UIApplication管理的事件队列中,
UIApplication会从事件队列中取出最前面的事件,将它传递给先发送事件给应用程序的主窗口.
主窗口会调用hitTest方法寻找最适合的视图控件,找到后就会调用视图控件的touches方法来做具体的事情.
当调用touches方法,它的默认做法, 就会将事件顺着响应者链条往上传递,
传递给上一个响应者,接着就会调用上一个响应者的touches方法
如何去寻找上一个响应者?
1.如果当前的View是控制器的View,那么控制器就是上一个响应者.
2.如果当前的View不是控制器的View,那么它的父控件就是上一个响应者.
3.在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理
4.如果window对象也不处理,则其将事件或消息传递给UIApplication对象
5.如果UIApplication也不能处理该事件或消息,则将其丢弃
3事件的产生与传递(由上至下) 和 事件响应(由下至上)
(通俗讲: 方便大家理解,并无其他意思,勿喷.)
第一步 事件的传递与传递 (找到哪里发生地点)
一个地方发生了大型抢劫时间.
国家知道了有这个事件,就先省份上找,并按照省份名单顺序, 国家问浙江: 浙江浙江,是你那边的事吗? 浙江回答:不是. 再问福建:
福建,是你那边的事吗? 福建回答:是的. 然后福建在问它下面的城市,以此类推,一直找到 最终的地方.
第二步 事件响应 (把事件交给合适的部门处理)
发生的地点把 这个事件, 交给了这个地点的部门, 地点的部门说对不起,我处理不了.
地点部门交给了 市部门,市部门收到事件,判断能否处理(能处理则不传递上去,不能则传给上级部门),以此类推,一直传到国家(如果连国家都不能处理则 丢弃这个事情)
补充
处理部门判断能否处理 的标准是 1手势的响应2继承于UIControl都会阻断 响应者链条的往上传递(button继承于UIControl)3继承于UIScroll也会阻断往上传递
即总的来说:能响应事件的对象通常默认取消事件往上传递(都有部门接收处理这个事件了,当然不用往上传了,哪怕像button scroll这种流氓部门 不管有对这个事件只接收不处理,它都打断了往上传递)
3-hitTest方法与PointInside方法
个人通俗理解
1顺序:先执行hitTest 再PointInside
2 hitTest作用:寻找个合适的View,先遍历subView寻找,找到则返回view,找不到返回self
PointInside判断点击的点 是否在这个View上
作用:寻找最适合的View
参数:当前手指所在的点.产生的事件
返回值:返回谁, 谁就是最适合的View.
只要一个事件,传递给一个控件时, 就会调用这个控件的hitTest方法
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
作用:判断point在不在方法调用者上
point:必须是方法调用者的坐标系
hitTest方法底层会调用这个方法,判断点在不在控件上.
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
return YES;
}
hitTest底层实现:
1.判断当前能不能接收事件
if(self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01)
return nil;
2.判断触摸点在不在当前的控件上
if(![self pointInside:point withEvent:event]) return nil;
3.从后往前遍历自己的子控件
int count = (int)self.subviews.count;
for (int i = count - 1; i >= 0;i-- ) {
UIView *childV = self.subviews[i];
把当前坐标系上的点转换成子控件坐标系上的点.
CGPoint childP = [self convertPoint:point toView:childV];
判断自己的子控件是不是最适合的View
UIView *fitView = [childV hitTest:childP withEvent:event];
如果子控件是最合适的View,直接返回
if (fitView) {
return fitView;
}
}
(下图代码可以看出 hitTest里面 执行pointInSide)
4.自己就是最适合的View
return self.
4一个控件什么情况下不能够接收事件.
1.不接收用户交互时不能够处理事件
userInteractionEnabled = NO
2.当一个控件隐藏的时候不能够接收事件
Hidden = YES的时候
3.当一个控件为透明白时候也不能够接收事件
注意:UIImageView的userInteractionEnabled默认就是NO,
因此UIImageView以及它的子控件默认是不能接收触摸事件的
5响应链的实际运用
1放大button的作用域(超过它的frame仍能响应)
2点击的view,却让其他view响应
6-hitTest练习1
业务逻辑:
底部一个按钮, 按钮的上面有一个View,遮挡在按钮的上面.
点击View时, View接收事件,当发现点击的点在按钮的位置时, 让底部的按钮处理事件.
实现思路:
实现View的touchBegain方法,先坚听UIView的点击.
并去实现UIView的HitTest方法, 在hitTest方法当中通过把当前点转换成按钮所在的坐标系
CGPoint btnP = [self convertPoint:point toView:self.btn];
转换过后查看当前点在不在按钮上,如果在按钮上,就直接返回按钮.
如果有在按钮上,保持系统默认做法.
实现代码:
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
判断当前点在不在按钮上.
把当前点转换成按钮所在的坐标系
CGPoint btnP = [self convertPoint:point toView:self.btn];
if ([self.btn pointInside:btnP withEvent:event]) {
return self.btn;
}else{
return [super hitTest:point withEvent:event];
}
}
7-hitTest方法练习2
业务逻辑:
按钮可以随着手指拖动而拖动.拖动过程当中,按钮当中的子控件也跟着拖动.
让超过按钮的子控件也能够响应事件,一般情况下,当一个控件超过他的父控件的时候,是不能够接收事件的.
现在要做的事情就让超过父控件的按钮也能够响应事件.
实现思路:
先办到让按钮能够跟随着手指移动而移动.
实现按钮的touchesMoved方法,在touchesMoved方法当中,获得当前手指所在的点.以前上一个点.
分别计算X轴的偏移量以及Y轴的偏移量.
然后修改当前按钮的transform让按钮办到能够跟随着手指移动而移动.
第二步, 实现按钮的hitTest方法.
在该方法当中去判断当前的点在不在按钮的子控件上.
如果在按钮的子控件上.就返回按钮的子控件如果不在的话, 就保持系统的默认做法.
实现代码:
第一步,让按钮能够跟随着手指移动而移动
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
获取当前的手指
UITouch *touch = [touches anyObject];
获取当前手指所在的点
CGPoint curP = [touch locationInView:self];
获取当前手指的上一个点
CGPoint preP = [touch previousLocationInView:self];
计算X轴的偏移量
CGFloat offsetX = curP.x - preP.x;
计算Y轴的偏移量
CGFloat offsetY = curP.y - preP.y;
修改按钮的形变,让按钮能够移动.
self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
}
第二步,实现hitTest方法,判断手指当前所在的点在不在按钮的子控件上.
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
把当前所在的点转换成按钮子控件上面的点
CGPoint chatP = [self convertPoint:point toView:self.chatBtn];
判断转换后的点在不在按钮的控件上.
if ([self.chatBtn pointInside:chatP withEvent:event]) {如果在
直接返回,也就意味着,当前最适合的View,就是这个按钮
return self.chatBtn;
}else{如果不在,那么就保持系统原有做法.
return [super hitTest:point withEvent:event];
}
}
相关知识扩充
关于手势
值得注意的是:手势识别器 是 先于touch方法(touch响应链!) 捕捉到touch object!!!
cancelsTouchesInView/delaysTouchesBegan/delaysTouchesEnded
(0)首先要知道的是
1.这3个属性是作用于GestureRecognizers(手势识别)与触摸事件之间联系的属性。实际应用中好像很少会把它们放到一起,大多都只是运用手势识别,所以这3个属性应该很少会用到。
2.对于触摸事件,window只会有一个控件来接收touch。这个控件是首先接触到touch的并且重写了触摸事件方法(一个即可)的控件
3.手势识别和触摸事件是两个独立的事,只是可以通过这3个属性互相影响,不要混淆。
4手势是view外部来添加 , touch是view内部处理,两个是分开,且手势优先级比touch高
(1)在默认情况下(即这3个属性都处于默认值的情况下)(这些属性是 手势对它自己的view!!!)
如果触摸window,首先由window上最先符合条件的控件(该控件记为hit-test
view)接收到该touch并触发触摸事件touchesBegan。同时如果某个控件的手势识别器接收到了该touch,就会进行识别。手势识别成功之后发送触摸事件touchesCancelled给hit-testview,hit-test
view不再响应touch。(即打断 往上传递的响应链条)
(2)cancelsTouchesInView:(默认yes)
默认为YES,这种情况下当手势识别器识别到touch之后,会发送touchesCancelled给hit-testview以取消hit-test view对touch的响应,这个时候只有手势识别器响应touch。
当设置成NO时,手势识别器识别到touch之后不会发送touchesCancelled给hit-test,这个时候手势识别器和hit-test view均响应touch。
(3)delaysTouchesBegan:(默认no)
默认是NO,这种情况下当发生一个touch时,手势识别器先捕捉到到touch,然后发给hit-testview,两者各自做出响应。
如果设置为YES,手势识别器在识别的过程中(注意是识别过程),不会将touch发给hit-test
view,即hit-testview不会有任何触摸事件。!!只有在识别失败之后才会将touch发给hit-testview,这种情况下hit-test
view的响应会延迟约0.15ms。
(4)delaysTouchesEnded:(默认yes)
默认为YES。这种情况下发生一个touch时,在手势识别成功后,发送给touchesCancelled消息给hit-testview,手势识别失败时,会延迟大概0.15ms,期间没有接收到别的touch才会发送touchesEnded。如果设置为NO,则不会延迟,即会立即发送touchesEnded以结束当前触摸。
(5)手势共存 与 排斥(以下是手势对手势!!)
1:[tapGesture requireGestureRecognizerToFail:swipeGesture]
swipe判断失败后 才判断tap
2:(代理方法)-
(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer
*)otherGestureRecognizer
这里返回YES,代表跟别的手势共存;如果返回NO,不一定代表不共存(可能另一个手势返回yes就可以共存,只要两个手势任一返回yes就可以)
3:-
(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer
*)otherGestureRecognizer
另外一个手势识别fail的时候,才会识别自己
4-
(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer
*)otherGestureRecognizer
我被另外一个手势变成Fail
(6)button 是用这个方法发送时间(补充)
- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
关于scrollView
有很多新闻类的App顶部都有一个滑动菜单栏,主要模型可能是由一个UIScrollView包含多个UIButton控件组成;当你操作的时候,手指如果是很迅速的在上面划过,会发现即使手指触摸的地方有UIButton,但是并没有触发该UIButton的任何触摸事件,这就是上面提到的case1;当你手指是缓慢划过或根本就没动,才会触发UIButton的触摸事件,这是case2的情况。
(0):scrollView默认封装了两个手势 pan和pinch
(1):scrollView的touch事件传递的两个重要的属性delaysContentTouches和canCancelContentTouches
delaysContentTouches(默认yes)(这个属性也是上面说scrollView默认阻断 响应链的原因)
delay延迟对subView的touch响应,肯定会优先响应UIScrollview滑动事件
delaysContentTouches。默认值为YES;如果设置为NO,则无论手指移动的多么快,始终都会将触摸事件传递给内部控件;设置为NO可能会影响到UIScrollView的滚动功能。
canCancelContentTouches(默认yes)
注意:如果scrollView(内部)- (BOOL)touchesShouldCancelInContentView:(UIView *)view
也设置,那两个要配合,平时就不要写这个方法
如果属性值为YES并且跟踪到手指正触摸到一个内容控件,这时如果用户拖动手指的距离足够产生滚动,那么内容控件将收到一个touchesCancelled:withEvent:消息,而scroll
view将这次触摸作为滚动来处理。如果值为NO,一旦content
view开始跟踪(tracking==YES),则无论手指是否移动,scrollView都不会滚动。
简单通俗点说,如果为YES,就会等待用户下一步动作,如果用户移动手指到一定距离,就会把这个操作作为滚动来处理并开始滚动,同时发送一个touchesCancelled:withEvent:消息给内容控件,由控件自行处理。如果为NO,就不会等待用户下一步动作,并始终不会触发scrollView的滚动了。
关于control
简单区分UIResponder与UIControl
UIResponder类:上承NSObject,下接UIView ,UIVIewController ,UIApplacation;响应点,压,滑;
UIControl类:上承UIView,下接UIButton等开关按钮;
主要区别在于:
前者,主要是响应某个动作,执行某个行为--
-(void)touchesBegan:(NSSet*)touches withEvent:(UIEvent *)event;
后者,在继承了前者的属性基础上,还能够相应某个动作,为某个对象,添加动作--
- (void) addTarget:(id)target action:(SEL)action forControlEvents(UIControlEvents)controlEvents
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
返回值:YES 接受用户通过addTarget:action:forControlEvents添加的事件继续处理。
返回值:NO 则屏蔽用户添加的任何事件
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event可以用这个追踪 button上touch的改变 (有点类似于viewControl里面的touchMoved)
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 如果用户重写了该方法,则不会执行由用户添加的其他事件,直接屏蔽了用户的事件
分派事件
使用下面两个方法分派事件给响应者处理:
- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event;
-
(void)sendActionsForControlEvents:(UIControlEvents)controlEvents;
// send all actions associated with events
事件与操作的区别:事件报告对屏幕的触摸;操作报告对控件的操纵。