iOS - 事件的響應鏈和傳遞鏈

UIResponder(響應對象)

An abstract interface for responding to and handling events.

一個UIResponder類為那些需要響應并處理事件的對象定義了一組接口片酝。在iOS中不是任何對象都能處理事件, 只有繼承了UIResponder的對象才能接收并處理事件汉额,稱為響應者對象液南。UIApplication背镇,UIViewControllerUIView都繼承自UIResponder钝计,因此他們都是響應者對象,茉帅,都能夠接收并處理事件。這意味著所有的視圖(all views)和大多數(shù)的關鍵視圖控制器對象都是響應者宾添。但是要注意核心動畫中的層(layer)不是響應者船惨。

繼承自UIResponder的類能處理事件是由于UIResponder內部提供了以下方法:

// 觸摸事件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);

// 按壓事件
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);

// 傳感器事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);

// 遠程控制事件
- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(4_0);

分發(fā)機制(Hit-Testing)

iOS使用hit-testing來找到觸摸點所在的視圖。hit-testing將會檢測是否觸摸事件在相關視圖的顯示區(qū)域之內缕陕。如果在粱锐,將遞歸檢測當前視圖的所有子視圖。視圖層級中最底層的view如果包含觸摸點將成為hit-test view扛邑。在iOS確認了hit-test view之后怜浅,將傳遞觸摸事件給對應的視圖進行處理。

為了解釋上面蔬崩,下面看一下官方例子:用戶觸摸視圖view E恶座,iOS將有序查找子視圖,找到hit-test view

1:由下圖可知沥阳,觸摸位于視圖A的區(qū)域之內跨琳,所以會對B,C進行檢測
2:如果觸摸事件不在視圖B區(qū)域中,但位于視圖區(qū)域C沪袭,將對視圖C的子視圖D,E進行檢測
3:如果觸摸事件不在視圖D區(qū)域中湾宙,但位于視圖區(qū)域E中,又因為視圖E是視圖層級結構中最底層的視圖冈绊,所以視圖E將成為hit-test view

Hit-testing returns the subview that was touched

20160719093700200.png

hitTest:withEvent:方法會根據(jù)給定觸摸點(CGPoint)和事件對象(UIEvent)兩個參數(shù)侠鳄,返回點擊的視圖(hit test view)

1死宣、該方法首先會調用pointInside:withEvent:方法,如果hitTest:withEvent:方法中所傳遞的參數(shù)point點是位于視圖之內伟恶,pointInside:withEvent:方法將返回true,然后毅该,在返回true的所有子視圖上將遞歸調用hitTest:withEvent:方法博秫。

2、如果hitTest:withEvent:方法中傳遞的point點不在視圖顯示區(qū)域之內眶掌,第一次調用pointInside:withEvent:方法將返回false挡育,那么該點將被忽略,hitTest:withEvent:方法將返回nil朴爬。如果一個子視圖返回false即寒,那么整個視圖層級都將被忽略,因為觸摸并不在子視圖當中,所以子視圖的子視圖同樣也不會發(fā)生觸摸事件母赵。

總結hitTest處理流程

調用當前viewpointInside:withEvent:方法來判定觸摸點是否在當前view內部逸爵,如果返回false,則hitTest:withEvent:返回nil凹嘲;如果返回true师倔,則向當前view內的subViews發(fā)送hitTest:withEvent:消息,所有subView的遍歷順序是從數(shù)組的末尾向前遍歷周蹭,直到有subView返回非空對象或遍歷完成趋艘。如果有subView返回非空對象,hitTest方法會返回這個對象谷醉,如果每個subView返回都是nil致稀,則返回自己。

注意:

hitTest:withEvent:方法忽略隱藏(hidden=YES)的視圖俱尼,禁止用戶操作(userInteractionEnabled=YES)的視圖抖单,以及alpha級別小于0.01(alpha<0.01)的視圖。

hit-test view將首先處理觸摸事件遇八,如果hit-test view并不能夠處理事件矛绘,那么該事件將由視圖的響應者鏈進行查找,一直到系統(tǒng)找到能夠處理事件的對象刃永。

事件響應者鏈

許多類型的事件都依賴于響應者鏈進行事件的傳遞货矮。響應者鏈關聯(lián)著一系列的響應者對象,由第一個響應者對象開始一直到application對象結束斯够,如果第一個響應者不能夠處理事件囚玫,事件將會被傳遞到響應者鏈中的下一個響應者對象。

