UIGestureRecognizer
一. UIGestureRecognizer属性方法
cancelsTouchesInView是否取消触摸控件的响应.默认为YES,这种情况下当手势识别器识别到触摸之后,会发送touchesCancelled给触摸到的控件以取消控件view对touch的响应,这个时候只有手势识别器响应touch,当设置成NO时,手势识别器识别到触摸之后不会发送touchesCancelled给控件,这个时候手势识别器和控件view均响应touch。
delaysTouchesBegan是否延迟发送触摸事件给触摸到的控件.默认是NO,这种情况下当发生一个触摸时,手势识别器先捕捉到到触摸,然后发给触摸到的控件,两者各自做出响应。如果设置为YES,手势识别器在识别的过程中(注意是识别过程),不会将触摸发给触摸到的控件,即控件不会有任何触摸事件。只有在识别失败之后才会将触摸事件发给触摸到的控件,这种情况下控件view的响应会延迟约0.15ms。
delaysTouchesEnded如果触摸识别失败是否立即结束本次手势识别的触摸事件(让触摸控件去识别触摸事件).默认为YES,这种情况下发生一个触摸时,在手势识别成功后,发送给touchesCancelled消息给触摸控件view,手势识别失败时,会延迟大概0.15ms,期间没有接收到别的触摸才会发送touchesEnded触摸结束方法,如果设置为NO,则不会延迟,即会立即发送touchesEnded以结束当前触摸。
- 指定一个手势需要另一个手势执行失败才会执行,同时触发多个手势使用其中一个手势的解决办法调用
requireGestureRecognizerToFail.有时手势是相关联的,如单机和双击,点击和长按,点下去瞬间可能只会识别到单击无法识别其他,该方法可以指定某一个手势,即便自己已经满足条件了,也不会立刻触发,会等到该指定的手势确定失败之后才触发(系统默认只允许一个手势识别器被执行).
二. UIGestureRecognizerDelegate
- 手指触摸屏幕后回调的方法,返回NO则不再进行手势识别,方法触发等;不会修改 UIGestureRecognizer 的状态机
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch.
- 开始进行手势识别时调用的方法,返回NO则结束,不再触发手势;在UIGestureRecognizer 识别手势转换状态时调用,返回 NO 会改变其状态机,使其 state 变为
UIGestureRecognizerStateFailed。
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer.
- 是否支持多手势触发,返回YES,则可以多个手势一起触发方法,返回NO(默认)则为互斥
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer.
- 下面两个方法也是用来控制手势的互斥执行的
- 这个方法返回YES,第一个手势和第二个互斥时,第一个会失效
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
- 这个方法返回YES,第一个和第二个互斥时,第二个会失效
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
三. UIScrollView
- UIScrollView处理触摸的原理很简单,就是借助拖拽手势(UIPanGestureRecognizer)来响应滑动,而手势有一个代理方法
gestureRecognizerShouldBegin,个人猜测UIScrollView通过这个代理方法来确定是否要启动Gesture 处理来截获触摸事件。
tracking:只读,一旦用户开始触摸视图(也许还没有开始拖动),该属性值为YES.
dragging只读,当用户开始拖动(手指已经在屏幕上滑动一段距离了),当touch后还没有拖动的时候,值是YES,否则NO.
decelerate只读,当用户松开手指,但视图仍在滚动时,该值返回YES.
- 从你的手指touch屏幕开始,tracking属性变为YES,scrollView开始一个timer,如果:
- 150ms内如果你的手指没有任何动作,消息就会传给subView。
- 150ms内手指有明显的滑动(一个swipe动作),scrollView就会滚动,消息不会传给subView。
- 150ms内手指没有滑动,scrollView将消息传给subView,但是之后手指开始滑动,scrollView传送touchesCancelled消息给subView,然后开始滚动。(例如, 当你touch一个table,停止了一会,然后开始scrolling,那一行就首先被highlight,但是随后就不在高亮了).
- 属性
canCancelContentTouches设置是否能从子控件把滑动事件重新返回给父控件UIScrollView,控制contentView里的触摸是否总能引发跟踪(tracking)。默认YES,如果属性值为YES并且跟踪到手指正触摸到一个内容控件,这时如果用户拖动手指的距离足够产生滚动,那么内容控件将收到一个touchesCancelled消息,而scrollView将这次触摸作为滚动来处理。如果为NO将不调用- (BOOL)touchesShouldCancelInContentView:(UIView *)view方法,这消息一旦传递给subView开始跟踪(tracking==YES),则无论手指是否移动,scrollView都不会滚动,scroll事件不会再发生。
- (BOOL)touchesShouldCancelInContentView:(UIView *)view(canCancelContentTouches默认为YES)被调用.如果这个参数view不是一个UIControl对象,默认返回YES,表示当前view对这个事件不感兴趣,会传递到其他view(scrollView);如果是一个UIControl对象返回NO,scrollView会停止拖动,触摸事件交由该view处理或向子view传递。
delaysContentTouches当我们把该属性设为NO以后,跳过了timer环节,而会立即调用ContentView上的点击事件。所以button的高亮设置生效。
- 子类可以重载
- (BOOL)touchesShouldBegin:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event inContentView:(UIView *)view(这个方法是最先接收到滑动事件的,优先于button的UIControlEventTouchDown,以及- touchesCancelled)方法返回YES(默认),scrollview不会拦截消息,让子view接收消息;返回NO,scrollview拦截消息,不会将消息传递到子view上.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView这个方法在任何方式触发contentOffset变化的时候都会被调用(包括用户拖动,减速过程,直接通过代码设置等),可以用于监控contentOffset的变化,并根据当前的contentOffset对其他view做出随动调整。
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView减速动画结束时被调用,这里有一种特殊情况:当一次减速动画尚未结束的时候再次dragScrollview,该方法不会被调用,并且这时 scrollView的dragging和decelerating属性都是YES。新的dragging如果有加速度,那么- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView会再一次被调用,然后才是- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;如果没有加速度,虽然- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView不会被调用,但前一次留下的- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView会被调用。
四. 手势识别器与原生触摸事件冲突
- 官方文档实际上说的比较清楚:UIWindow会先将触摸事件发送给Hit-Testing返回的View和它的父View上的UIGestureRecognizer,如果UIGestureRecognizer成功识别了这个手势,之后UIWindow不会再向View发送触摸事件,并且会取消之前发送的触摸事件。UIGestureRecognizer首先收到触摸事件,Hit-Testing返回的View延迟收到触摸事件;UIGestureRecognizer识别成功后,会调用响应链的
touchesCancelled方法.
- 对于部分UIControl来说(UIControl的系统子类),为了防止UIControl默认的手势与其父View上的UIGestureRecognizer的冲突,UIControl最后会响应触摸事件,如果想要UIGestureRecognizer处理触摸事件,则需要将其直接与UIControl进行绑定。UIButton等部分UIControl会拦截其父View上的UIGestureRecognizer,但不会拦截自己和子View上的UIGestureRecognizer.
- 上述的原因是部分UIControl重写了
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer方法,虽然 UIGestureRecognizer会首先受到触摸事件,但是在状态转换之前,调用了 Hit-Testing 返回的View也就是UIControl的- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer方法,UIControl 会使父 View上的UIGestureRecognizer失效,而自己的 UIGestureRecognizer 却不会失效,这就是系统实现这个机制的方法。
- 当UIScrollView及其子类为最优响应者时,如果父控件中有gestureRecognizer会被其截获手势事件(父控件中的手势来响应事件)。解决的三种办法:
- 可以通过给父控件手势设置
cancelsTouchesInView为NO,则会同时响应gestureRecognizer的事件和UIScrollView及其子类的代理方法和touches事件。
- 在父控件中的手势的代理方法里面做一下判断,当touch.view是我们需要触发的view的时候,returnNO,这样就不会走手势方法,而由这个touch.view去响了。
(
) -(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { if ([NSStringFromClass([touch.view class]) isEqualToString:@"UITableViewCellContentView"]){ return NO; } return YES; } ()
- 可以通过给UIScrollView及其子类添加gestureRecognizer,从而来调用需要处理的事情。