一.事件
1.iOS三大事件包含触摸事件,设备移动事件,远程控制事件
2.iOS规定只有继承UIResponder的类才能处理事件
AppDelegate,,UIWindow,UIViewController,UIView都是继承它,具有处理事件的能力。
AppDelegate处理UIApplication的事件,UIViewController处理它的View的事件。
那么,我们看一下UIResponder中部分方法
//触摸事件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//设备移动事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event
//远程控制事件
- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event
3.触摸事件
一个触摸点(一根手指)对应一个UITouch对象,每一个UITouch会产生一个UIEvent的对象,包含事件类型等信息,所以参数是NSSet<UITouch *> *类型。
比如写一个View随手指拖动的效果
//继承UIView的ZSView,重写touchmove方法
-(void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
UITouch * touch=[touches anyObject];
CGPoint point=[touch locationInView:self];
CGPoint prepoint=[touch previousLocationInView:self];
NSLog(@"%@-%@",NSStringFromCGPoint(point),NSStringFromCGPoint(prepoint));
CGFloat offsentx=point.x-prepoint.x;
CGFloat offsenty=point.y-prepoint.y;
[self setTransform:CGAffineTransformTranslate(self.transform, offsentx, offsenty)];
//如果传nil,返回相对于当前window的CGPoint
// CGPoint point2=[touch locationInView:nil];
// CGPoint prepoint2=[touch previousLocationInView:nil];
// NSLog(@"2:%@-%@",NSStringFromCGPoint(point2),NSStringFromCGPoint(prepoint2));
//NSLog(@"%s",__func__);
}
二.事件传递
触摸事件的传递是从父控件传递到子控件,然后找到最合适的控件的过程;
UIApplication->UIWindow->UIView
如果父控件不接受事件,所以子控件都不能接受事件;
如何找到最合适的控件处理事件呢?
a.是否可以接受触摸事件
b.是否在自己身上
c.重复往前遍历子控件,重复ab
d.如果没有符合条件的子控件,就自己处理
代码实现就是hitTest的内部处理
//当前事件传递给当前View,当前View的hitTest方法会被调用,去寻找最合适的UIView,返回值就是最合适的UIView,然后调用该UIView的touches方法
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
//1.是否可以接受触摸事件
//2.是否在自己身上(pointInside方法)
//3.从后往前遍历子控件,重复12
//4.如果没有符合条件的子控件,就自己处理
return [super hitTest:point withEvent:event];
}
//判断点击点在不在当前View身上,在hitTest内部调用(第2步)
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
//point表示当前触摸点
return NO;
}
找几个例子一下就能明白:
1.重写各个View的touchesBegan
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%@--touches",NSStringFromClass(self.class));
}
结果是:点击哪个View,哪个View的touchesBegan方法被调用(hitTest的自动实现)
2.重写各个View的hitTest,但是仅仅调用父类方法(相当于没重写)
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
return [super hitTest:point withEvent:event];
}
结果是:点击哪个View,哪个View的touchesBegan方法被调用(hitTest的自动实现)
3.重写红色View的hitTest,返回self
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
//如果事件传递到红色View,返回最合适UIView为自己
return self;
}
结果是:点击灰色View和红色View,都是红色View的touchesBegan方法被调用
分析:点击灰色View,满足ab,再从后往前遍历子View,遍历到第一个红色View,返回最优响应者是红色View则停止遍历,点击红色也是同理。
4.重写灰色View的hitTest,返回第一个自View
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
return self.subviews[0];
}
结果是:点击灰色View及其子View,或者是蓝色View都是绿色View的touchesBegan方法被调用
分析:window遍历到vc.view的子控件时,满足ab,再从后遍历子View,遍历到第一个灰色View,返回最优响应者是其中第一个子控件也就是绿色View,点击其他View也是同理。
5.重写绿色View的hitTest,返回nil
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
return nil;
}
结果是:点击绿色和黑色都会让灰色View的touchesBegan方法被调用(穿透效果)
分析:点击绿色和黑色时,事件传递到绿色View,触发hitTest,返回空,也就是没和合适的View,所以回到上一层hitTest,也就是灰色View的hitTest,返回自己的View。
6.重写红色View的pointInside,返回YES
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
return YES;
}
结果是:点击灰色View及其子View,都是红色View的touchesBegan方法被调用
分析:点击灰色View,满足ab,倒叙遍历子控件,遍历到红色View时,判断点击在红色区域内部,返回红色View;点击绿色或者黑色View,同样会遍历到红色View时被阻止向下遍历,所以也是红色ViewtouchesBegan方法被调用。
7.重写绿色View的pointInside,返回YES
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
return YES;
}
结果是:点击灰色View,调用绿色View的touchesBegan,其余都正常
分析:自己分析吧,懒得写了。
响应链
当我们确定谁是那个最适合的UIView的时候,怎么构成一个响应链呢。
重写每个UIView的touchesBegan,调用父类touchesBegan方法
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%@--touches",NSStringFromClass(self.class));
[super touchesBegan:touches withEvent:event];
}
结果是:
subView touches
superView touches
uiviewcontroller-view touches
window-touches
application-touches
也就是找到最合适处理事件的控件之后,调用touches,然后向上传递,这些响应者连接在一起构成了事件响应链。如果subview重写touches,就来到subview的touches,如果不重写,就来到superview的touches,如果superview不重写,就来到uiviewcontroller的touches,如果uiviewcontroller不重写,就来到window的touches,如果window不重写,就来到application(appdelegate)的touches,如果都不重写,这个事件就抛弃了。
脑子里回想两件事情,点击APP上一个按钮,如何找到被点击按钮的(命中测试),又如何再找到之后对事件做响应处理的(响应链传递),更容易帮你理解响应链的整体概念。
总结事件传递的完整过程:
a.先将事件由上向下(从父控件向子控件)传递,找到最合适处理事件的控件。
b.调用最合适控件的touches..方法
c.如果调用[super touches]方法,就会将事件顺着响应链条向上传递,传递给上一个响应者。
d.接着调用上一个响应者的touches方法
三.应用
最近发现触及到事件响应链的应用场景的两个问题,第一个是子View的大小超出父View的大小,但是还需要实现点击子View的效果(默认是不能点击超出父View区域的);第二个问题是子View覆盖在父View上,但是要实现穿透子View去响应父View点击事件,针对这两个问题,先说一下解决方案,再看一下响应链是如何做到的。
1.子View超出父View的情况:
ZSView * view =[[ZSView alloc]initWithFrame:CGRectMake(200, 400, 50, 50)];
view.backgroundColor=[UIColor lightGrayColor];
[self.view addSubview:view];
[view setUserInteractionEnabled:YES];
//UIButton * btn = [[UIButton alloc]initWithFrame:CGRectMake(100, -100, 200, 200)];
UIButton * btn = [[UIButton alloc]initWithFrame:view.bounds];
CGRect frame = btn.frame;
frame.size.width=100;
btn.frame=frame;
btn.center=CGPointMake(CGRectGetMidX(view.bounds), -CGRectGetMidY(btn.bounds) - CGRectGetMidY(view.bounds)+10);
btn.backgroundColor=[UIColor orangeColor];
[btn addTarget:self action:@selector(clickbtn) forControlEvents:UIControlEventTouchUpInside];
[view addSubview:btn];
重写父View的pointInside方法
@implementation ZSView
/*
如果点击区域在ZSView的范围内,point为正数,返回YES,否则对应x,y为负数,返回NO。所以我们重写该方法,把超出部分的point返回YES就可以,或者把超出子View的区域转换成自己View的坐标系,也就point为正数了。
*/
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
NSArray *subViews = self.subviews;
if ([subViews count] > 0)
{
UIView *subview = [subViews objectAtIndex:0];
if ([subview pointInside:[self convertPoint:point toView:subview] withEvent:event])
{
return YES;
}
}
if (point.x > 0 && point.x < self.frame.size.width && point.y > 0 && point.y < self.frame.size.height)
{
return YES;
}
return NO;
}
2.穿透子View点击父View
UIButton * btn = [[UIButton alloc]initWithFrame:CGRectMake(0, 0, 300, 300)];
btn.backgroundColor=[UIColor redColor];
[btn addTarget:self action:@selector(clickBtn) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
ZSTableView * tableview = [[ZSTableView alloc]initWithFrame:CGRectMake(50, 50, 200, 200)];
[tableview setUserInteractionEnabled:NO];
tableview.backgroundColor=[UIColor lightGrayColor];
tableview.delegate=self;
tableview.dataSource=self;
[btn addSubview:tableview];
重写子View(ZSTableView)的hitTest方法
-(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView *hitView =[super hitTest:point withEvent:event];
if(hitView == self){
//自动将事件传递到上一层
return nil;
}
return hitView;
}
还有同学举到其他例子:
//www.greatytc.com/p/d8512dff2b3e
四.手势
1.手势分类
UITapGestureRecognizer 轻拍手势
UISwipeGestureRecognizer 轻扫手势
UILongPressGestureRecognizer 长按手势
UIPanGestureRecognizer 平移手势
UIPinchGestureRecognizer 捏合(缩放)手势
UIRotationGestureRecognizer 旋转手势
UIScreenEdgePanGestureRecognizer 屏幕边缘平移
基本使用就不介绍了,主要有一些常用手势属性。
2.UIPanGestureRecognizer,UIPinchGestureRecognizer
让UIView随平移手势拖动和捏合手势缩放
UIPanGestureRecognizer * pan=[[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(panView:)];
[self.testview addGestureRecognizer:pan];
UIPinchGestureRecognizer *pinch=[[UIPinchGestureRecognizer alloc]initWithTarget:self action:@selector(pinchView:)];
[self.testview addGestureRecognizer:pinch];
-(void)panView:(UIPanGestureRecognizer*)panGes
{
//获得平移偏移量(相对于手势起始位置)
CGPoint panPoint=[panGes translationInView:self.testview];
//进行偏移累计
self.testview.transform=CGAffineTransformTranslate(self.testview.transform, panPoint.x, panPoint.y);
//重置偏移量(因为平移偏移是相对于起始位置,如果不重置该值,累计值就会叠加递增)
[panGes setTranslation:CGPointZero inView:self.testview];
}
-(void)pinchView:(UIPinchGestureRecognizer*)pinchGes
{
//进行缩放累计
self.testview.transform=CGAffineTransformScale(self.testview.transform, pinchGes.scale, pinchGes.scale);
//重置缩放范围
[pinchGes setScale:1.0];
}
3.处理手势和UIView事件冲突
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
//如果点击到GrayView,手势不响应事件
if([touch.view isKindOfClass:GrayView.class])
{
return NO;
}
return YES;
}