第一響應者首先接收事件读规。代表性的就是:視圖是第一響應者對象抓督。一個對象要成為第一響應者需要做兩件事:

  • 1、重寫canBecomeFirstResponder方法束亏,返回true铃在,接收成為第一響應者信息
  • 2、以及becomeFirstResponder方法碍遍,如果有必要定铜,對象能夠自己給自己發(fā)送信息

注意:

在對象被賦值成為第一響應者之前,確保APP已經(jīng)建立的對象圖形(object graph)怕敬。例如:我們可以在viewDidAppear:方法中調用becomeFirstResponder方法揣炕,但是,如果我們嘗試viewWillAppear:中賦值第一響應者东跪,我們的對象圖形可能還沒有建立畸陡,所以becomeFirstResponder方法將返回false矮烹。

響應者鏈遵守事件傳遞的具體路徑

如果最初的對象hit-test視圖或者第一響應者(first responder)不能夠處理事件,UIKit將傳遞事件到響應者鏈中的下一個響應者罩锐。每一個響應者都會決定是否處理事件還是調用nextResponder方法將事件傳遞給下級響應者。該過程一直到有一個響應者對象能夠處理事件或者沒有下級響應者為止卤唉。

下圖顯示了兩個APP配置下2種不同事件類型的路徑傳遞涩惑,事件傳遞路徑取決于具體的結構,所有的事件傳遞都遵守相同的起始桑驱。

image.png

左邊App事件所傳遞的路徑

1竭恬、初始視圖(initial view)將嘗試著處理事件或消息。如果它不能處理事件熬的,將傳遞事件到自己的父視圖(superview)痊硕,因為初始視圖并不是它所在視圖控制器中視圖層級的最頂部視圖
2、父視圖(superview)將嘗試處理所傳遞的事件押框,如果父視圖不能夠處理事件岔绸,該事件將傳遞到它自己的父視圖,因為它仍然不是視圖層級的最頂部視圖
3橡伞、視圖控制器視圖層級中最頂部視圖(topmost view)將嘗試處理所傳遞的事件盒揉,如果最頂部視圖不能夠處理事件,它將傳遞事件給它的視圖控制器
4兑徘、視圖控制器(view controller)將嘗試處理所傳遞事件刚盈,如果它不能夠處理事件,該事件將被傳遞到window
5挂脑、如果window對象不能夠處理事件藕漱,它將傳遞事件到APP全局單例對象(singleton app object).
6、如果app對象不能夠處理事件崭闲,該事件將被放棄

右邊App事件的傳遞流程

1肋联、視圖傳遞事件到它所在的視圖控制器的視圖層級中,一直到最頂部視圖镀脂。
2牺蹄、最頂部視圖將傳遞事件到它的視圖控制器
3、視圖控制器將傳遞事件到它的最頂部視圖的父視圖薄翅。1~3步重復沙兰,直到找到根控制器
4、根視圖控制器將傳遞事件到window對象
5翘魄、window對象將傳遞事件到app對象

唯一不同就在于鼎天,如果當前的ViewController是有層級關系的,那么當子ViewController不能處理事件時暑竟,它會將事件繼續(xù)往上傳遞斋射,直到傳遞到其Root ViewController育勺,其他流程是一樣的。

事件的傳遞和響應鏈

傳遞鏈

由系統(tǒng)向離用戶最近的view傳遞 UIKit –> active app’s event queue –> window –> root view –>……–>lowest view

響應鏈

由離用戶最近的view向系統(tǒng)傳遞罗岖。initial view –> super view –> …..–> view controller –> window –> Application

hitTest:withEvent:的使用

  • 1涧至、增加視圖的觸摸區(qū)域

比如:按鈕本身大小為20和20,由于太小不方便操作桑包,所以可以通過自定義UIButton南蓬,重寫hitTest方法,增加點擊區(qū)域哑了。下面實現(xiàn)每個方向增加40的可點擊區(qū)域赘方,具體實現(xiàn)代碼:

class MyButton: UIButton {
    override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
        if !self.userInteractionEnabled || self.hidden || self.alpha == 0 {
            return nil
        }
        
        // 擴大點擊區(qū)域
        if CGRectContainsPoint(CGRectInset(self.bounds, -40, -40), point) {
            for subview in self.subviews.reverse() {
                let convertPoint = subview.convertPoint(point, fromView:self)
                if let sview = subview.hitTest(convertPoint, withEvent: event) {
                    return sview
                }
            }
            return self
        }
        return nil
    }
}

