使用hitTest实现点击重叠部分时,底下重叠的视图响应的效果

877 阅读4分钟

视图B和C加载在视图A上,B和C有重叠,重叠部分C在B上面。此时要做到点击重叠部分时,C不响应,而B响应,点击不重叠部分时则正常响应的效果。用UIView的hitTest方法实现了该功能。

hitTest原理

hitTest方法

func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
  • point:在接收器的局部坐标系(界)中指定的点。
  • event:系统保证调用此方法的事件。如果从事件处理代码外部调用此方法,则可以指定nil。
  • 返回值:返回所能包含point的view和view.subviews中最后的一个view。如果point完全位于视图层次结构之外,则返回nil。

调用顺序

触摸事件寻找最佳响应者,即hitTest 的调用顺序大致如下:

touch(UIEvent)->UIApplication->UIWindow->window.subviews->...->view
  • 当App接收触摸事件时,主线程的runloop被唤醒,触发source1回调。source1回调又触发了一个source0回调,将接收到的触摸事件(IOHIDEvent对象)封装成UIEvent对象,此时APP将正式开始对于触摸事件的响应。source0回调将触摸事件添加到UIApplication的事件队列中。

  • UIApplication会从事件队列中取出最早的事件进行分发处理,首先将事件传递给窗口对象(UIWindow),如果有多个UIWindow对象,则先选择最后加上的UIWindow对象。

  • UIWindow会调用其hitTest:withEvent:方法在视图(UIView)层次结构中找到一个最合适的UIView来处理触摸事件。

触摸事件的传递顺序

通过hitTest我们已经找到了最佳响应者,下面要做的事就是让这个最佳响应者响应触摸事件。这个最佳响应者对于触摸事件拥有决定权,它可以决定是自己独自响应这个事件,也可以自己响应之后还把它传递给其他响应者。

事件传递顺序大致为:

view -> superView ...- > UIViewController.view -> UIViewController -> UIWindow -> UIApplication -> 事件丢弃

1、 首先由 view 来尝试处理事件,如果他处理不了,事件将被传递到他的父视图superview

2、superview 也尝试来处理事件,如果他处理不了,继续传递他的父视图 UIViewcontroller.view

3、UIViewController.view尝试来处理该事件,如果处理不了,将把该事件传递给UIViewController

4、UIViewController尝试处理该事件,如果处理不了,将把该事件传递给主窗口Window

5、主窗口Window尝试来处理该事件,如果处理不了,将传递给应用单例Application

6、如果Application也处理不了,则该事件将会被丢弃。

所以当我们点击该视图时,父视图会依次调用该方法,点击事件皆为点击的视图。其顺序为,当前点击的视图调用方法,返回点击视图。然后父视图也会调用一遍该方法,同样返回点击视图,以此类推。

实现

实现效果

已知视图B和C分别加载在视图A上,其中B在C底下,当我们点击C与B的重叠部分之后,要求返回的响应结果重叠部位底下的视图,即B视图的,而非默认的C,且其他的地方的点击正常。

实现方法

当点击该视图时,获取其父视图的子视图数组,并依次遍历,判断该子视图是否在该点击坐标范围之内,如在且该视图不是点击的视图时,就判断是重叠的视图,返回该视图,则原本的点击事件就被替换为该视图底下重叠视图的点击视图了。

使用func convert(_ point: CGPoint, to coordinateSpace: UICoordinateSpace) -> CGPoint方法将当前的坐标转换为目标视图的坐标。

使用CALayer的open func contains(_ p: CGPoint) -> Bool方法,判断该点是否落在对应的视图的layer的范围内。

代码实现:

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    let view = super.hitTest(point, with: event)
    guard view == self else {
        return view
    }
    guard let superView = view?.superview else {
        return view
    }
    print(String(format: "self:%p, view:%p", self, view!))
    for inView in superView.subviews {
        if let relatePoint = view?.convert(point, to: inView),
inView.layer.contains(relatePoint) && inView != view {
            ///点击重叠范围时,更改重叠部位底下视图的颜色,并返回底下的视图。
            inView.backgroundColor = fetchColor()
            return inView
        }
    }
    return view
}

注意点

1、hitTest的point参数是调用它的对象对应的坐标,而我们要对点击视图的父视图的子视图进行遍历并对比坐标,判断坐标是否落在视图上,则要保证调用的对象就是我们点击的视图,而不是其父视图。 所以在调用该方法的时候要先判断调用的对象是否是他自己:

let view = super.hitTest(point, with: event)
guard view == self else {
    return view
}

2、使用view.layer.contains(point)方法判断坐标的时候,其坐标是以view为准的,其坐标范围为(0, 0, width, height),需要注意。

代码示例

GitHub:github.com/MichaelLynx…