响应者链

一. Hit-Testing

  1. 什么是Hit-Testing?

    • 对于触摸事件, window首先会尝试将事件交给事件触发点所在的View来处理, 也就是hit-test view
    • 而寻找hit-test view的这个过程, 被称之为hit-testing
    • Hit-Testing Returns the View Where a Touch Occurred, 这个过程会返回点击事件所在的View
  2. Hit-Testing的过程

    • 系统首先找到触摸点所在位置的View, 然后找到这个View的最高级父控件
    • 从最高级的父控件开始进行寻找, 首先会调用hit-test方法
    • hit-test内部调用pointInside方法, 来判断触摸点point是否在当前的View中
      • 如果在, 则返回YES, 则进入这个View中, 继续遍历他的子控件, 执行hit-test方法
      • 如果不在, 则返回NO, 离开这个View的hit-test检查, 执行同级另一个View的hit-test继续查找
    • 遍历到在View等级系统中, 处于最低级的View, 他没有子控件, 因此这个就是最合适的hit-test view, 点击事件就会交给他来处理
    • 递归: 进入hit-test -> 调用pointInside -> (如果返回YES, 则进入子控件的hit-test -> 回到第一步) -> 离开hit-test
  3. Hit-tset的一般作用

    • 阻止某个View的响应(应用的较少), 让pointInside:方法始终返回NO, 即可阻止这个View的响应

    • 扩大按钮的响应区域(如果按钮过小, 但是你想扩大他的响应范围)

        - (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
            return CGRectContainsPoint(HitTestingBounds(self.bounds, self.minimumHitTestWidth, self.minimumHitTestHeight), point);
        }   
                
        CGRect HitTestingBounds(CGRect bounds, CGFloat minimumHitTestWidth, CGFloat minimumHitTestHeight) {
            CGRect hitTestingBounds = bounds;
            
            // 如果要求的宽度, 超过了控件的宽度
            if (minimumHitTestWidth > bounds.size.width) {
                hitTestingBounds.size.width = minimumHitTestWidth; // 修改响应的宽度
                // 响应的X值 -= (响应的宽度 - 原宽度) / 2  (X)
                hitTestingBounds.origin.x -= (hitTestingBounds.size.width - bounds.size.width)/2;
            }
            if (minimumHitTestHeight > bounds.size.height) {
                hitTestingBounds.size.height = minimumHitTestHeight;
                hitTestingBounds.origin.y -= (hitTestingBounds.size.height - bounds.size.height)/2;
            }
            return hitTestingBounds;
        }
      
    • 子控件超出父控件后无法响应(TabBar按钮超出父控件范围)

        - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
        
            if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
                return nil;
            }
            /**
             *  此注释掉的方法用来判断点击是否在父View Bounds内,
             *  如果不在父view内,就会直接不会去其子View中寻找HitTestView,return 返回
             */
        //    if ([self pointInside:point withEvent:event]) {
                for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
                    CGPoint convertedPoint = [subview convertPoint:point fromView:self];
                    UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event]; // 寻找该控件中的hit-test view
                    if (hitTestView) {
                        return hitTestView;
                    }
                }
                return self;
        //    }
            return nil;
        }
      

二. 响应者链条由响应者组成(The Responder Chain Is Made Up of Responder Objects)

  1. hit-test view是最优先处理事件的View, 如果hit-test view不能处理事件的话, 那么事件会沿着响应者链条找到能够处理这个事件的View.

  2. 响应者链条的事件传递:

    • 响应者链条是由一系列的响应者对象构成的, 很多事件都是依靠响应者链条来实现事件传递
    • 传递从first responder开始, 到Application结束
    • 如果first responder不能处理这个事件, 那么系统会顺着响应者链条向前寻找下一个响应者
  3. 响应者对象(Responder object):

    • 响应者对象能够响应处理事件, UIResponder类是所有响应者对象的基类, 他定义了事件处理以及一般响应行为
    • 凡是继承自UIResponder, 例如UIView,UIApplication,UIViewController等, 都可以响应事件.
    • 注意: CoreAnimation Layers不是响应者
  4. 第一响应者(First Responder):

    • First Responder用于第一个来接收事件.
    • 一般First Responder都是一个View对象, 一个对象变为First Responder需要通过一下两种情况:
      • 重写canBecomeFirstResponder方法, 并且返回YES
      • 接收becomeFirstResponder消息, 响应者对象可以给自己发送这个消息
  5. 要在View完全渲染结束的时候, 才能指定其成为First Responder:

    • 例如: 如果在ViewWillAppear方法中调用becomeFirstResponder方法, 那么这个方法总是会返回NO
    • 因此, 应该在viewDidAppear方法中, 指定称为First Responder
  6. 除了Event事件, 还有其他几种情况需要使用到响应者链:

    1. 触摸事件(TouchEvent): 如果hitTest view无法处理一个触摸时间, 那么这个事件就会放弃这个hit-test view的响应者链
    2. 运动事件(MotionEvent): 如果要处理shake-motion摇动事件, 那么当前First responder对象必须要实现motionBegan:withEvent:motionEnded:withEvent:两个方法之一
    3. 远程控制事件(RemoteControlEvent): 处理这类时间, 当前的First responder必须实现remoteControlReceivedWithEvent方法
    4. 行动消息(ActionMessage): 当用户点击一个UIButton或UISwitch等控件的时候, 如果消息的发出者target为nil的话, 那么这条消息会由First Responder的响应链的开始发出
    5. 编辑菜单消息(文本的赋值粘贴)(Editing-menu messages): 当点击编辑菜单的命令时, iOS会通过响应者链条寻找实现了cut: copy paste:方法的对象
    6. 文本编辑(TextEditing): 当用户使用UITextField或UITextView时, 这个View会自动的变为First Responder, 通常情况下, 键盘会自动的弹出并且TextView会进入编辑状态. 你可以展示一个自定义的输入View来替代键盘, 同时可以给任意一个响应者对象添加一个自定义的输入View.
  7. 只有TextView和TextField才会自动变成First Responder, 其他的响应者对象, 需要主动调用becomeFirstResponder方法来称为第一响应者