hitTest:withEvent:方法首先檢查視圖是否允許接收觸摸事件。視圖允許接收觸摸事件的條件是:

  • 視圖不是隱藏的:self.hidden == NO
  • 視圖是允許交互的:self.userInteractionEnabled ==true
  • 視圖透明度大于:0.01:self.alpha >0.01
  • 視圖包含這個點: pointInside:withEvent: ==true

如果視圖允許接收觸摸事件弱左,這個方法通過從后往前發(fā)送hitTest:withEvent:消息給每一個子視圖來穿過接收者的子樹窄陡,直到子視圖中的一個返回nil。這些子視圖中的第一個返回的非nil就是在觸摸點下面的最前面的視圖拆火,被接收者返回跳夭。如果所有的子視圖都返回nil或者接收者沒有子視圖,那么返回接收者自己榜掌。否則优妙,如果視圖不允許接收觸摸事件,這個方法返回nil而根本不會傳遞到接收者的子樹憎账。因此套硼,hit-test可能不會訪問所有的視圖體系結構中的視圖。

測試功能

  func testExpandButtonClickArea(){
        //為了便于觀察胞皱,添加一個背景視圖邪意,大小正好為100*100
        let backgroundView = UIView(frame:  CGRect(x: 60, y: 160, width: 100, height: 100))
        backgroundView.backgroundColor = UIColor.purpleColor()
        view.addSubview(backgroundView)
        
        let btn = MyButton(type: .Custom)
        btn.frame = CGRect(x: 100, y: 200, width: 20, height: 20)
        btn.backgroundColor = UIColor.redColor()
        btn.setTitle("btn", forState: .Normal)
        btn.addTarget(self, action: #selector(UIButtonViewController.tapButton), forControlEvents: .TouchUpInside)
        view.addSubview(btn)
    }
    
    func tapButton(){
        print("button has been pressed!");
    }

點擊紫色區(qū)域內容溉躲,同樣可以響應點擊事件祝蝠,可以在console看到打印輸出:button has been pressed!

20160719095223943.png
  • 2梆暖、實現(xiàn)傳遞事件到點擊視圖之下的視圖

有的時候對于一個視圖忽略觸摸事件并傳遞給下面的視圖是很重要的凹炸。例如,假設一個透明的視圖覆蓋在應用內所有視圖的最上面缘圈。覆蓋層有子視圖應該相應觸摸事件的一些控件和按鈕靴拱。但是觸摸覆蓋層的其他區(qū)域應該傳遞給覆蓋層下面的視圖国觉。為了完成這個行為酒贬,覆蓋層需要覆蓋hitTest:withEvent:方法來返回包含觸摸點的子視圖中的一個又憨,然后其他情況返回nil,包括覆蓋層包含觸摸點的情況:

class SHView: UIView {
    override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
        var hitTestView = super.hitTest(point, withEvent:event)
        if hitTestView == self{
           hitTestView = nil
        }
        return hitTestView;
    }
}

測試部分代碼:

