當使用者手指點選螢幕後,響應事件會按照響應者鏈逐級的找到應該響應該事件的控制元件。我們也可以自己通過程式碼來控制UI控制元件對於響應者鏈的判斷邏輯,來改變一個UI控制元件本來預設的響應邏輯。這裡不去解析響應鏈的遍歷順序,只舉例一個實際應用的場景。
首先解析關於響應鏈的一個方法:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
// ...
}
複製程式碼
每當點選事件發生後,相關的UI控制元件會按照響應鏈遍歷順序依次遞迴呼叫這個方法,返回的布林值代表該點選事件是否在當前UI控制元件內部,如果返回YES,會繼續向該UI控制元件內部的子控制元件去依次呼叫,直到找到最後的應該響應該點選事件的UI控制元件。
所以,重寫這個方法可以修改響應者鏈判定的結果,如果固定返回NO,那麼該UI控制元件就不會攔截響應事件,響應事件會繼續向後傳遞。
需求
在一個介面中
- 需要新增一個浮動的按鈕。
- 在點選按鈕的時候,底部彈出一個選單欄。
- 彈出選單欄時,按鈕滑出螢幕隱藏。
- 彈出選單欄時,介面被一個半透明蒙版遮蓋。
- 彈出選單欄時,點選半透明蒙版關閉選單欄。
正常的實現方式
正常的實現方式一般如下:
- 介面新增一個子控制元件 按鈕。
- 介面新增一個子控制元件 蒙版。
- 介面新增一個子控制元件 選單欄。
通過按鈕、蒙版、選單欄的各種互動實現三個控制元件的移動、顯示邏輯才完成需求。
利用重寫響應者鏈方法進行封裝
只建立一個子控制元件 AlertView,AlertView建立時和介面的試圖大小一致,以達到覆蓋整個介面實現蒙版的功能。
將按鈕、選單欄全部新增到AlertView中,使其稱為AlertView的子控制元件。
選單欄由一個UITableView物件實現,預設的frame中的y為螢幕的高度,這樣達到隱藏的效果:
- (UITableView *)tableView {
if (!_tableView) {
CGFloat x = 0;
CGFloat w = kScreenWidth;
CGFloat h = kCellHeight * 3 + (kIsIPhoneX ? 35 : 0);
CGFloat y = kScreenHeight;
_tableView = [[UITableView alloc] initWithFrame:CGRectMake(x, y, w, h)];
_tableView.backgroundColor = [UIColor whiteColor];
_tableView.delegate = self;
_tableView.dataSource = self;
_tableView.scrollEnabled = NO;
[_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"alertcell"];
}
return _tableView;
}
複製程式碼
浮動按鈕預設直接顯示在螢幕右側:
- (UIButton *)alertButton {
if (!_alertButton) {
_alertButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_alertButton setBackgroundImage:[UIImage imageNamed:@"tool"] forState:UIControlStateNormal];
_alertButton.frame = CGRectMake(kScreenWidth - 22, kScreenHeight - self.tableView.height - 10, 22, 43);
[_alertButton addTarget:self action:@selector(alertButtonClick:) forControlEvents:UIControlEventTouchUpInside];
}
return _alertButton;
}
複製程式碼
點選按鈕的時候,將選單欄向上移動,按鈕右移隱藏,並同時改變AlertView的背景色,達到顯示蒙版的效果:
- (void)alertButtonClick:(UIButton *)sender {
[UIView animateWithDuration:0.3 animations:^{
sender.x = kScreenWidth;
self.tableView.y = kScreenHeight - self.tableView.height;
self.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.3];
} completion:^(BOOL finished) {
}];
}
複製程式碼
重寫AlertView的touchesBegan:WithEvnet: 方法,點選AlertView時,下移隱藏選單欄,左移顯示按鈕,並將AlertView的背景色設定為透明度,達到隱藏蒙版的效果:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self dismissAlert];
}
- (void)dismissAlert {
[UIView animateWithDuration:0.3 animations:^{
self.alertButton.x = kScreenWidth - self.alertButton.width;
self.tableView.y = kScreenHeight;
self.backgroundColor = [UIColor clearColor];
} completion:^(BOOL finished) {
}];
}
複製程式碼
上面的程式碼有一個非常大的問題,就是即便選單欄隱藏的時候,AlertView會攔截螢幕的點選事件。下面就是最重要的地方,重寫AlertView的pointInside: withEvent: 方法:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
// 當選單欄隱藏的時候,只有點選在按鈕區域內,才會成為響應者。
if (self.tableView.y == kScreenHeight) {
return CGRectContainsPoint(self.alertButton.frame, point);
} else { // 當選單欄顯示時,採用預設的響應邏輯。
return [super pointInside:point withEvent:event];
}
}
複製程式碼
當選單欄顯示時,AlertView的使用預設的響應邏輯,攔截介面的點選事件。
當選單欄隱藏時,只有點選區域是在AlertView的浮動按鈕範圍內時,才會攔截點選事件,否則AlertView會返回NO,響應者鏈遍歷過程中會判斷AlertView螢幕點選事件不在AlertView內,繼續遍歷到當前介面,並讓當前介面成為響應者。
這樣就只需要為當前介面新增一個子控制元件即可完成浮動按鈕、蒙版、選單欄的全部需求。
其它類似的應用場景
利用上面的方式,還可以應用在其它類似的應用場景。
類似微信首頁新增好友按鈕點選後出現的下拉選單
可以建立一個僅有選單顯示面積大小的控制元件,重寫AlertView的pointInside: withEvent: 方法,直接返回YES,這樣這個選單即使frame只有顯示面積那麼大,還是能夠攔截全螢幕的點選事件。然後在重寫它的touchBegin方法去因此選單欄即可。這樣就可以省去建立一個螢幕大小的背景色為透明的蒙版控制元件,就可以達到攔截點選選單可見範圍以外區域隱藏選單欄的需求。
部分可穿透點選事件到後面的蒙版
比如一些給當前介面加修飾邊框的蒙版,邊框要攔截點選事件,但是中間透明部分又可以點選介面。這裡就可以通過重寫pointInside: withEvent: 判斷點選區域是邊框範圍就返回YES,否則就返回NO。
異形按鈕的實現
比如類似於拼圖一樣的非正方體控制元件密集排列,還需要讓它們的點選事件不能相互衝突,下面是一個具體實現的例子:iOS響應者鏈的具體應用-異形按鈕。