项目里有一个需求,类似新浪或者腾讯微博的顶部title栏的类别选择器的消失(在选择器展开的时候,触摸屏幕任何地方使其消失)。
最开始的想法是当这个选择器(selectorView)展开的时候,在当前屏幕上加入一个铺满整个屏幕的透明button来拦截所有的触摸事件。
可是这个方案实现起来非常麻烦,也不优雅,而且发现button拦截不到scrollView的滑动事件,所以决定放弃。
后来经过经理提醒,在UIApplication下有一个sendEvent函数,可以从这里入手。
于是找了一下iOS事件机制的资料,sendEvent函数的介绍如下:
sendEvent:
Dispatches an event to the appropriate responder objects in the application.
- (void)sendEvent:( UIEvent *) eventParameters
event
A
UIEvent
object encapsulating the information about an event, including the touches involved.
摘抄《iOS程序之事件处理流程》资料:
在iOS系统中有个很重要的概念:Responder。基本上所有的UI相关的控件,view和viewcontroller都是继承自UIResponder。事件的分发正是通过由控件树所构成的responder chain(响应链)所进行的。一个典型的iOS响应链如下:
当用户发起一个事件,比如触摸屏幕或者晃动设备,系统产生一个事件,同时投递给UIApplication,而UIApplication则将这个事件传递给特定的UIWindow进行处理(正常情况都一个程序都只有一个UIWindow),然后由UIWindow将这个事件传递给特定的对象(即first responder)并通过响应链进行处理。虽然都是通过响应链对事件进行处理,但是触摸事件和运动事件在处理上有着明显的不同(主要体现在确定哪个对象才是他们的first responder):
看起来很对路,触摸事件发生后,会先经过hitTest确定触摸事件发生在哪个view上,然后该事件会经由sendEvent分发到“合适”的对象进行处理,也就是说sendEvent相当于事件的中转站,在这里可以拦截所有的iOS事件。
在iOS系统中,一共有三种形式的事件:触摸事件(Touch Event),运动事件(Motion Event)和远端控制事件(Remote-control Event)。顾名思义,触摸事件就是当用户触摸屏幕时发生的事件,而运动事件是用户移动设备时发生的事件:加速计,重力感应。远端控制事件可能比较陌生:如通过耳机进行控制iOS设备声音等都属于远端控制事件—-下面不展开说,因为和主题无关,详细的内容可以参考: 《Remote Control of Multimedia》 。
于是理了一下思路,决定就从它入手。
具体流程是这样:
1.新建一个自定义的UIApplication(MyApplication),并替换系统默认的UIApplication:
在程序入口处(main.m)修改代码,这样程序就会调用我们的自定义Application类
int main(int argc, char *argv[])
{
@autoreleasepool {
return UIApplicationMain(argc, argv, NSStringFromClass([MyApplication class]), NSStringFromClass([AppDelegate class]));
}
}
2.在MyApplication中实现sendEvent函数,利用系统通知中心(NSNotificationCenter)发送触摸事件:
-(void)sendEvent:(UIEvent *)event
{
if (event.type==UIEventTypeTouches) {
if ([[event.allTouches anyObject] phase]==UITouchPhaseBegan) {
//响应触摸事件(手指刚刚放上屏幕)
[[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:nScreenTouch object:nil userInfo:[NSDictionary dictionaryWithObject:event forKey:@"data"]]];
//发送一个名为‘nScreenTouch’(自定义)的事件
}
}
[super sendEvent:event];
}
3.在selectorView的构造函数中注册nScreenTouch事件,并判断该次触摸时间是否由selectorView引发,如果不是,则隐藏selectorView。
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
//注册nScreenTouch事件
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onScreenTouch:) name:nScreenTouch object:nil];
}
return self;
}
-(void)dealloc
{
//移除nScreenTouch事件
[[NSNotificationCenter defaultCenter] removeObserver:self name:nScreenTouch object:nil];
[super dealloc];
}
-(void) onScreenTouch:(NSNotification *)notification
{
UIEvent *event=[notification.userInfo objectForKey:@"data"];
UITouch *touch=[event.allTouches anyObject];
if (touch.view!=self) {
//取到该次touch事件的view,如果不是触摸了selectorView,则隐藏selectorView.
[UIView animateWithDuration:0.5 animations:^
{
self.alpha=0;
}];
[UIView commitAnimations];
}
}
这样就实现了触摸任意地方,能隐藏弹出窗口的需求。相比较添加隐藏view的方案,这个方案更优雅,只是性能可能会有点损耗,但是可以通过添加全局的开关来控制发送消息的时机(比如只有当selectorView显示之后,才发送那个事件)。
总结
通过sendEvent配合消息中心,可以实现很多看起来挺复杂的功能,而且从解耦的角度,也非常优雅。
另附两个链接可供参考!http://mithin.in/2009/08/26/detecting-taps-and-events-on-uiwebview-the-right-way
http://atastypixel.com/blog/a-trick-for-capturing-all-touch-input-for-the-duration-of-a-touch/