The Responder Chain Is Made Up of Responder Objects(响应者链条是由响应者对象构成的)

Many types of events rely on(依靠) a responder chain for event delivery(事件传递). The responder chain is a series of linked responder objects(一系列相连的响应者对象). It starts with the first responder and ends with the application object(从第一响应者开始, 到Application结束). If the first responder cannot handle an event, it forwards the event to the next responder in the responder chain.(如果第一响应者无法处理, 则顺着链条向前找下一个响应者)

A responder object is an object that can respond to and handle events(响应者对象能响应和处理时间). The UIResponder class is the base class for all responder objects, and it defines the programmatic interface not only for event handling but also for common responder behavior(UIResponder类定义了事件处理以及一般响应行为). Instances of the UIApplication, UIViewController, and UIView classes are responders, which means that all views and most key controller objects are responders. Note that Core Animation layers are not responders(CALayer不是响应者).

The first responder is designated to receive events first(First responder用于第一个接收事件). Typically, the first responder is a view object. An object becomes the first responder by doing two things(一个对象变成First responder通过两件事):

  1. Overriding the canBecomeFirstResponder method to return YES重写canBecomeFirstResponder方法, 并返回YES.
  2. Receiving a becomeFirstResponder message. If necessary, an object can send itself this message接收becomeFirstResponder消息, 一个对象可以给自己发这个消息.

Note: Make sure that your app has established its object graph(建立他的对象图表) before assigning an object to be the first responder(在指定一个对象变为first responder之前). For example, you typically call the becomeFirstResponder method in an override of the viewDidAppear: method. If you try to assign the first responder in viewWillAppear:, your object graph is not yet established, so the becomeFirstResponder method returns NO如果在你的View没有渲染完毕时让他成为第一响应者, 这时候这个方法始终会返回NO.

Events are not the only objects that rely on the responder chain(事件不只是唯一的依靠于响应者链条的). The responder chain is used in all of the following:

  1. Touch events(触摸事件). If the hit-test view cannot handle a touch event, the event is passed up错过 a chain of responders响应者链条 that starts with the hit-test view从hit-test view开始的.

  2. Motion events(运动事件). To handle shake-motion(摇动事件) events with UIKit, the first responder must implement either the motionBegan:withEvent: or motionEnded:withEvent: method of the UIResponder class, as described in Detecting Shake-Motion Events with UIEvent第一响应者需要实现motionBeganmotionEnded两个方法之一.

  3. Remote control events(远程控制事件). To handle remote control events, the first responder must implement the remoteControlReceivedWithEvent: method of the UIResponder class.远程控制事件, 必须实现remoteControlReceivedWithEvent方法

  4. Action messages(行动消息(按钮)). When the user manipulates(操控) a control, such as a button or switch, and the target(目标为nil) for the action method is nil, the message is sent through a chain of responders starting with the first responder, which can be the control view itself(当action message被处罚, 并且没有方法对应的target的时候, 这个message由first responder的响应者链条的开始发出).

  5. Editing-menu messages(编辑菜单消息(文本的赋值粘贴)). When a user taps the commands of the editing menu, iOS uses a responder chain to find an object that implements the necessary methods (such as cut:, copy:, and paste:)(当点击编辑菜单的命令时, iOS会通过响应者链条寻找实现了cut: copy paste:方法的对象). For more information, see Displaying and Managing the Edit Menu and the sample code project, CopyPasteTile.

  6. Text editing(文本编辑). When a user taps a text field or a text view(TextField和TextView), that view automatically becomes the first responder(这个View会自动的变成first responder). By default, the virtual keyboard appears and the text field or text view becomes the focus of editing(键盘弹出, textView进入编辑). You can display a custom input view instead of the keyboard(你可以显示一个自定义的输入View, 而不是键盘) if it’s appropriate for your app. You can also add a custom input view to any responder object(你可以给任意一个响应者对象添加input view). For more information, see Custom Views for Data Input.

