先介绍下事件分发:
移动平台上的开发主要关注数据以及数据的处理,事件的处理以及UI。所以事件的分发处理是很重要的一个环节,对于一个平台的优劣来说也是一项重要的参数。如果事件的分发设计的不好,一些复杂的UI场景就会变得很难写甚至没法写。从小屏没有触摸的功能机开始到现在大屏多点触摸的智能机,对于事件的分发处理基本思路都是一样的——链(设计模式中有个模式就是职责链chain of responsibility),只是判定的复杂程度不同。
iOS中的事件有3类,触摸事件(单点,多点,手势)、传感器事件(加速度传感器)和远程控制事件,这里我介绍的是第一种事件的分发处理。
上面的这张图来自苹果的官方。描述了Responder的链,同时也是事件处理的顺序。通过这两张图,我们可以发现:
1. 事件顺着responder chain传递,如果一环不处理,则传递到下一环,如果都没有处理,最后回到UIApplication,再不处理就会抛弃
2. view的下一级是包含它的viewController,如果没有viewController则是它的superView
3. viewController的下一级是它的view的superView
4. view之后是window,最后传给application,这点iOS会比OS X简单(application就一个,window也一个)
总结出来传递规则是这样的:
这样事件就会从first responder逐级传递过来,直到被处理或者被抛弃。
由于UI的复杂,这个responder chain是需要根据事件来计算的。比如,我现在在一个view内加入了2个Button,先点击了一个,则first responder肯定是这个点击过的button,但我下面可以去点击另一个button,所以显然,当触摸事件来时,这个chain是需要重新计算更新的,这个计算的顺序是事件分发的顺序,基本上是分发的反过来。
无论是哪种事件,都是系统本身先获得,是iOS系统来传给UIApplication的,由Application再决定交给谁去处理,所以如果我们要拦截事件,可以在UIApplication层面或者UIWindow层面去拦截。
UIView是如何判定这个事件是否是自己应该处理的呢?iOS系统检测到一个触摸操作时会打包一个UIEvent对象,并放入Application的队列,Application从队列中取出事件后交给UIWindow来处理,UIWindow会使用hitTest:withEvent:方法来递归的寻找操作初始点所在的view,这个过程成为hit-test view。
hitTest:withEvent:方法的处理流程如下:调用当前view的pointInside:withEvent:方法来判定触摸点是否在当前view内部,如果返回NO,则hitTest:withEvent:返回nil;如果返回YES,则向当前view内的subViews发送hitTest:withEvent:消息,所有subView的遍历顺序是从数组的末尾向前遍历,直到有subView返回非空对象或遍历完成。如果有subView返回非空对象,hitTest方法会返回这个对象,如果每个subView返回都是nil,则返回自己。
好了,我们还是看个例子:
这里ViewA包含ViewB和ViewC,ViewC中继续包含ViewD和ViewE。假设我们点击了viewE区域,则hit-test View判定过程如下:
1. 触摸在A内部,所以需要检查B和C
2. 触摸不在B内部,在C内部,所以需要检查D和E
3. 触摸不在D内部,但在E内部,由于E已经是叶子了,所以判定到此结束
我们可以运行一段代码来验证,首先从UIView继承一个类myView,重写里面的
- - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
- {
- UIView *retView = nil;
- NSLog(@"hitTest %@ Entry! event=%@", self.name, event);
- retView = [super hitTest:point withEvent:event];
- NSLog(@"hitTest %@ Exit! view = %@", self.name, retView);
- return retView;
- }
- - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
- {
- BOOL ret = [super pointInside:point withEvent:event];
- // if ([self.name isEqualToString:@"viewD"]) {
- // ret = YES;
- // }
- if (ret) {
- NSLog(@"pointInside %@ = YES", self.name);
- } else {
- NSLog(@"pointInside %@ = NO", self.name);
- }
- return ret;
- }
在viewDidLoad方法中手动加入5个view,都是myView的实例。
- - (void)viewDidLoad
- {
- [super viewDidLoad];
- _viewA = [[myView alloc] initWithFrame:CGRectMake(10, 10, 300, 200) Color:[UIColor blackColor] andName:@"viewA"];
- [self.view addSubview:_viewA];
- [_viewA release];
- _viewB = [[myView alloc] initWithFrame:CGRectMake(10, 240, 300, 200) Color:[UIColor blackColor] andName:@"viewB"];
- [self.view addSubview:_viewB];
- [_viewB release];
- _viewC = [[myView alloc] initWithFrame:CGRectMake(10, 10, 120, 180) Color:[UIColor blueColor] andName:@"viewC"];
- [_viewB addSubview:_viewC];
- [_viewC release];
- _viewD = [[myView alloc] initWithFrame:CGRectMake(170, 10, 120, 180) Color:[UIColor blueColor] andName:@"viewD"];
- [_viewB addSubview:_viewD];
- [_viewD release];
- _viewE = [[myView alloc] initWithFrame:CGRectMake(30, 40, 60, 100) Color:[UIColor redColor] andName:@"viewE"];
- [_viewD addSubview:_viewE];
- [_viewE release];
- }
这个样式如下:
当我点击viewE的时候,打印信息如下:
2014-01-25 18:32:46.538 eventDemo[1091:c07] hitTest viewB Entry! event=<UITouchesEvent: 0x8d0cae0> timestamp: 6671.26 touches: {(
)}
2014-01-25 18:32:46.538 eventDemo[1091:c07] pointInside viewB = YES
2014-01-25 18:32:46.539 eventDemo[1091:c07] hitTest viewD Entry! event=<UITouchesEvent: 0x8d0cae0> timestamp: 6671.26 touches: {(
)}
2014-01-25 18:32:46.539 eventDemo[1091:c07] pointInside viewD = YES
2014-01-25 18:32:46.539 eventDemo[1091:c07] hitTest viewE Entry! event=<UITouchesEvent: 0x8d0cae0> timestamp: 6671.26 touches: {(
)}
2014-01-25 18:32:46.540 eventDemo[1091:c07] pointInside viewE = YES
2014-01-25 18:32:46.540 eventDemo[1091:c07] hitTest viewE Exit! view = <myView: 0x8c409f0; frame = (30 40; 60 100); layer = <CALayer: 0x8c40a90>>
2014-01-25 18:32:46.540 eventDemo[1091:c07] hitTest viewD Exit! view = <myView: 0x8c409f0; frame = (30 40; 60 100); layer = <CALayer: 0x8c40a90>>
2014-01-25 18:32:46.541 eventDemo[1091:c07] hitTest viewB Exit! view = <myView: 0x8c409f0; frame = (30 40; 60 100); layer = <CALayer: 0x8c40a90>>
2014-01-25 18:32:46.541 eventDemo[1091:c07] touchesBegan viewE
2014-01-25 18:32:46.624 eventDemo[1091:c07] touchesEnded viewE
从打印信息可以看到,先判断了viewB,然后是viewD,最后是viewE,但事件就是直接传给了viewE。
拦截处理方式1:
我们知道事件的分发是由Application到Window再到各级View的,所以显然最安全可靠的拦截地方是Application。这里拦截事件后如果不手动往下分发,则进入hit-test View过程的机会都没有。
UIApplication和UIWindow都有sendEvent:方法,用来分发Event。我们可以继承类,重新实现sendEvent:方法,这样就可以拦截下事件,完成一些特殊的处理。
比如:有一个iPad应用,要求在非某个特定view的区域触摸时进行一项处理。
我们当然可以在其余每一个view里面增加代码进行判断,不过这样比较累,容易漏掉一些地方;另外当UI需求变更时,维护的GG往往会栽进这个坑,显然这不是一个好方法。
这里比较简单的解决方案就是在继承UIApplication类,实现自己的sendEvent:,在这个方法里面初步过滤一下事件,是触摸事件就发送Notification,而特定的view会注册这个Notification,收到后判断一下是否触摸到了自己之外的区域。
恩,还是上代码吧,比较清楚一点:
1. 继承UIApplication的DPApplication类
- #import <UIKit/UIKit.h>
- extern NSString *const notiScreenTouch;
- @interface DPApplication : UIApplication
- @end
- #import "DPApplication.h"
- NSString *const notiScreenTouch = @"notiScreenTouch";
- @implementation DPApplication
- - (void)sendEvent:(UIEvent *)event
- {
- if (event.type == UIEventTypeTouches) {
- if ([[event.allTouches anyObject] phase] == UITouchPhaseBegan) {
- [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:notiScreenTouch object:nil userInfo:[NSDictionary dictionaryWithObject:event forKey:@"data"]]];
- }
- }
- [super sendEvent:event];
- }
- @end
2.要在main.m文件中替换掉UIApplication的调用
- #import <UIKit/UIKit.h>
- #import "AppDelegate.h"
- #import "DPApplication.h"
- int main(int argc, charchar *argv[])
- {
- @autoreleasepool {
- //return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
- return UIApplicationMain(argc, argv, NSStringFromClass([DPApplication class]), NSStringFromClass([AppDelegate class]));
- }
- }
3. 这时已经实现了拦截消息,并在touchBegan的时候发送Notification,下面就是在view里面注册这个Notification并处理
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onScreenTouch:) name:notiScreenTouch object:nil];
- - (void)onScreenTouch:(NSNotification *)notification
- {
- UIEvent *event=[notification.userInfo objectForKey:@"data"];
- NSLog(@"touch screen!!!!!");
- CGPoint pt = [[[[event allTouches] allObjects] objectAtIndex:0] locationInView:self.button];
- NSLog(@"pt.x=%f, pt.y=%f", pt.x, pt.y);
- }
这样就实现了事件的预处理,固有的事件处理机制也没有破坏,这个预处理是静悄悄的进行的。当然,如果我需要把某些事件过滤掉,也只需在DPApplication的sendEvent:方法里面抛弃即可。
拦截处理方式2:
http://www.cnblogs.com/Quains/p/3369132.html
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
返回在层级上离当前view最远(离用户最近)且包含指定的point的view。
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event 返回boolean值指出receiver是否包含指定的point。
重写hittext方法,拦截用户触摸视图的顺序
hitTest方法的都用是由window来负责触发的。
如果希望用户按下屏幕 , 就立刻做出响应 , 使用touchesBegin
如果希望用户离开屏幕 , 就立刻做出响应 , 使用touchesEnd
通常情况下使用touchesBegin,以防止用户认为点击了没有反应。
把hitTest的点转换为 redView的点,使用convertPoint: toView;
CGPoint redP = [self convertPoint:point toView:self.redView];
判断一个点是否在视图的内部:
if ([self.greenView pointInside:greenP withEvent:event]) {
return self.greenView;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { NSLog(@"clcik me root"); } /* 重写hittext方法,拦截用户触摸视图的顺序 hitTest方法的都用是由window来负责触发的。 如果希望用户按下屏幕 , 就立刻做出响应 , 使用touchesBegin 如果希望用户离开屏幕 , 就立刻做出响应 , 使用touchesEnd 通常情况下使用touchesBegin。 */ -(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { //1.判断当前视图是否能接受用户响应 /*self.UserInteractionEnabled=YES self.alpha > 0.01; self.hidden = no; */ //2.遍历其中的所有的子视图,能否对用户触摸做出相应的响应 //3.把event交给上级视图活上级视图控制器处理 //4.return nil;如果发挥nil,说明当前视图及其子视图均不对用户触摸做出反应。 /* 参数说明: point:参数是用户触摸位置相对于当前视图坐标系的点; 注视:以下两个是联动使用的,以递归的方式判断具体响应用户事件的子视图 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event; - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event; 这两个方法仅在拦截触摸事件时使用,他会打断响应者链条,平时不要调用。 提醒:如果没有万不得已的情况,最好不要自己重写hitTest方法; */ return nil; CGPoint redP = [self convertPoint:point toView:self.redView]; //转换绿色视图的点 CGPoint greenP = [self convertPoint:point toView:self.greenView]; //pointInside 使用指定视图中的坐标点来判断是否在视图内部,最好不要在日常开发中都用。 if ([self.greenView pointInside:greenP withEvent:event]) { return self.greenView; } NSLog(@"%@",NSStringFromCGPoint(redP)); if ([self.redView pointInside:redP withEvent:event]) { return self.redView; } return [super hitTest:point withEvent:event]; }
不继承重写的话可以用category来实现:
+(void)initialize{ Method m = class_getInstanceMethod([UIView class],@selector(pointInside:withEvent:)); Method m2 = class_getInstanceMethod([UIView class],@selector(pointInside11:withEvent:)); method_exchangeImplementations(m, m2); } - (BOOL)pointInside11:(CGPoint)point withEvent:(UIEvent *)event { //todo if (*****) { return NO; } return [self pointInside11:point withEvent:event]; }
不解:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event 和- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event 方法都执行三遍,不知道具体原理 ,运行下看了event参数,可能是苹果判断单指多指等复杂操作才这样的。
相关推荐
本主题将详细讲解如何实现iOS与H5页面之间的事件拦截和实时通信。 首先,iOS与H5交互的基础是WebView。在iOS应用中,我们通常使用UIWebView或WKWebView来加载和显示HTML内容。这两个控件提供了JavaScriptCore框架,...
在iOS系统中,电话拦截是一项重要的功能,它旨在帮助用户管理来电,避免不必要的打扰,特别是来自骚扰电话或诈骗电话的侵扰。iOS电话拦截通过内置的屏蔽机制和第三方应用程序实现,为用户提供了一套完整的防骚扰解决...
【iOS5短信拦截小demo】是一个面向iOS平台的示例项目,主要展示了如何在iOS系统中实现短信的拦截功能。这个项目可能适用于开发者或者对iOS系统底层操作感兴趣的用户,特别是那些想要了解或开发类似功能的人。 在iOS...
【标题】"IOS 5 拦截手机短信(需越狱)" 描述了在iOS 5系统上通过越狱手段实现对手机短信的拦截这一技术主题。在苹果的封闭式操作系统环境下,用户通常无法直接访问或修改系统底层功能,但通过越狱,开发者和高级用户...
在iOS开发中,手势交互是用户体验的重要组成部分。本教程聚焦于如何将系统内置的导航控制器(NavController)的返回手势拦截并映射到自定义的手势识别器(GestureRecognizer),特别是全屏返回手势。这一技巧可以让...
Runloop是iOS中的一个核心组件,负责管理应用程序的事件处理。通过在Runloop中添加观察者,我们可以在程序即将退出时获取通知,并采取措施防止奔溃。 以下是一个使用Runloop防止奔溃的例子: ```objc - (void)...
本文将深入探讨如何在Swift中实现系统返回按钮事件的拦截。 首先,我们需要理解的是,iOS中的导航控制器(UINavigationController)默认提供了左侧的返回按钮,这个按钮的行为通常是返回上一级视图控制器。在Swift...
为了实现这一需求,文中提出了通过URL拦截的方法来解决。 首先,我们需要了解iOS中的URL Loading System,这是一个系统级别的网络请求框架,UIWebView正是利用这个系统来加载网页内容。NSURLCache作为URL Loading ...
1、基于NSURLProtocol拦截请求: HTTP 请求开始时,URL 加载系统创建一个合适的 NSURLProtocol 对象处理对应的 URL 请求,因此我们只需写一个继承自 NSURLProtocol 的类,并通过 - registerClass: 方法注册我们的...
"iOS oc拦截网页wkwebview视频内容的demo"是一个示例项目,它展示了如何利用Objective-C(oc)来拦截WKWebView加载的网页中的视频内容。WKWebView是苹果提供的一个高级组件,用于在iOS应用中渲染网页,它相比...
版权声明:本文为CSDN博主「#公众号:iOS逆向」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/z929118967/article/details/115690756
本文将深入探讨如何在代理方法中拦截URL,以实现iOS和JS之间的有效通信。首先,我们需要理解iOS中的WKWebView组件,它是苹果提供的用于加载和呈现Web内容的工具,它支持与JavaScript的深度集成。 ### WKWebView简介...
2、应用场景: 2.1、 自定义请求头的HTTPHeaderField 2.2、针对NSURLSessionConfiguration设置代理IP和端口,让一些特殊的请求走自定义的隧道IP和端口 ...3、原理:利用NSURLProtocol 拦截 HTTP 请求
本文将深入探讨iOS程序异常crash的捕获与拦截机制,以及如何通过源码和工具来实现这一目标。 一、异常处理基础 1. 异常体系:iOS中的异常处理基于Objective-C的异常体系,它通过抛出(throw)、捕获(catch)和...
在iOS开发中,侧滑返回(Swipe Back)是苹果提供的一种常见手势,让用户可以方便地在导航控制器的视图之间切换。然而,在某些特定场景下,如使用ScrollView或地图组件时,侧滑返回可能会与其他交互产生冲突,导致不...
iOS9.3.3骚扰电话拦截黑名单数据库
2、原理:利用NSURLProtocol 拦截 HTTP 请求 3、应用场景:隧道APP请求我们自己接口的都不走隧道、修改请求的HTTPHeaderField,设置代理IP和端口、防抓包(使Thor,Charles,Burp等代理抓包方式全部失效)
3. 触发播放:库可能还会处理播放事件,确保视频在合适的时机开始播放,避免因为自动播放策略而被阻止。 引入JavaScript库的方式有两种常见方法: - 直接在HTML文件中通过`<script>`标签引入库。 - 通过Webview的`...
iOS 网络请求拦截详解 iOS 平台上的网络请求拦截是移动应用开发中的一项重要技术。通过拦截网络请求,可以实现统计所有的网络请求,了解应用程序的网络行为,从而优化应用程序的性能和用户体验。在本文中,我们将...