func testCoverView(){
        let btn1 = UIButton(type: .Custom)
        btn1.frame = CGRect(x: 80, y: 200, width: 20, height: 20)
        btn1.backgroundColor = UIColor.redColor()
        btn1.setTitle("btn1", forState: .Normal)
        btn1.addTarget(self, action: #selector(OverSuperViewController.tapButton(_:)), forControlEvents: .TouchUpInside)
        view.addSubview(btn1)
        
        let btn2 = UIButton(type: .Custom)
        btn2.frame = CGRect(x: 120, y: 200, width: 20, height: 20)
        btn2.backgroundColor = UIColor.yellowColor()
        btn2.setTitle("btn2", forState: .Normal)
        btn2.addTarget(self, action: #selector(OverSuperViewController.tapButton(_:)), forControlEvents: .TouchUpInside)
        view.addSubview(btn2)
        
        //添加一個覆蓋層
        let backgroundView = SHView(frame:  CGRect(x: 60, y: 160, width: 100, height: 100))
        backgroundView.backgroundColor = UIColor.purpleColor()
        backgroundView.alpha = 0.75;
        view.addSubview(backgroundView)
    }
 
    func tapButton(button:UIButton){
        print("button = %@,title = %@",button,button.currentTitle);
    }

當點擊覆蓋層的時候锭吨,如果點擊的位置屬于對應的按鈕的區(qū)域蠢莺,將響應對應的觸發(fā)事件,點擊btn1將打印按鈕1的相關信息零如,點擊按鈕2將打印按鈕2的相關信息躏将。頁面效果如下:

屏幕快照 2019-02-22 下午5.28.33.png
  • 3锄弱、超出父視圖區(qū)域部分響應事件

首先看一下頁面效果:當前頁面上有3個控件,紫色視圖是紅色視圖的子視圖祸憋,紅色視圖是灰色視圖的子視圖会宪。最上面是一個按鈕,方便我們進行測試:現(xiàn)在我們要實現(xiàn)點擊紅色視圖之外的紫色區(qū)域能夠響應事件蚯窥。

20160719103214623.png

實現(xiàn)代碼:自定義TestView實現(xiàn)hitTest方法狈谊,并調用我們對UIView的擴展方法

class TestView: UIView {
    override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
        super.hitTest(point, withEvent: event)
        return overlapHitTest(point, withEvent: event)
    }
}
 
extension UIView{
    func overlapHitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
        // We should not send touch events for hidden or transparent views, or views with userInteractionEnabled set to NO;
        if !self.userInteractionEnabled || self.hidden || self.alpha == 0 {
            return nil
        }
        // If touch is inside self, self will be considered as potential result.
        var hitView: UIView? = self
        if !self.pointInside(point, withEvent: event) {
            if self.clipsToBounds {
                return nil
            } else {
                hitView = nil
            }
        }
        // Check recursively all subviews for hit. If any, return it.
        for subview in self.subviews.reverse() {
            let insideSubview = self.convertPoint(point, toView: subview)
            if let sview = subview.overlapHitTest(insideSubview, withEvent: event) {
                return sview
            }
        }
        // Else return self or nil depending on result from step 2.
        return hitView
    }
}

測試部分代碼:

 func testOverSuperview(){
        
        let view1 = TestView(frame:CGRect(x: 100, y: 100, width: 200, height: 200))
        view1.backgroundColor = UIColor.lightGrayColor()
        view.addSubview(view1)
        
        let view2 = UIView(frame: CGRect(x: 40, y: 40, width: 100, height: 100))
        view2.backgroundColor = UIColor.redColor()
        view1.addSubview(view2)
        
        let view3 = UIButton(type: .Custom)
        view3.frame = (frame: CGRect(x: 10, y: 10, width: 200, height: 80))
        view3.backgroundColor = UIColor.purpleColor()
        view3.addTarget(self, action: #selector(ThirdViewController.tapButton), forControlEvents: .TouchUpInside)
        view2.addSubview(view3)
    }
    
    func tapButton(){
        print("button has been pressed!");
    }

參考

UIResponder

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市沟沙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌壁榕,老刑警劉巖矛紫,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異牌里,居然都是意外死亡颊咬,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進店門牡辽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來喳篇,“玉大人,你說我怎么就攤上這事态辛◆锢剑” “怎么了?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵奏黑,是天一觀的道長炊邦。 經(jīng)常有香客問我,道長熟史,這世上最難降的妖魔是什么馁害? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮蹂匹,結果婚禮上碘菜,老公的妹妹穿的比我還像新娘。我一直安慰自己限寞,他們只是感情好忍啸,可當我...
    茶點故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著昆烁,像睡著了一般吊骤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上静尼,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天白粉,我揣著相機與錄音传泊,去河邊找鬼。 笑死鸭巴,一個胖子當著我的面吹牛眷细,可吹牛的內容都是我干的。 我是一名探鬼主播鹃祖,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼溪椎,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了恬口?” 一聲冷哼從身側響起校读,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎祖能,沒想到半個月后歉秫,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡养铸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年雁芙,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钞螟。...
    茶點故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡兔甘,死狀恐怖,靈堂內的尸體忽然破棺而出鳞滨,到底是詐尸還是另有隱情洞焙,我是刑警寧澤,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布拯啦,位于F島的核電站闽晦,受9級特大地震影響,放射性物質發(fā)生泄漏提岔。R本人自食惡果不足惜仙蛉,卻給世界環(huán)境...
    茶點故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望碱蒙。 院中可真熱鬧荠瘪,春花似錦、人聲如沸赛惩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽喷兼。三九已至篮绰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間季惯,已是汗流浹背吠各。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工臀突, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人贾漏。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓候学,卻偏偏與公主長得像,于是被迫代替她去往敵國和親纵散。 傳聞我的和親對象是個殘疾皇子梳码,可洞房花燭夜當晚...
    茶點故事閱讀 44,941評論 2 355

推薦閱讀更多精彩內容