iOS事件
- 在用户使用app过程中,会产生各种各样的事件
- iOS中的事件可以分为3大类型
- 触摸事件
- 加速计事件:摇一摇
- 远程控制事件:利用耳机控制音量
响应者对象
- 在i0S中不是任何对象都能处理事件,只有继承了
UIResponder
的对象才能接收并处理事件。我们称之为“响应者对象” -
UIApplication
、UIViewController
、UIView
都继承自UIResponder
,因此它们都是响应者对象,都能够接收并处理事件
UIResponder
UIResponder内部提供了以下方法来处理事件,触摸事件
- 触摸事件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withE*ent:(UIEvent *)event;
- (void)touchesCancelted:(NSSet *)ouches withivent:(UIEvent *)event;
- 加速计事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- 远程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;
UIView的触摸事件处理
UIView是 UIResponder
的子类,可以覆盖下列4个方法处理不同的触摸事件
- 一根或者多根手指开始触摸view,系统会自动调用view的下面方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
- 一根或者多根手指在view上移动,系统会自动调用view的下面方法(随着手指的移动,会持续调用该方法)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
- 一根或者多根手指离开view,系统会自动调用view的下面方法
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
- 触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,系统会自动调用view的下面方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
UITouch
- 当用户用一根手指触摸屏幕时,会创建一个与手指相关联的
UITouch
对象 - 一根手指对应一个
UITouch
对象 -
UITouch
的作用- 保存着跟手指相关的信息,比如触摸的位置、时间、阶段
- 当手指移动时,系统会更新同一个
UITouch
对象,使之能够一直保存该手指的触摸位置 - 当手指离开屏幕时,系统会销毁相应的
UITouch
对象 - UIView默认只支持单个手指触摸事件,如果要支持多个手指触摸,需要设置属性
multipleTouchEnabled
的值为YES - 提示:iPhone开发中,要以免使用双击事件!
UITouch的属性
- 触摸产生时所处的窗口
@property (nonatomic,readon ly,retain) UIWindow *window;
- 触摸产生时所处的视图
@property(nonatomic,readonly,retain) UIView *view;
- 短时间内点按屏幕的次数,可以根据tapCoun、判斯单击、双击或更多的点击
@property(nonatomic,readonly) NSUInteger tapCount;
- 记录了触摸事件产生或变化时的时间,单位是秒
@property(nonatomic,readonly) NSTimeInterval timestamp;
- 当前触摸事件所处的状态
@property(nonatomic,readonly) UITouchPhase phase;
UITouch的方法
- (CGPoint)locationInView:(UIView *)view;
返回值表示触摸在view上的位置
这里返回的位置是针对view的坐标系的(以view的左上角为原点(O,0))调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
- (CGPoint)previousLocationInView:(UIView *)view;
该方法记录了前一个触摸点的位置
实例
实现如下图所示的效果
在自定义的 View
对象里,代码如下:
@implementation RedView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%s",__func__);
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%@",NSStringFromSelector(_cmd));
//获取UITouch对象
UITouch *touch = [touches anyObject];
//获取当前点
CGPoint currentPo = [touch locationInView:self];
//获取上一个点
CGPoint previewPo = [touch previousLocationInView:self];
//获取x轴偏移量
CGFloat offsetX = currentPo.x - previewPo.x;
//获取y轴偏移量
CGFloat offsetY = currentPo.y - previewPo.y;
self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%s",__func__);
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%s",__func__);
}
@end
UIEvent
- 每产生一个事件,就会产生一个UIEvent对象
- UIEvent:称为事件对象,记录事件产生的时刻和类型
常见属性
事件类型
@property(nonatomic,readonly) UIEventType type
@property(nonatomic,readonly) UIEventSubtype subtype;
事件产生的时间
@property(nonatomic,readonly) NSTimeInterval timestamp;
UIEvent还提供了相应的方法可以获得在某个view土面的触摸对象(UITouch)
当手指触摸屏幕的时候即生成一个 UITouch
对象和 UIEvent
对象
事件的产生和传递
- 发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中
- UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)
- 主窗口会在视图层次结构中
找到一个最合适的视图来处理触摸事件
,这也是整个事件处理过程的第一步 - 找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理,即:
- touchesBegin……
- touchesMoved……
- touchesEnded……
UIView不能接收触摸事件的三种情况
- 不接收用户交互:userInteractionEnabled = NO,提示:UIImageView的userInteractionEnabled 默认就是NO,因此UIImageView以及它的子控件默认是不能接收触摸事件的。
- 隐藏:hidden = YES;
- 透明:alpha = 0.0 ~ 0.01;
如何找到最合适的控件来处理事件?
- 自己是否能接收触摸事件?
- 触摸点是否在自己身上?
- 从后往前遍历子控件,重复前面的整个步骤
hitTest底层实现
- hitTest在事件传递的时候调用,调用时机是当事件传递给控件的时候,就会调用这个方法,作用是寻找最合适的view
- pointInside方法的作用是判断当前这个点在不在方法调用者(控件)上
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
//判断当前控件能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01)
{
return nil;
}
//判断点在不在当前控件
if ([self pointInside:point withEvent:event])
{
return nil;
}
/**自己写的
//从后往前遍历子控件
__block UIView *targetView = nil;
[self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
CGPoint subPoint = [self convertPoint:point toView:obj];
UIView *fitView = [obj hitTest:subPoint withEvent:event];
if (fitView) {
targetView = fitView;
*stop = YES;
}
}];
if (targetView != nil)
{
return targetView;
}
*/
//从后向前遍历子控件
for (NSInteger i = [self.subviews count] - 1; i >= 0; i--)
{
UIView *subview = self.subviews[i];
//把当前控件上的坐标系统转换成子控件上的坐标系
CGPoint subPoint = [self convertPoint:point toView:subview];
UIView *fitView = [subview hitTest:subPoint withEvent:event];
if (fitView ) {
//找到最合适的view
return fitView;
}
}
return self;
}
hitTest练习1
下面的图片中绿色的图层处于上方,按钮处于下方,当用户点击绿色视图和按钮重叠的区域时,要求响应的是处于下方的按钮。
实现效果.gif- 实现流程步骤,在自定义的绿色视图的类中,重写它的hitTest方法,然后刚当前坐标转换成按钮的,如果点击的区域在按钮的范围内,则返回按钮,否则按默认的来处理,完整的代码如下:
#import "GreenView.h"
@interface GreenView ()
//注意这里从xib或者storyBoad直接拖拽可能不能成功,需要先手写下面这段代码然后反向拖拽对应xib或者storyBoard里的控件
@property (nonatomic, weak) IBOutlet UIButton *button;
@end
@implementation GreenView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
//当前坐标系上的点转换到按钮上的点
CGPoint btnPoint = [self convertPoint:point toView:self.button];
//判断点在不在按钮上
BOOL inside = [self.button pointInside:btnPoint withEvent:event];
if (inside) {
return self.button;
}
else{
return [super hitTest:point withEvent:event];
}
}
@end
hitTest练习2
实现如下场景,要求:
- 点击 “弹出对话框” 时出现新的图片
- 拖拽 “弹出对话框” 时新的图片也跟着拖动,
- 点击新出现的图片时,高亮状态下图片变化
详细的效果请参考下图
hitTest练习2.gif完整的代码如下所示:
#import <UIKit/UIKit.h>
@interface CustomBtn : UIButton
//出现的新的图片按钮
@property (nonatomic, strong) UIButton *subBtn;
@end
#import "CustomBtn.h"
@implementation CustomBtn
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint position = [touch locationInView:self];
CGPoint previsePoint = [touch previousLocationInView:self];
CGFloat xOffset = position.x - previsePoint.x;
CGFloat yOffset = position.y - previsePoint.y;
CGPoint center = self.center;
center.x += xOffset;
center.y += yOffset;
self.center = center;
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
CGPoint subPoint = [self convertPoint:point toView:self.subBtn];
if ([self.subBtn pointInside:subPoint withEvent:event])
{
return self.subBtn;
}
else{
return [super hitTest:point withEvent:event];
}
}
@end
#import "CustomBtn.h"
#import "ThirdViewController.h"
@interface ThirdViewController ()
@end
@implementation ThirdViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
}
//点击 “弹出对话框” 按钮时触发此方法
- (IBAction)customBtnClicked:(CustomBtn *)sender
{
UIButton *subBtn = [UIButton buttonWithType:UIButtonTypeCustom];
[subBtn setBackgroundImage:[UIImage imageNamed:@"对话框"] forState:UIControlStateNormal];
[subBtn setBackgroundImage:[UIImage imageNamed:@"小孩"] forState:UIControlStateHighlighted];
subBtn.bounds = CGRectMake(0, 0, 150, 150);
subBtn.center = CGPointMake(75, -75);
[sender addSubview:subBtn];
sender.subBtn = subBtn;
}
@end
事件的响应
触摸事件处理的详细过程
- 用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件
- 找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理
- touchesBegan…
- touchesMoved…
- touchedEnded…
- 这些touches方法的
默认做法
是将事件顺着响应者链条
向上传递,将事件交给上一个响应者进行处理,即自己不处理(不实现touches...
方法)而交由上一个响应者处理,个人理解的是:如果控件处理了,则不再向上抛
- 事件传递的完整过程
- 先将事件对象由上往下传递(由父控件传递给子控件),找到最合适的控件来处理这个事件。
- 调用最合适控件的touches....方法
- 如果调用了[super touches....];就会将事件顺着响应者链条往上传递,传递给上一个响应者
- 接着就会调上一个响应者的touches....方法
用下面例子来进行演示
响应者链在该图中,YellowView
的父视图是 BlueView
,而 BlueView
的父视图是 OrangeView
,依次是 WhiteView
,当点击 YellowView
时,会首先检测 YellowView
是否实现了 touches...
等方法,如果其没有实现则事件传给 BlueView
,如果其实现了 touches...
方法,则调用完 touches...
方法,事件结束,如果其不仅实现了 touches...
方法,并且在 touches...
方法内部调用了 [super touches...]
,则事件继续向上传递给 BlueView
,依次类推,利用这个特性可以做到多个视图触发同一事件。
代码如下:
#import "YellowView.h"
@implementation YellowView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%s",__func__);
[super touchesBegan:touches withEvent:event];
}
@end
#import "BlueView.h"
@implementation BlueView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%s",__func__);
[super touchesBegan:touches withEvent:event];
}
@end
#import "OrangeView.h"
@implementation OrangeView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%s",__func__);
[super touchesBegan:touches withEvent:event];
}
@end
#import "WhiteView.h"
@implementation WhiteView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%s",__func__);
[super touchesBegan:touches withEvent:event];
}
@end
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%s",__func__);
}
@end
当事件传递给白色视图以后,白色视图会继续向上传递给它的父视图,即控制器的view,因为这个视图不处理,则继续传递给当前控制器,输出结果如下所示:
事件传递输出结果- 如何判断上一个响应者
- 如果当前这个view是控制器的view,那么
控制器
就是上一个响应者 - 如果当前这个view不是控制器的view,那么
父控件
就是上一个响应者
网友评论