在开发中我们经常会遇到按钮太小导致用户点击不到,显然,这样的作品是当然不能给用户一个良好的体验的。那么,这个时候我们就要对UI控件做出合理的处理,既能符合美工的要求又能给用户带来良好的体验。

iOS事件流的传递和响应问题

要想正确的对控件的响应做处理,我们先要明白下面几个问题

  1. iOS中view的事件到底是怎样传递和处理的?
  2. 为什么父view关闭了事件响应,子view就无法响应事件?底层原理?
  3. 如何让父view和子view同时响应同一事件?(默认情况下只会响应子view的事件回调)
  4. 为什么子view没有添加事件,当我们在子view触发事件的时候会由其父view来响应事件?

分析

iOS的事件可以分为三种情况

  • Touch Events(触摸事件)
  • Motion Event (运动事件,是指手机传感器所触发的事件等)
  • Remote Event(外部事件,是指由耳机控制或蓝牙设备所触发的事件等)

我们这里主要对触摸事件做一个分析
触摸事件可以分为(传递和响应)两个阶段

  • 传递:当我们触摸手机屏幕时,系统为我们找到最合适的view。过程:每当手指接触屏幕,操作系统会把事件传递给当前的APP。在UIApplication接收到手指的事件以后,就会调用UIwindow的hitTest:withEvent:,看看当前点击的点是不是在window内,如果是则继续依次调用其subviews的hitTest:withEvent:方法,直到找到最后需要的view。当调用结束之后,便能找到最合适的那个view。这里其实是一个简单的递归,递归的内容为window的subviews。
  • 响应: 当操作系统找到最合适的view的时候,事件会由上而下【子view-->父view-->控制器view-->控制器】来找出合适响应事件的view。如果当前view有响应事件,则不会继续向下寻找了。如果没有添加手势事件,看看其是否实现了如下的方法:
    - (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;

若实现则由此view来响应,若未实现则继续向上寻找响应者。一旦找到最合适响应的View就结束, 在执行响应的绑定的事件,如果没有就抛弃此事件。

下面就解答一下上面的问题:

  1. 如上所述。
  2. 因为在时间传递的时候,会从下到上寻找最合适的view,当我们把父view的可交互性设置为false。则:系统会在递归的时候过滤掉此父view,那么其所有的子view都不会被认定为最合适的view。故:此事件就永远不会被其子view所响应。
  3. 我们知道当事件在响应的时候,当系统找到最佳响应者的时候,事件就不会继续向下传递响应了。但是,我们在时间传递的时候可以明确地知道,事件会从父view传递到子view。我们这时,在父view的hitTest:withEvent:方法里面做一个监听,然后做相应的处理,就可以实现同时响应了。
  4. 如上所述,系统会在子view关闭事件响应的时候认定其父view为最佳响应者。

下面进入正题,介绍一下关于UIButton的热区扩大

思路

基于上面的理解,当我们点击子view区域外的部分的时候,其实是点击在了它的父view上,然后事件在传递的时候把父view作为最合适的view,则:事件响应只在其父view和它父view的父view上寻找最佳响应者。此时,我们只需要让系统认定子view为那个最合适的view就可以了。那么怎么处理呢?
我们在UIView的API里面看到下面两个方法:

- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;   // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;   // default returns YES if point is in bounds

我们是否可以重写其hitTest: withEvent:方法来让事件的传递继续向上呢?
答案当然是可以的:

//扩大点击区域
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
if(UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero) || !self.enabled || self.hidden)
{
    return [super pointInside:point withEvent:event];
}
CGRect relativeFrame = self.bounds;
      CGRect hitFrame = UIEdgeInsetsInsetRect(relativeFrame, self.hitTestEdgeInsets);
return CGRectContainsPoint(hitFrame, point);
      }

然后我们可以自己定义这个hitTestEdgeInsets,来实现热区的扩大。
当然:为了方便我们不一定要通过继承的方式来实现,因为runtime会去调用分类的方法,这样就灵活多了

最后:这里知识对UIbutton的热区问题做了一个小小的总结,但是响应链的问题不仅仅体现在这里,希望大家有举一反三的思考,这里算是抛砖引玉了,献上DEMO,哈哈~


你远道而来这人世间,想必也是因为热爱吧 ||