前言
簡單說說彈幕開發碰到的兩個小問題。
正文
-
需求:實現一個彈幕容器,裡面同時會有多行互不重疊的、運動中的彈幕 。每一條彈幕均需要支援點選事件。
-
用腳底板想的方法:在彈幕容器裡面建立幾個
UIButton
,並且addTarget
,增加點選事件。最後利用UIView
的block
API 實現動畫。 -
結果:嗯...可惜的是,程式碼執行起來,你會發現在
UIButton
運動過程,點選事件並沒有響應,而且非常奇怪的是:為什麼在UIButton
動畫過程,去點選UIButton
動畫的終點,點選事件竟然響應了??這是為什麼呢? -
Core Anmation
動畫過程原理的引用:
在iOS中,螢幕每秒鐘重繪60次。如果動畫時長比60分之一秒要長,Core Animation就需要在設定一次新值和新值生效之間,對螢幕上的圖層進行重新組織。這意味著CALayer除了“真實”值(就是你設定的值)之外,必須要知道當前顯示在螢幕上的屬性值的記錄。 每個圖層屬性的顯示值都被儲存在一個叫做
呈現圖層
的獨立圖層當中,他可以通過-presentationLayer方法來訪問。這個呈現圖層
實際上是模型圖層
的複製,但是它的屬性值代表了在任何指定時刻當前外觀效果。換句話說,你可以通過呈現圖層
的值來獲取當前螢幕上真正顯示出來的值。
補充:模型圖層在動畫開始的那一刻就已經達到終點位置,響應點選事件的也是它。
解決辦法:重寫彈幕容器 view
的 touchesBegan
方法。程式碼如下:
@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
原理講解:手指點選螢幕,經過系統響應(之前過程省略不說,文末有參考連結),呼叫UIApplication
的sendEvent:
方法,將UIEvent
傳給UIWindow
, 通過遞迴呼叫UIView
層級的hitTest(_:with:)
,結合point(inside:with:)
找到UIEvent
中每一個UITouch
所屬的UIView
(其實是想找到離觸控事件點最近的那個UIView
)。這個過程是從UIView
層級的最頂層往最底層遞迴查詢。同一層級的UIView
,會優先深度遍歷介面靠前的UIView
。找到最底層UIView
後,沿著Responder Chain
逐步向上傳遞(UIControl
子類預設會攔截傳遞)。
解決思路:重寫 ZYYBarrageView
的 hitTest(_: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];
}
}
複製程式碼
如此,可以完美解決啦~