iOS 彈幕開發過程碰到的問題總結

鄭一一發表於2017-12-14

前言

簡單說說彈幕開發碰到的兩個小問題。

正文

  • 需求:實現一個彈幕容器,裡面同時會有多行互不重疊的、運動中的彈幕 。每一條彈幕均需要支援點選事件。

  • 用腳底板想的方法:在彈幕容器裡面建立幾個 UIButton,並且 addTarget,增加點選事件。最後利用 UIViewblock API 實現動畫。

  • 結果:嗯...可惜的是,程式碼執行起來,你會發現在 UIButton 運動過程,點選事件並沒有響應,而且非常奇怪的是:為什麼在 UIButton 動畫過程,去點選 UIButton 動畫的終點,點選事件竟然響應了??這是為什麼呢?

  • Core Anmation 動畫過程原理的引用:

在iOS中,螢幕每秒鐘重繪60次。如果動畫時長比60分之一秒要長,Core Animation就需要在設定一次新值和新值生效之間,對螢幕上的圖層進行重新組織。這意味著CALayer除了“真實”值(就是你設定的值)之外,必須要知道當前顯示在螢幕上的屬性值的記錄。 每個圖層屬性的顯示值都被儲存在一個叫做呈現圖層的獨立圖層當中,他可以通過-presentationLayer方法來訪問。這個呈現圖層實際上是模型圖層的複製,但是它的屬性值代表了在任何指定時刻當前外觀效果。換句話說,你可以通過呈現圖層的值來獲取當前螢幕上真正顯示出來的值。

補充:模型圖層在動畫開始的那一刻就已經達到終點位置,響應點選事件的也是它。

解決辦法:重寫彈幕容器 viewtouchesBegan 方法。程式碼如下:

@interface ZYYBarrageView ()
@property (nonatomic, strong) UIView *redView; // 將要做平移的 subview
@end

@implementation ZYYBarrageView

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self commonInit];
    }
    
    return self;
}

- (void)commonInit {
    self.redView = [[UIView alloc] initWithFrame:CGRectMake(0.f, 0.f, 30.f, 30.f)];
    self.redView.backgroundColor = [UIColor redColor];
    [self addSubview:self.redView];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 重點開始!!UITouch 獲取在 barrageView 座標系下的座標
    CGPoint touchPoint = [[touches anyObject] locationInView:self];
    // 判斷觸控點是否在 redView 的呈現樹的框框之中
    if ([self.redView.layer.presentationLayer hitTest:touchPoint]) {
        // 響應紅色塊點選
        return;
    } else {

    }
}
複製程式碼

進一步的需求:在 ZYYBarrageView 的同一層級,但層次偏後會有 UIButton。正常情況下,因為 ZYYBarrageView 的存在,UIButton 是無法響應點選事件的。程式碼如下:

@property (nonatomic, strong) ZYYBarrageView *barrageView; // 彈幕 view 支援多行 view 在裡面進行運動
@property (nonatomic, strong) UIButton *yellowBtn; // 靠後的 UIButton

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // self.yellowBtn 位於 self.barrageView 之後
    [self.view addSubview:self.yellowBtn];
    [self.view addSubview:self.barrageView];
}

- (ZYYBarrageView *)barrageView {
    if (!_barrageView) {
        _barrageView = [[ZYYBarrageView alloc] initWithFrame:CGRectMake(0.f, 30.f, SCREEN_WIDTH, 30.f)];
        _barrageView.backgroundColor = [UIColor clearColor];
    }
    
    return _barrageView;
}

- (UIButton *)yellowBtn {
    if (!_yellowBtn) {
        _yellowBtn = [UIButton buttonWithType:UIButtonTypeCustom];
        _yellowBtn.frame = CGRectMake(90.f, 30.f, 80.f, 30.f);
        _yellowBtn.backgroundColor = [UIColor yellowColor];
        [_yellowBtn setTitle:@"黃色按鈕" forState:UIControlStateNormal];
        [_yellowBtn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
        [_yellowBtn addTarget:self action:@selector(onYellowBtn:) forControlEvents:UIControlEventTouchUpInside];
    }
    
    return _yellowBtn;
}

- (void)onYellowBtn:(id)sender {
    // 響應黃色按鈕
}
複製程式碼

怎麼辦?

Responder Chain 原理講解:手指點選螢幕,經過系統響應(之前過程省略不說,文末有參考連結),呼叫 UIApplicationsendEvent: 方法,將 UIEvent 傳給 UIWindow, 通過遞迴呼叫 UIView 層級的 hitTest(_:with:),結合 point(inside:with:) 找到 UIEvent 中每一個UITouch 所屬的 UIView(其實是想找到離觸控事件點最近的那個 UIView)。這個過程是從 UIView 層級的最頂層往最底層遞迴查詢。同一層級的 UIView,會優先深度遍歷介面靠前的 UIView。找到最底層 UIView 後,沿著 Responder Chain 逐步向上傳遞(UIControl 子類預設會攔截傳遞)。

解決思路:重寫 ZYYBarrageViewhitTest(_:with:) 方法。程式碼如下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    BOOL isPointInsideSubview = [self.redView.layer.presentationLayer hitTest:point];
    if (isPointInsideSubview == NO) {
        // 如果沒有點選在移動的 redView 上,返回 nil
        // 系統會去遍歷位於 ZYYBarrageView 後面的 UIButton,UIButton 能得到響應
        return nil;
    } else {
        return [super hitTest:point withEvent:event];
    }
}
複製程式碼

如此,可以完美解決啦~

參考連結

iOS觸控事件的流動

隱式動畫關於呈現樹的講解

相關文章