iOS事件响应链中hitTest的应用示例

// 作用: 去寻找最适合的View
// 什么时候调用: 当一个事件传递给当前View,就会调用.
// 返回值: 返回的是谁,谁就是最适合的View(就会调用最适合的View的touch方法)
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        return super.hitTest(point, with: event)
}

hitTest的底层实现:

  • 1.先看自己是否能接受触摸事件
  • 2.再看触摸点是否在自己身上
  • 3.从后往前遍历子控件,拿到子控件后,再次重复1,2步骤,要把父控件上的坐标点转换为子控件坐标系下的点,再次执行hitTest方法
  • 4.若是最后还没有找到合适的view,那么就return self,自己就是合适的view

备注:当控件接收到触摸事件的时候,不管能不能处理事件,都会调用hitTest方法

应用实例

1.扩大UIButton的响应热区

import UIKit

private var ts_touchAreaEdgeInsets: UIEdgeInsets = .zero

extension UIButton {
    /// Increase your button touch area.
    /// If your button frame is (0,0,40,40). Then call button.ts_touchInsets = UIEdgeInsetsMake(-30, -30, -30, -30), it will Increase the touch area
    public var ts_touchInsets: UIEdgeInsets {
        get {
            if let value = objc_getAssociatedObject(self, &ts_touchAreaEdgeInsets) as? NSValue {
                var edgeInsets: UIEdgeInsets = .zero
                value.getValue(&edgeInsets)
                return edgeInsets
            }else {
                return .zero
            }
        }
        set(newValue) {
            var newValueCopy = newValue
            let objCType = NSValue(uiEdgeInsets: .zero).objCType
            let value = NSValue(&newValueCopy, withObjCType: objCType)
            objc_setAssociatedObject(self, &ts_touchAreaEdgeInsets, value, .OBJC_ASSOCIATION_RETAIN)
        }
    }
    
    open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        if UIEdgeInsetsEqualToEdgeInsets(self.ts_touchInsets, .zero) || !self.isEnabled || self.isHidden {
            return super.point(inside: point, with: event)
        }
        
        let relativeFrame = self.bounds
        let hitFrame = UIEdgeInsetsInsetRect(relativeFrame, self.ts_touchInsets)
        
        return hitFrame.contains(point)
    }
}

使用示例

override func touchesBegan(_ touches: Set, with event: UIEvent?) {
    areaBtn.ts_touchInsets = .init(top: -30, left: -30, bottom: -30, right: -30)
}

也可以自定义UIButton,在自定义的button里实现

import UIKit
class TouchIncreaseButton: UIButton {
    
    private let btnWidth : CGFloat = 44
    private let btnHeight : CGFloat = 44

    private func hitTestBounds(minimumHitTestWidth minWidth : CGFloat,minimumHitTestHeight minHeight : CGFloat) -> CGRect {
        var hitTestBounds = self.bounds
        if minWidth > bounds.size.width {
            hitTestBounds.size.width = minWidth
            hitTestBounds.origin.x -= (hitTestBounds.size.width - bounds.size.width)/2
        }
        if minHeight > bounds.size.height {
            hitTestBounds.size.height = minHeight
            hitTestBounds.origin.y -= (hitTestBounds.size.height - bounds.size.height)/2
        }
        
        return hitTestBounds
    }
    
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        
        let rect = hitTestBounds(minimumHitTestWidth: btnWidth, minimumHitTestHeight: btnHeight)
        return rect.contains(point)
    }
}

2.子view超出了父viewbounds响应事件

iOS事件响应链中hitTest的应用示例_第1张图片
demo.png

重载父viewhitTest(_ point: CGPoint, with event: UIEvent?)方法

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    for subView in self.subviews {
        // 把父类point点转换成子类坐标系下的点
        let convertedPoint = subView.convert(point, from: self)
        
        // 注意点:hitTest方法内部会调用pointInside方法,询问触摸点是否在这个控件上
        // 根据point,找到适合响应事件的这个View
        let hitTestView = subView.hitTest(convertedPoint, with: event)
        if hitTestView != nil {
            return hitTestView
        }
    }
    return nil
}

3.使部分区域失去响应.

iOS事件响应链中hitTest的应用示例_第2张图片
tableView.png

场景需求:如图,tableView占整个屏幕,tableView底下是一个半透明的HUD,点击下面没有内容区域,要让HUD去响应事件.

在自定义的tableView中重载hitTest方法

// tableView会拦截底部`backgroundView`事件的响应,
// 实现点击tableViewCell 之外的地方,让tableView底下的backgroundView响应tap事件
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    if let hitView = super.hitTest(point, with: event) , hitView.isKind(of: BTTableCellContentView.self) {
        return hitView
    }
    // 返回nil 那么事件就不由当前控件处理
    return nil;
}

4.让非scrollView区域响应scrollView拖拽事件

iOS事件响应链中hitTest的应用示例_第3张图片
scrollView.png

如图,这是一个使用scrollView自定义实现的卡片式轮播器,如何实现拖拽scrollView两边的view区域,和拖拽中间scrollView一样的效果呢?只需要在scrollView的父View重载hitTest方法

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    if self.point(inside: point, with: event) == true {
        return scrollView
    }
    return nil
}

3.1.使自定义Button失去响应

class NoEventButton: UIButton {
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        return nil
    }
}

参考:iOS事件响应链中Hit-Test View的应用
ios开发事件处理之 四:hittest方法的底层实现与应用

你可能感兴趣的:(iOS事件响应链中hitTest的应用示例)