UITouch的解释及作用
- 一根手指对应一个UITouch对象
- 当用户用一根手指触摸屏幕时,会创建一个与手指相关联的UITouch对象
- 保存着跟手指相关的信息,比如触摸的位置、时间、阶段
- 当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置
- 当手指离开屏幕时,系统会销毁相应的UITouch对象
UITouch的方法
- (CGPoint)locationInView:(UIView *)view;
返回值表示触摸在view上的位置
这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0))
调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
- (CGPoint)previousLocationInView:(UIView *)view;
该方法记录了前一个触摸点的位置
UITouch的属性
触摸产生时所处的窗口
@property(nonatomic,readonly,retain) UIWindow *window;
触摸产生时所处的视图
@property(nonatomic,readonly,retain) UIView *view;
UIEvent的解释及作用
每产生一个事件,就会产生一个UIEvent对象
UIEvent:称为事件对象,记录事件产生的时刻和类型
UIEvent还提供了相应的方法可以获得在某个view上面的触摸对象(UITouch)
UIEvent的属性
@property(nonatomic,readonly) UIEventType type;
@property(nonatomic,readonly) UIEventSubtype subtype;
@property(nonatomic,readonly) NSTimeInterval timestamp;
touches和event参数
一次完整的触摸过程,会经历3个状态:
触摸开始:- (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
4个触摸事件处理方法中,都有NSSet *touches和UIEvent *event两个参数
一次完整的触摸过程中,只会产生一个事件对象,4个触摸方法都是同一个event参数
如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象
如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象
根据touches中UITouch的个数可以判断出是单点触摸还是多点触摸
事件的产生和传递
发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中
-
UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)
主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步
找到合适的视图控件后,就会调用视图控件的touches方法( touchesBegan…
touchesMoved…
touchedEnded…)来作具体的事件处理
触摸事件处理的详细过程
用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件
找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理(touchesBegan…,touchesMoved…,touchedEnded…)
touchesBegan方法的默认做法是将事件顺着响应者链条向上传递,将事件交给上一个响应者进行处理
响应者对象
- 在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接收并处理事件。我们称之为“响应者对象”(注意:之前说的类对象其实就是类,例如UIImageView类对象,其实就是UIImageView类,而不是UIImageView类创建出来的对象)
- UIApplication、UIViewController、UIView都继承自UIResponder,因此它们都是响应者对象,都能够接收并处理事件
UIResponder内部提供了以下方法来处理事件.因为UIApplication、UIViewController、UIView都继承UIResponder,所以这三个都能拥有以下方法(事件)
触摸事件
- (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;
加速计事件
- (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不接收触摸事件的三种情况
- 1:userInteractionEnabled = NO(不接收用户交互)
- 2:hidden = YES(隐藏)
- 3:alpha = 0.0 ~ 0.01(透明)
- 提示:UIImageView的userInteractionEnabled默认就是NO,因此UIImageView以及它的子控件默认是不能接收触摸事件的
触摸事件的传递
- 触摸事件的传递是从父控件传递到子控件(最里面的就是父控件,最外面的就是子控件)
- 点击了绿色的view,事件传递的过程:UIApplication -> UIWindow -> 白色 -> 绿色
- 点击了蓝色的view,事件传递的过程:UIApplication -> UIWindow -> 白色 -> 橙色 -> 蓝色
- 点击了黄色的view,事件传递的过程:UIApplication -> UIWindow -> 白色 -> 橙色 -> 蓝色 -> 黄色
- 触摸事件的传递过程中,如果父控件不能接收触摸事件(上面的1,2,3),那么子控件也不可能接收到触摸事件(掌握)。
- 这种情况接收到触摸事件的只有父控件的父控件。例如蓝色userInteractionEnabled = NO,那么子控件黄色就不能接收到触摸事件,此时点击黄色区域,提示橙色被点击,而不是提示黄色被点击。
- 详细解释:点击黄色,触摸事件的传递过程是从父控件到子控件,当传递到橙色时,橙色要传递给蓝色,因为蓝色不接收用户交互,所以触摸事件就在经过橙色view停止,不再进行传递。
- 如果父控件隐藏,那么里面的子控件也跟着隐藏
- 如果父控件透明(alpha),那么子控件也跟着透明。不过在storyboard中看不出子控件也跟着透明,运行时才会看到
- 如何找到最合适的控件来处理事件(利用事件的传递过程)?
- A:自己是否能接收触摸事件?
- B:触摸点是否在自己身上?(触摸点在子控件身上,若这个子控件有父控件,那么这个触摸点也在父控件身上)
- C:从后往前遍历子控件,重复前面的两个步骤
- D:如果没有符合条件的子控件,那么就自己最适合处理
事例+附图
- 点击(触摸)黄色的View,找到最合适的控件来处理事件的详细步骤:
- UIApplication从队列当中取出事件
- 事件先传递给UIWindow
- UIWindow满足A,B,此时事件传递给控制器的白色View
- 白色的View满足A,B,此时白色的View把事件传递给橙色View(因为白色View子控件是绿色和橙色,在storyboard中,橙色是后添加的,后添加的先遍历)
- 橙色的View满足A,B,此时橙色的view把事件传递给红色的View
- 红色的View满足A,不满足B。然后橙色的view把事件传递给蓝色的View
- 蓝色的View满足A,B。蓝色的View把事件传递给黄色的View
- 黄色的View满足A,B,但是没有子控件,所以C,D不满足,最终蓝色的View来处理事件,所以执行蓝色的View点击
精华总结1:
- 判断点在不在白色的View身上,一定要以白色View的左上角为坐标原点,不能以父控件(红色View)的左上角为原点判断,
- 判断依据:判断点A(x,y)是否大于当前View(白色View)的宽度和高度,如果有一个不满足,则点不在当前的View上。例如如果A的坐标为(15,4),而白色View的宽高都为28,所以满足条件,可得知A点在白色View上
- 如果以红色View的左上角为坐标原点,判断当前点A是在白色View上的话,A点的坐标为(50,30),而白色的View的宽高都为28,所以不满足条件,由此判断A点不在白色View。
-判断点在不在白色View身上的官方语言是判断当前点在不在方法调用者身上。就是说,当前方法的调用者:白色View - 精华:hitTest方法内部实现了A,B,C,D(详细看下面的代码)。 所以说方法的调用者就是 执行白色View的hitTest方法
精华总结2(结合总结1):
事件传递到红色View时,执行红色view的hitTest方法,hitTest方法里面的A,B,C,执行到C,从后往前遍历子控件,子控件就是白色View,执行白色View(截图16-06)的hitTest方法,参数中的point是父控件红色View的传递过来的坐标,所以传递过来的point要转换成当前方法所在的View的piont
转换规则:当前点的位置不变,变得只是坐标原点(父控件的坐标原点转成了子控件的坐标原点。)然后计算点的位置距离这个子控件的x,y坐标,这个坐标就是这个点在子控件的point,如果此时point大于这个子控件的宽和高,那么point不在这个子控件上
代码方式转换:如16-06代码截图(结合 总结1,总结2)
- 详细图解 (结合 总结1,总结2)
HitTest方法底层实现
- 精华:hitTest方法内部实现了A,B,C,D
//什么时候调用:当一个事件传递给当前View时调用
//作用:寻找最适合的View
//返回值:找到的最适合的View,如果没有找到最适合的View,返回是一个nil.
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
//1.判断当前View能否接收事件
if(self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) {
return nil;
}
//2.判断当前点在不在View身上
if (![self pointInside:point withEvent:event]) {
return nil;
}
//3.从后往前遍历自己的子控件.让自己的子控件寻找最适合的View;
int count = (int)self.subviews.count;
for (int i = count - 1; i >= 0; i--) {
//取出子控件
UIView *childV = self.subviews[i];
//把当前点的坐标(父控件)系转换成子控件身上的坐标系
CGPoint childP = [self convertPoint:point toView:childV];
UIView *fitView = [childV hitTest:childP withEvent:event];
if (fitView) {
return fitView;
}
}
//4.没有找到比自己更适合的view,那么它自己就是最适合的view
return self;
}
//什么时候调用:在hitTest方法当中调用
//作用:判断当前点在不在方法调用者身上.
//返回值:如果返回yes,在View身,如果 为NO,不在View身上.
//传入的点point,必须得与方法调用者在同一个坐标系(即point是父控件中的坐标,必须转换成子控件身上的坐标,即坐标原点要由父控件变成子控件的坐标原点,唯一不变的是坐标代表的距离不变,这个距离要和当前子控件的宽高进行比较,如果都小于的话,才能证明这个带你在子控件身上).
//- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
// return YES;
//}
@end
hitTest方法练习
CharBtn.h文件
#import <UIKit/UIKit.h>
@interface ChatBtn : UIButton
/** <#注释#>*/
@property (nonatomic, weak) UIButton *popBtn;
@end
CharBtn.m文件
#import "ChatBtn.h"
@implementation ChatBtn
//当第一次点击"弹出对话框"时,按照事件的传递过程:APPLication--->UIWindow--->"弹出对话框"按钮,执行到"弹出对话框"按钮时候,所以执行"弹出对话框"重写的hitTest方法(重写只为拦截之用,但还是会执行btnclick方法),在这个方法里面进行判断(A,B,C,D步骤),结果返回系统的做法return [super hitTest:point withEvent:event];所以跳出hitTest方法之后,就会执行btnclick方法.在里面创建一个btn控件并添加到父控件"弹出对话框",并把这个控件赋值给ChatBtn类型的对象sender的popBtn属性,从而popBtn就拿到了btn,拥有了btn的图片以及尺寸存储在内存中
//第一次点击"弹出对话框"时打印两行null的原因: 第一行:因为没有给self.popBtn赋值,所以self.popBtn为null.第二行:self.popBtn为null,返回的是一个方法,所以再次执行这个重写的方法就返回了,打印第二行null。
//第二次点击"对话框",按照事件的传递过程:APPLication--->UIWindow--->"弹出对话框"按钮,执行到"弹出对话框"按钮时候,"弹出对话框"的hitTest:方法同样会进行拦截(虽然点击的是"对话框",但是因为事件是有传递过程的,每传递到一个控件时,都会执行控件的hitTest方法),self.popBtn此时已经被对话框(小孩)btn赋值,所以就会执行return self.popBtn;就会执行“对话框”按钮的响应事件,即点击“对话框”按钮有反应(popBtn里面之前已经存储了不惦记btn的图片和点击btn时图片的状态以及尺寸等).注意:不会来到btnClick方法,因为你点击的是"对话框“按钮(点在对话框"按钮身上).如果返回的是系统原有的做法return [super hitTest:point withEvent:event];,那肯定是执行btnClick方法(就相当于没重写htiTest方法,即:点击谁,就执行谁的btnClick)。
//如果 一个子控件超出父控件的大小, 默认情况下是不能处理的事件.
//点击"弹出对话框"按钮的子控件是 "对话框"按钮. 并且子控件超出了父控件的范围
//精华:点击对话框按钮,事件传递过程:APPLication--->UIWindow--->按钮,执行到按钮时,在按钮的hitTest方法中先判断出按钮能接收事件,但是点不在按钮身上(因为子控件超出了父控件的大小,并且当前点是父控件未包含的区域,)所以不会执行第三步(不会从后往前遍历按钮的子控件,),就会执行第四步:没有符合条件的子控件,那么就自己最适合处理,所以"弹出对话框"按钮自己处理
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
//通过sender.popBtn = btn;拿到对话框按钮btn,在这个类中用self.popBtn表示
//point是点击"对话框"按钮中的某个点(当然第一次点击"弹出对话框"时,point指的就是那时的点,第二次点击时,才是“对话框”按钮的点)
NSLog(@"%@--",self.popBtn);
//如果点在自己的子控件上面.让子控件处理事件.
if (self.popBtn) {
//把当前点转换成"对话框"坐标系上面的点
CGPoint popBtnP = [self convertPoint:point toView:self.popBtn];
if ( [self.popBtn pointInside:popBtnP withEvent:event]) {//判断点在不在对话框"按钮身上
return self.popBtn;
}else {
return [super hitTest:point withEvent:event];//点不在对话框"按钮身上,保持系统原有的做法
}
}
return [super hitTest:point withEvent:event];
}
//
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//获取UITouch
UITouch *touch = [touches anyObject];
//获取当前手指的点
CGPoint curP = [touch locationInView:self];
//获取上一个手指所在的点
CGPoint preP = [touch previousLocationInView:self];
//计算偏移量
CGFloat offsetX = curP.x - preP.x;
CGFloat offsetY =curP.y - preP.y;
//平移
self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
}
@end
ViewController.h文件
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@end
ViewController.m文件
#import "ViewController.h"
#import "ChatBtn.h"
@interface ViewController ()
@end
@implementation ViewController
//重:因为"弹出对话框"按钮已经和ChatBtn绑定,所以点击"弹出对话框",传递的sender的类型是CharBtn类型
- (IBAction)btnClick:(ChatBtn *)sender {
//添加一个子控件
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
[btn setImage:[UIImage imageNamed:@"对话框"] forState:UIControlStateNormal];
//UIControlStateHighlighted可以添加图片,UIControlStateSelected不可以添加图片,只能被选中
[btn setImage:[UIImage imageNamed:@"小孩"] forState:UIControlStateHighlighted];
btn.frame = CGRectMake(50, -80, 100, 80);
//btn是UIButton类型的指针,popBtn也是UIButton类型的指针,所以可以赋值
sender.popBtn =btn;
//sender.popBtn =(ChatBtn *)btn;
[sender addSubview:btn];
}
@end
Main.storyboard文件
触摸事件处理的详细过程
用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件
找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理(touchesBegan…,touchesMoved…,touchedEnded…)
touchesBegan方法的默认做法是将事件顺着响应者链条向上传递(父类),将事件交给上一个响应者进行处理
事件响应代码
- 上一个响应者
- 1.点击当前的View,如果当前的View是控制器的View.那么它的上一个响应者就是所在的控制器
- 2.如果不是控制器View,那么它的上一个响应者就是它的父控件
//重写系统的方法--->保持系统默认做法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];//保持系统默认做法
}
//重写系统的方法--->处理事件,没有保持系统默认做法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%@---touch",[self class]); //处理事件(打印当前类),没有保持系统默认做法
}
//重写系统的方法--->处理事件,并保持系统默认做法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%@---touch",[self class]);//处理事件(打印当前类)
[super touchesBegan:touches withEvent:event];//保持系统默认做法,即顺着响应者链条传递给上一个响应者(父类)。
}
- E:左侧的图:事件传递到了绿色view
- 如果绿色view不处理(事件),就会保持系统默认的做法,沿着响应者链条(1)向上传递给黄色View
- 如果黄色view也不处理(事件),就会保持系统默认的做法,沿着响应者链条(2)向上传递给青色View(控制器的view)。
- 如果青色view(控制器的view)也不处理(事件),就会保持系统默认的做法,沿着响应者链条(3)向上传递给View Controlleer控制器
- 如果View Controlleer控制器也不处理事件,就会保持系统默认的做法,沿着响应者链条(4)向上传递给window(窗口)
- 如果window(窗口)也不处理事件,就会保持系统默认的做法,沿着响应者链条(5)向上传递给Application
- 此时Application发现没有人处理事件,那么这个事件就会被抛弃掉
- F:右侧的图:事件传递到了绿色view
- 如果绿色view不处理(事件),就会保持系统默认的做法,沿着响应者链条(1)向上传递给黄色View(子控制器的view)
- 如果黄色view(子控制器的view)也不处理,就会保持系统默认的做法,沿着响应者链条(2)向上传递给iew Controlleer控制器
- 如果View Controlleer控制器也不处理事件,就会保持系统默认的做法,沿着响应者链条(3)向上传递给青色view(父控制器的view)
- 如果青色view(父控制器的view)也不处理(事件),就会保持系统默认的做法,沿着响应者链条向上传递给View Controlleer控制器
- 如果View Controlleer控制器也不处理事件,就会保持系统默认的做法,沿着响应者链条(4)向上传递window(窗口)
- 如果window(窗口)也不处理事件,就会保持系统默认的做法,沿着响应者链条(5)向上传递给Application
- 此时Application发现没有人处理事件,那么这个事件就会被抛弃掉
事件的传递和响应过程(看ABCDEF): 传递:在hitTest方法中找到适合的view(A,B,C,D) 响应:在toucherBegan方法中处理事件(E或F)
UIGestureRecognizer(手势识别器)
- UIGestureRecognizer是一个抽象类,定义了所有手势的基本行为,使用它的子类才能处理具体的手势
- UITapGestureRecognizer(敲击)
- UIPinchGestureRecognizer(捏合,用于缩放)
- UIPanGestureRecognizer(拖拽)
- UISwipeGestureRecognizer(轻扫)
- UIRotationGestureRecognizer(旋转)
- UILongPressGestureRecognizer(长按)
- 如果将手势识别的代理方法(下面的方法):支持多个手势设置为NO,那么系统默认只能识别一个手势.
- 例如:果你添加了2个手势(旋转,缩放)到代码中,当你运行时,旋转图片的时候,不能缩放, 缩放图片的时候不能旋转
//是否允许支持多个手势
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return NO;
}
//旋转,缩放,位移中:
//第一个参数是上次(之前)的位置
//第二个参数(第三个参数)是距离上次(之前)位置的偏移量
//等号左边的self.imageV.transform是(这次)现在的位置(现在的位置是根据右边的参数计算出来的位置)
//旋转
self.imageV.transform = CGAffineTransformRotate(self.imageV.transform, rotation.rotation);
//缩放
self.imageV.transform = CGAffineTransformScale(self.imageV.transform, pinch.scale, pinch.scale);
//位移
self.imageV.transform = CGAffineTransformTranslate(self.imageV.transform, transP.x, transP.y);
手势识别
#import "ViewController.h"
@interface ViewController ()<UIGestureRecognizerDelegate>
@property (weak, nonatomic) IBOutlet UIImageView *imageV;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.imageV.userInteractionEnabled = YES;
[self panGes];
}
//是否允许支持多个手势
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return NO;
}
//拖动手势
- (void)panGes {
//1.创建手势
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
//2.添加手势
[self.imageV addGestureRecognizer:pan];
}
//当拖动时调用
- (void)pan:(UIPanGestureRecognizer *)pan {
//NSLog(@"%s",__func__);
//获取偏移量(相对于最原始的值),用transP表示。例如偏移量为100(相对于0来说)(其实是有x和y值)
//pan.view是当前手势的所在view。所在的view是UIImageView。所以pan.view等价于self.imageV;
CGPoint transP = [pan translationInView:pan.view];
NSLog(@"transP=%@",NSStringFromCGPoint(transP));//1
//CGAffineTransformTranslate的第一个参数是记录上次的位置,第二个参数和第三个参数是偏移量x和偏移量y,只有知道上次的位置和偏移量,才能知道下次的位置
//如果屏蔽2的代码,当你拖动图片的时候,其实系统底层已经记录了你拖动到了什么位置,只是不显示。通过1的打印就可以知道,只是我们通过2将拖动到的位置显示了出来,仅此而已
self.imageV.transform = CGAffineTransformTranslate(self.imageV.transform, transP.x, transP.y);//2
//复位(相对于上一次的操作) ,把偏移量为1进行清零,那么下次获取偏移量时,就相对于0来说,如果不清零,下次真实的偏移量若为200时,就会走100+200=300的距离,但是实际的偏移量为200
[pan setTranslation:CGPointZero inView:pan.view];
}
@end