UI層級的精簡和控制元件的封裝

Whip發表於2019-06-13

當使用者手指點選螢幕後,響應事件會按照響應者鏈逐級的找到應該響應該事件的控制元件。我們也可以自己通過程式碼來控制UI控制元件對於響應者鏈的判斷邏輯,來改變一個UI控制元件本來預設的響應邏輯。這裡不去解析響應鏈的遍歷順序,只舉例一個實際應用的場景。

首先解析關於響應鏈的一個方法:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    // ...
}
複製程式碼

每當點選事件發生後,相關的UI控制元件會按照響應鏈遍歷順序依次遞迴呼叫這個方法,返回的布林值代表該點選事件是否在當前UI控制元件內部,如果返回YES,會繼續向該UI控制元件內部的子控制元件去依次呼叫,直到找到最後的應該響應該點選事件的UI控制元件。

所以,重寫這個方法可以修改響應者鏈判定的結果,如果固定返回NO,那麼該UI控制元件就不會攔截響應事件,響應事件會繼續向後傳遞。

需求

在一個介面中

  • 需要新增一個浮動的按鈕。
  • 在點選按鈕的時候,底部彈出一個選單欄。
  • 彈出選單欄時,按鈕滑出螢幕隱藏。
  • 彈出選單欄時,介面被一個半透明蒙版遮蓋。
  • 彈出選單欄時,點選半透明蒙版關閉選單欄。

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內,繼續遍歷到當前介面,並讓當前介面成為響應者。

UI層級的精簡和控制元件的封裝

這樣就只需要為當前介面新增一個子控制元件即可完成浮動按鈕、蒙版、選單欄的全部需求。

其它類似的應用場景

利用上面的方式,還可以應用在其它類似的應用場景。

類似微信首頁新增好友按鈕點選後出現的下拉選單

可以建立一個僅有選單顯示面積大小的控制元件,重寫AlertView的pointInside: withEvent: 方法,直接返回YES,這樣這個選單即使frame只有顯示面積那麼大,還是能夠攔截全螢幕的點選事件。然後在重寫它的touchBegin方法去因此選單欄即可。這樣就可以省去建立一個螢幕大小的背景色為透明的蒙版控制元件,就可以達到攔截點選選單可見範圍以外區域隱藏選單欄的需求。

部分可穿透點選事件到後面的蒙版

比如一些給當前介面加修飾邊框的蒙版,邊框要攔截點選事件,但是中間透明部分又可以點選介面。這裡就可以通過重寫pointInside: withEvent: 判斷點選區域是邊框範圍就返回YES,否則就返回NO。

異形按鈕的實現

比如類似於拼圖一樣的非正方體控制元件密集排列,還需要讓它們的點選事件不能相互衝突,下面是一個具體實現的例子:iOS響應者鏈的具體應用-異形按鈕

相關文章