美文网首页
iOS开发之-事件的传递和响应

iOS开发之-事件的传递和响应

作者: 剑老师 | 来源:发表于2021-03-27 11:05 被阅读0次

    本文首发于公众号【一个老码农】

    • 什么是响应者链
      iOS 响应者链是支撑 App 界面交互的重要基础。当点击屏幕会产生一个触摸事件,主线程runloop会接收到它并放到消息队列里,UIApplication 会从消息队列里取事件分发下去,经过多个响应者对象的传递,找到合适的响应视图。这多个响应者对象连接起来的链条,我们称之为响应者链。

    • 事件传递的过程
      当一个触摸事件产生时,响应者链是怎样找到响应视图的呢?
      1.首页事件会先由UIApplication传递至window
      2.调用UIWindow的hitTest:方法,在hitTest方法中调用pointInside方法判断点击是否在当前Window中。
      3.如果window的pointInside方法返回YES(一般都会返回yes),则倒序遍历子View,并调用子view的hitTest:方法。
      4.在子view的hitTest方法中再调用pointInside方法判断点击事件是否在当前子view中。
      5.如果在当前view中,则判断有没有子view,如果有子view,则继续遍历子view。
      6.若第一次有hitTest方法返回的UIView对象为非空。则递归返回此对象,此对象就是此次事件的最佳响应者。
      7.若所有子view的hitTest方法都返回nil,则返回当前视图做为最佳响应者。

    注:在查找响应者过程当中,hitTest和pointInside方法其实是会调用两次的,两次为同一个事件,只是事件的状态不同。一次为事件的begain,一次为事件的end。

    • 事件响应的过程是怎样的
      找到合适的响应者后,首先判断响应者是否能响应此事件,如果不能响应,则会向父view传递,直到找到响应者,如果一直没有找到响应者,则会一直到UIWindow,如果window还无法处理则会交给UIApplication,如果UIApplication还无法处理,则忽略掉此事件。
      找到响应者后,会顺序调用响应者的以下几个方法
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    }
        
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    }    
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    }
    
    • hitTest方法和pointInside方法的作用
      hitTest方法的作用是,为事件寻找一个合适的view作为响应者,内部实现有以下判断:
      1.view的userInteractionEnabled属性为false的不响应事件
      2.view的ishidden属性为true的不响应事件
      3.view透明度小于等于0.01的不响应事件
      4.调用pointInside,判断点击位置是否在当前view内,不在当前view内的,不响应事件
      pointInside方法会在hitTest方法内部调用,用来判断当前事件是否在当前view范围内,如果不在此view范l围内,则不能响应事件。

    • 我们可以用hitTest和pointInside做哪些事情

      实例1:事件透传

    view上有一个button,view和button分别绑定了事件。但是我在点击button的时候,想忽略掉button点击事件,转而让view响应。

    分析:在Button的hitTest方法中,如果super.hitTest返回的对象是自己,则返回nil。这样就可以忽略掉button的点击事件。

    代码如下:

    override func viewDidLoad() {
            super.viewDidLoad()        
            let bgView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
            bgView.backgroundColor = UIColor.blue        
            let tap = UITapGestureRecognizer(target: self, action: #selector(viewTap))
            view.addGestureRecognizer(tap)
            view.addSubview(bgView)
            
            let button = CustomButton(type: .custom)
            button.frame = CGRect(x: 100, y: 100, width: 50, height: 50)
            button.setTitle("Click", for: .normal)
            button.setTitleColor(.blue, for: .normal)
            button.addTarget(self, action: #selector(click), for: .touchUpInside)
            button.backgroundColor = UIColor.red
            bgView.addSubview(button)        
        }
    
    @objc private func viewTap() {
         print("调用的是view")
    }
                
    @objc private func click() {
         print("点击")
    }
    
    //重写button的hitTest方法
    class CustomButton: UIButton {
        override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
            print("CustomButton hitTest")
            let testView = super.hitTest(point, with: event)
            if testView == self {
                return nil
            }
            return testView 
        }
    }
    

    点击Button之后控制台打印效果:


    实例2:Button超过父View的范围,点击范围外的部分,仍然让其响应事件

    如图,点击蓝色外的红色部分,仍然让其响应Click事件。


    分析:在父view的pointInside里面,判断子view是否为Button类型,如果是,则判断是否点击在button范围内。

    如下代码,点击蓝色区域外的button区域,则可以响应button事件:

    class CustomView: UIView {
        override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
            for subView in subviews {
                if subView.classForCoder == CustomButton.classForCoder() {
                    let subPoint = self.convert(point, to: subView)
                    if subView.point(inside: subPoint, with: event) {
                        return true
                    }
                }
            }
            return super.point(inside: point, with: event)
        }
    }
    

    实例3:让UIButton的响应区域变大
    如,我想让工程中所有的UIButton响应区域都至少为50*50pt。
    思路:重写UIButton的pointInside方法,在此方法内判断button的响应区域,若在button的50pt范围内的,则返回true。代码如下:

    extension UIButton {
        ///所有按钮的可点击区域不小于50*50
        open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
            var bounds = self.bounds
            let widthDelta = max(50.0 - bounds.size.width, 0)
            let heightDelta = max(50.0 - bounds.size.height, 0)
            bounds = bounds.insetBy(dx: -0.5 * widthDelta, dy: -0.5 * heightDelta)
            return bounds.contains(point)
        }
    }
    

    总结:
    1.事件的传递是由UIApplication、UIWindow、UIView...从父view至子view递归的传递,直至找到最佳响应者。事件的响应是从子view至父view一层一层的传递,直至找到可以处理响应事件的view。

    2.我们可以通过重写hitTest和pointInside来改变事件的响应链或修改view的响应区域。

    关注公众号【一个老码农】免费获取iOS进阶学习视频

    原文地址:事件的传递和响应

    相关文章

      网友评论

          本文标题:iOS开发之-事件的传递和响应

          本文链接:https://www.haomeiwen.com/subject/oliehltx.html