UIKit automatically sets the text field or text view that a user taps to be the first responder(UIKit 自动的将textView和textField设置为first responder当开始编辑的时候); Apps must explicitly set all other first responder objects with the becomeFirstResponder method(其他的响应者对象, 必须手动调用becomeFirstResponder方法).


三. 响应者链的传递方式(The Responder Chain Follows a Specific Delivery Path)

  1. 响应者链的传递方式:
    • 首先, 当触发一个事件的时候, 被点击的View是初始View, 他会先从这个view开始寻找事件处理者
    • 如果这个View无法处理这个事件, 那么会顺着他的父级(响应者链)继续寻找下一个响应者
    • 如果有响应者能够处理这个事件, 或没有找到能够处理这个事件的响应者, 则传递结束
    • 传递的顺序: view -> ViewController -> window -> Application -> 丢弃

If the initial object(初始对象)—either the hit-test view or the first responder—doesn’t handle an event, UIKit passes the event to the next responder in the chain(如果hit-test view和first responder都无法处理事件, 那么UIKit会沿着响应者连寻找下一个响应者). Each responder decides whether it wants to handle the event or pass it along to its own next responder by calling the nextResponder method(每个响应者都可以决定是否要去处理时事件或者通过调用nextResponder方法, 交个下一个响应者处理).This process continues until a responder object either handles the event or there are no more responders(当有响应者处理这个事件, 或没有更多响应者的时候结束传递).

The responder chain sequence(链条) begins when iOS detects(检测到) an event and passes it to an initial object, which is typically a view. The initial view has the first opportunity to handle an event. Figure 2-2 shows two different event delivery paths for two app configurations. An app’s event delivery path depends on its specific construction(当前的构造), but all event delivery paths adhere to(依附) the same heuristics(探索法).

Figure 2-2 The responder chain on iOS

For the app on the left, the event follows this path:

  1. The initial view attempts to handle the event or message. If it can’t handle the event, it passes the event to its superview, because the initial view is not the top most view in its view controller’s view hierarchy(如果接收到事件的初始View无法处理事件, 那么这个事件会交给他的SuperView, 因为他不是viewController等级中的最高级View).

  2. The superview attempts to handle the event. If the superview can’t handle the event, it passes the event to its superview, because it is still not the top most view in the view hierarchy.

  3. The topmost view in the view controller’s view hierarchy attempts to handle the event. If the topmost view can’t handle the event, it passes the event to its view controller.

  4. The view controller attempts to handle the event, and if it can’t, passes the event to the window.

  5. If the window object can’t handle the event, it passes the event to the singleton app object.

  6. If the app object can’t handle the event, it discards(丢弃) the event.

  7. view -> ViewController -> window -> Application -> 丢弃

The app on the right follows a slightly different path, but all event delivery paths follow these heuristics:

  1. A view passes an event up its view controller’s view hierarchy until it reaches the topmost view.

  2. The topmost view passes the event to its view controller.

  3. The view controller passes the event to its topmost view’s superview.
    Steps 1-3 repeat until the event reaches the root view controller.

  4. The root view controller passes the event to the window object.

  5. The window passes the event to the app object.

Important: If you implement a custom view to handle remote control events, action messages, shake-motion events with UIKit, or editing-menu messages, don’t forward the event or message to nextResponder directly to send it up the responder chain(不要手动直接调用nextResponder方法, 将事件直接传递给下一个响应者). Instead, invoke the superclass implementation of the current event handling method and let UIKit handle the traversal of the responder chain for you(应该调用父类的时间处理方法的实现, 并且让UIKit通过响应者链来传送事件).


四. 总结

  1. 响应者链是由当前你所点击的view的父级结构 + window + Application组成
  2. 事件的响应分为两个阶段, hit-test寻找响应者
    • hit-test
      • 当用户触摸屏幕的时候, 系统会从最高级, 也就是RootView开始调用hitTestpointInside方法, 来寻找最适合处理事件的View, 即Hit-test View
      • 这个过程总是从View等级中最高的View开始, 不断的遍历Subview
      • 可以用这个方法来手动选择负责响应事件的View
    • Fine Responder
      • 当hit-test寻找到hit-test View之后, 会从这个view开始判断能否处理事件
      • 如果不能, 系统会自动通过响应者链来寻找下一个响应者继续判断
      • 如果找到了能够响应事件的响应者, 那么这个事件由这个响应者来处理
      • 如果没有响应者能够接收直到Application也无法处理, 那么这个事件将会被丢弃
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容