iOS中的事件
触摸事件,加速事件(摇一摇),远程控制事件(耳机线控,窗口播放)
以最常见的触摸事件为例,当触摸手机屏幕时操作系统会将这个事件添加到由UIApplication管理的事件队列中(FIFO)UIApplication发送事件到应用程序的主窗口(Window)Window会在图层结构中找到最合适的图层来处理事件。
UIResponder
UIResponder类是专门用来响应用户的操作处理各种事件的,iOS中大部分控件都继承自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;//中断,被手势或者系统中断
事件传递链
UIApplication传递事件到当前Window是明确的,接下来就是从Window开始找最佳响应视图,此过程有两个重要的方法:
hitTest方法继承自UIView(UIWindow是继承自UIView的)。从UIApplication开始调用Window的hitTest方法,默认是递归调用的。
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
return [super pointInside:point withEvent:event];
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
return [super hitTest:point withEvent:event];
}
传递过程如下:
1.系统从UIApplication开始,当前window调用hitTest,hitTest内部会通过以下条件判断window能否能响应事件
- 不允许交互:userInteractionEnabled=NO
- 隐藏:hidden = YES
- 透明度:alpha < 0.01,alpha小于0.01为全透明
2.如果能响应,该函数内部会调用pointInside判断当前触摸点是不是在视图范围内
3.如果在window范围内,开始反向遍历window的子视图列表subviews,遍历的同时会调用subviews中每个子视图的hitTest,判断逻辑和上面的一样,如果找到循环就会停止。
4.此过程会递归,直到找到最外层合适的view,最后返回的view就是最佳响应视图。
一种hitTest可能的实现方式如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
if (!self.userInteractionEnabled|| self.hidden || self.alpha == 0.0){
return nil;
}
if (![self pointInside:point withEvent:event]){
return nil;
}
// 后加入的视图在图层上方,所以反向遍历是合理的
NSInteger count = self.subviews.count;
for (NSInteger i = count - 1; i >= 0; i--)
{
UIView *view = self.subviews[i];
// 坐标的转换
CGPoint subPoint = [self convertPoint:point toView:view];
// 继续递归
UIView *lastView = [view hitTest:subPoint withEvent:event];
if (lastView)
{
return lastView;
}
}
return self;
}
以上,这就是事件传递过程,由内往外的传递过程(从window开始到最外层视图 )
此过程查找结束返回最终的view,UIApplication会调用UIWindow的sendEvent,从而触发对应的响应方法:
PS:这里通过在UIWIndow中重写sendEvent而不调用super的实现,你会发现所有的点击事件都不会触发
- (void)sendEvent:(UIEvent *)event;
以下是需要注意的点:
-
实际调用hitTest过程,系统为了找到精准的触摸点会多次调用
-
如果重写hitTest返回self,传递过程就会终止,当前view就是最合适的view;返回nil,传递也会终止,父视图superView就是最合适的view
-
如果遍历subviews的过程都没找到合适的view,那么subviews中的子view的hitTest会都会被被调用一次
-
hitTest会调用pointInside判断当前视图是否在点击区域,所以超出父视图边界的控件无法响应事件
-
同一个view上的两个子视图有重叠部分,后加入的视图会被加入到事件传递链
事件响应链
首先,响应者链中的各个响应者都继承自UIResponder,常见的UIView,viewController,UIWindow以及AppDelegate都继承自UIResponder。响应者链上的响应者在hitTest过程中就已经确定,可以通过迭代nextResponder查看所有的响应者。
事件响应链如下:
-
通过hitTest返回的view为当前事件的第一响应者,nextResponder为上一个响应者
-
如果当前view默认不去重写,或者重写调用了父类的实现,响应就会就会沿着响应者链向上传递(上一个响应者一般是superView,可以通过nextResponder属性获取上一个响应者)
-
如果上一个响应者是viewController,由viewController的view处理,view本身没处理,则传递给viewController本身
-
重复上述过程,直到传递到window,window如果也不能处理,传递到UIApplication,如果UIApplication的delegate继承自UIResponder,则交给delegate处理,delegate也不处理最后丢弃
以上就是响应者链,事件响应过程是从外向内传递,和事件传递的过程正好相反
通过遍历查找所有响应者:
UIResponder *respon = self;
while (respon) {
NSLog(@"%@",respon);
respon = respon.nextResponder;
}
有手势的情况下
手势识别器的作用就是,识别到对应的手势后发送消息给target。iOS中的手势分为两种,Apple文档中有提到:
- 离散型手势 (UITapGestureRecognizer,UISwipeGestureRecognizer)
- 持续性手势 (UIPinchGestureRecognizer,UIPanGestureRecognizer,UIRotationGestureRecognizer,UILongPressGestureRecognizer)
离散型手势的情况:
view未添加点击手势,点击一次屏幕会调用touchesBegan和touchesEnde,当我们不考虑touchesEnde的时候可以认为它是一次性的
touchesBegan
touchesEnde
view添加tap手势,点击屏幕会触发手势对应的方法,touchesBegan和touchesCancelled,这里虽然调用了touchesCancelled,但实际上touchesBegan已经触发了
touchesBegan
tap
touchesCancelled
连续型手势的情况:
view未添加连续手势,当手指在屏幕上拖动时,先touchesBegan,然后touchesMoved随着手指拖动持续调用,停止后调用touchesEnde
touchesBegan
touchesMoved
...
touchesEnded
view添加pan拖拽手势,当手指在屏幕上拖动时,touchesBegan和touchesMoved会先调用,当pan手势方法触发以后,touchesMoved将不再出现,同时touchesCancelled也触发了
touchesBegan
touchesMoved
pan //识别到 pan之后,就只有pan手势会响应
touchesCancelled
pan
pan
...
以下结论主要针对连续型手势:
- 若手势成功识别事件,就会取消第一响应者view对事件的响应(touchesCancelled)
- 若手势没能识别事件,第一响应者view就会接手事件的处理
通过断点在sendEvent:处查看UIEvent事件,在event->_allTouchesMutable->_gestureRecognizers手势中可以看到当前touch对象中包含所有的手势对象,通过断点可以看到数组中第一个手势的对象地址0x10510a2d0正是添加的tap手势的地址。因此可以说明,手势会先响应
touch的gestureRecognizers数组:
_gestureRecognizers __NSArrayM * @"6 elements" 0x0000000282bd46f0
[0] UITapGestureRecognizer * 0x10510a2d0 0x000000010510a2d0
[1] UIPanGestureRecognizer * 0x10510a3f0 0x000000010510a3f0
[2] UITapGestureRecognizer * 0x10510a1b0 0x000000010510a1b0
[3] UITapGestureRecognizer * 0x105107f70 0x0000000105107f70
[4] _UISystemGestureGateGestureRecognizer * 0x105011020 0x0000000105011020
[5] _UISystemGestureGateGestureRecognizer * 0x10500ff50 0x000000010500ff50
添加的tap手势对象:
[2890:328171] tap:<UITapGestureRecognizer: 0x10510a2d0; state = Ended; view = <BlueView 0x105109e40>; target= <(action=tap:, target=<FirstViewController 0x1050114b0>)>>
有UIControl(按钮)的情况
以Button为例,给Button添加添加tap手势和TouchDown类型target,结果和上面的例子一样,对于一次性手势都会响应
touchesBegan
TouchDown
tap
touchesCancelled
给Button只添加TouchDragInside类型target,touchesMoved和TouchDragInside都会响应
touchesMoved
TouchDragInside
给Button添加pan手势和TouchDragInside类型target,系统识别到pan手势后就会touchesCancelled,只有手势pan会执行
touchesMoved
TouchDragInside
pan //识别到 pan之后,就只有pan手势会响应
touchesCancelled
pan
pan
...
网友评论