触摸是iOS程序的精髓所在,良好的触摸体验能让iOS程序得到非常好的效果,例如Clear。
鉴于同学们只会用cocos2d的 CCTouchDispatcher 的 api 但并不知道工作原理,但了解触摸分发的过程是极为重要的。毕竟涉及到权限、两套协议等的各种分发。于是我写了这篇文章来抛砖引玉。
本文以cocos2d-iphone源代码为讲解。cocos2d-x 于此类似,就不过多赘述了。
零、cocoaTouch的触摸
在讲解cocos2d触摸协议之前,我觉得我有必要提一下CocoaTouch那四个方法。毕竟cocos2d的Touch Delegate 也是通过这里接入的。
- (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;
1、一个UITouch的生命周期
一个触摸点会被包装在一个UITouch中,在TouchesBegan的时候创建,在Cancelled或者Ended的时候被销毁。也就是说,一个触摸点在这四个方法中内存地址是相同的,是同一个对象。
2、UIEvent
这是一个经常被大伙儿忽视的东西,基本上没见过有谁用过,不过这个东西的确不常用。可以理解为UIEvent是UITouch的一个容器。
你可以通过UIEvent的allTouches方法来获得当前所有触摸事件。那么和传入的那个NSSet有什么区别呢?
那么来设想一个情况,在开启多点支持的情况下,我有一个手指按在屏幕上,既不移动也不离开。然后,又有一只手指按下去。
这时TouchBegan会被触发,它接到的NSSet的Count为1,仅有一个触摸点。
但是UIEvent的alltouches 却是2,也就是说那个按在屏幕上的手指的触摸信息,是可以通过此方法获取到的,而且他的状态是UITouchPhaseStationary
3、关于Cancelled的误区
有很多人认为,手指移出屏幕、或移出那个View的Frame 会触发touchCancelled,这是个很大的误区。移出屏幕触发的是touchEned,移出view的Frame不会导致触摸终止,依然是Moved状态。
那么Cancelled是干什么用的?
官方解释:This method is invoked when the Cocoa Touch framework receives a system interruption requiring cancellation of the touch event; for this, it generates a UITouch object with a phase of UITouchPhaseCancel. The interruption is something that might cause the application to be no longer active or the view to be removed from the window
当Cocoa Touch framework 接到系统中断通知需要取消触摸事件的时候会调用此方法。同时会将导致一个UITouch对象的phase改为UITouchPhaseCancel。这个中断往往是因为app长时间没有响应或者当前view从window上移除了。
据我统计,有这么几种情况会导致触发Cancelled:
1、官方所说长时间无响应,view被移除
2、触摸的时候来电话,弹出UIAlert View(低电量 短信 推送 之类),按了home键。也就是说程序进入后台。
3、屏幕关闭,触摸的时候,某种原因导致距离传感器工作,例如脸靠近。
4、手势的权限盖掉了Touch, UIGestureRecognizer 有一个属性:
@property(nonatomic) BOOL cancelsTouchesInView;
// default is YES. causes touchesCancelled:withEvent: to be sent to the view for all touches recognized as part of this gesture immediately before the action method is called
关于CocoaTouch就说到这里,CocoaTouch的Touch和Gesture混用 我会在将来的教程中写明。
一、TouchDelegate的接入。
众所周知CCTouchDelegate是通过CocoaTouch的API接入的,那么是从哪里接入的呢?我们是知道cocos2d是跑在一个view上的,这个view 就是 EAGLView 可在cocos2d的Platforms的iOS文件夹中找到。
在它的最下方可以看到,他将上述四个api传入了一个delegate。这个delegate是谁呢?
没错就是CCTouchDispatcher
但纵览整个EAGLView的.m文件,你是找不到任何和CCTouchDispatcher有关的东西的。
那么也就是说在初始化的时候载入的咯?
EAGLView的初始化在appDelegate中,但依然没看到有关CCTouchDispatcher 有关的东西,但可以留意一句话:
[director setOpenGLView:glView];
点开后可以发现
CCTouchDispatcher *touchDispatcher = [CCTouchDispatcher sharedDispatcher];
[openGLView_ setTouchDelegate: touchDispatcher];
[touchDispatcher setDispatchEvents: YES];
呵呵~ CCTouchDispatcher 被发现了!
二、两套协议
CCTouchDispatcher 提供了两套协议。
@protocol CCTargetedTouchDelegate
- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event;
@optional
- (void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event;
- (void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event;
- (void)ccTouchCancelled:(UITouch *)touch withEvent:(UIEvent *)event;
@end
@protocol CCStandardTouchDelegate
@optional
- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)ccTouchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
@end
与之对应的还有两个在CCTouchDispatcher 中的添加操作
-(void) addStandardDelegate:(id) delegate priority:(int)priority;
-(void) addTargetedDelegate:(id) delegate priority:(int)priority swallowsTouches:(BOOL)swallowsTouches;
其中StandardTouchDelegate 单独使用的时候用法和 cocoaTouch 相同。
我们这里重点说一下CCTargetedTouchDelegate
在头文件的注释中可以看到:
使用它的好处:
1、不用去处理NSSet, 分发器会将它拆开,每次调用你都能精确的拿到一个UITouch
2、你可以在touchbegan的时候retun yes,这样之后touch update 的时候 再获得到的touch 肯定是它自己的。这样减轻了你对多点触控时的判断。
除此之外还有
3、TargetedTouchDelegate支持SwallowTouch 顾名思义,如果这个开关打开的话,比他权限低的handler 是收不到 触摸响应的,顺带一提,CCMenu 就是开了Swallow 并且权限为-128(权限是越小越好)
4、 CCTargetedTouchDelegate 的级别比 CCStandardDelegate 高,高在哪里了呢? 在后文讲分发原理的时候 我会说具体说明。
三、CCTouchHandler
在说分发之前,还要介绍下这个类的作用。
简而言之呢,这个类就是用于存储你的向分发器注册协议时的参数们。
类指针,类所拥有的那几个函数们,以及触摸权限。
只不过在 CCTargetedTouchHandler 中还有这么一个东西
@property(nonatomic, readonly) NSMutableSet *claimedTouches;
这个东西就是记录当前这个delegate中 拿到了多少 Touches 罢了。
只是想在这里说一点:
UITouch只要手指按在屏幕上 无论是滑动 也好 开始began 也好 finished 也好
对于一次touch操作,从开始到结束 touch的指针是不变的.
四、触摸分发
前面铺垫这么多,终于讲到重点了。
这里我就结合这他的代码说好了。
首先先说dispatcher定义的数据成员
NSMutableArray *targetedHandlers;
NSMutableArray *standardHandlers;
BOOL locked;
BOOL toAdd;
BOOL toRemove;
NSMutableArray *handlersToAdd;
NSMutableArray *handlersToRemove;
BOOL toQuit;
BOOL dispatchEvents;
// 4, 1 for each type of event
struct ccTouchHandlerHelperData handlerHelperData[kCCTouchMax];
开始那两个 数组 顾名思义是存handlers的 不用多说
之后下面那一段的东西是用于线程间数据修改时的标记。
提一下那个lock为真的时候 代表当前正在进行触摸分发
然后是总开关
最后就是个helper 。。
然后说之前提到过的那两个插入方法
-(void) addStandardDelegate:(id) delegate priority:(int)priority;
-(void) addTargetedDelegate:(id) delegate priority:(int)priority swallowsTouches:(BOOL)swallowsTouches;
就是按照priority插入对应的数组中。
但要注意一点:当前若正在进行事件分发,是不进行插入的。取而代之的是放到一个缓存数组中。等触摸分发结束后才加入其中。