寫在前面
之前寫了篇《彈幕的原理分析與實(shí)現(xiàn)》 的文章,最近有些閱讀的朋友提出了些疑問,大家比較關(guān)注的一個問題就是如何響應(yīng)彈幕的點(diǎn)擊事件,今天寫這個續(xù)篇,主要是帶著大家一起實(shí)現(xiàn)一下功能。
上來開搞
有些朋友可能覺得這太簡單了,直接在彈幕的view上加一個tap的手勢不就完事了?于是乎,寫了如下代碼:
//BulletView.m 文件中
- (void)addTapGesture {
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapHandler:)];
[self addGestureRecognizer:tap];
}
- (void)tapHandler:(UITapGestureRecognizer *)gesture {
if (self.tapBlock) {
self.tapBlock();
}
}
運(yùn)行程序,尼瑪,不起作用啊,是我寫錯了嗎,再次檢查一遍,全宇宙最通俗簡單的幾行代碼,怎么可能寫錯,Why????
iOS開發(fā)機(jī)制,當(dāng)view在animation的過程中,是不會響應(yīng)任何事件的,所以移動中的彈幕對我們這招免疫。作為一名合格的程序猿,我們絕對不能在困難面前低頭,此路不通我們換條路就行了。
言歸正傳
既然我們不能在移動中的彈幕view上加點(diǎn)擊事件,那么我們是不是可以在彈幕view的父容器view中加點(diǎn)擊事件呢,然后判斷這個點(diǎn)擊的點(diǎn)是否落在了這個彈幕view的范圍內(nèi),如果是,我們就認(rèn)為你觸發(fā)了這個彈幕view的點(diǎn)擊事件。
這么做還有一個好處就是我可以根據(jù)需求任意調(diào)整父容器view的位置,例如,默認(rèn)彈幕是顯示在屏幕下方的,如果點(diǎn)擊輸入框喚起了鍵盤,那么此時彈幕的位置應(yīng)該現(xiàn)在的鍵盤上方,所以我們只需要改變父容器view的y坐標(biāo)就可以達(dá)到目的。 尼瑪,果然程序猿的腦子天生就是用來解決問題的。

第一步,如圖所示,我們在Controller的View上添加一個BulletBackgroundView,然后將彈幕view添加在BulletBackgroundView上。
- (void)addBulletView:(BulletView *)bulletView {
bulletView.frame = CGRectMake(CGRectGetWidth(self.view.frame)+50, 20 + 34 * bulletView.trajectory, CGRectGetWidth(bulletView.bounds), CGRectGetHeight(bulletView.bounds));
[self.bulletBgView addSubview:bulletView];//添加到bulletBgView上
[bulletView startAnimation];
}
此時我們要的效果是當(dāng)點(diǎn)擊A點(diǎn)的時候,不會有彈幕的點(diǎn)擊事件觸發(fā),點(diǎn)擊B點(diǎn)時會響應(yīng)事件。說起來容易,那么接下來如何實(shí)現(xiàn)呢?
第二步,接受tapgesture事件并進(jìn)行點(diǎn)擊位置的判斷,首先我們先看一下CALayer中有這么兩個方法,都可以達(dá)到我們的目的,
//返回包含某一點(diǎn)的最上層的子layer
- (nullable CALayer *)hitTest:(CGPoint)p;
//返回layer的bounds內(nèi)是否包含某一點(diǎn)
- (BOOL)containsPoint:(CGPoint)p;
這里邊我們選擇第一種方式,通過hitTest返回包含某個點(diǎn)的最上層的子layer,感興趣的同學(xué)也可以嘗試一下第二種方式。
這個點(diǎn)p我們可以通過gesture對象傳進(jìn)來,代碼如下:
- (void)tapHandler:(UITapGestureRecognizer *)gesture {
CGPoint clickPoint = [gesture locationInView:self];
//遍歷backgroundview上的所有subviews,其實(shí)就是所有的移動的彈幕view了
for (UIView *v in [self subviews]) {
if ([v isKindOfClass:[BulletView class]]) {
//返回point的最上層的layer,其實(shí)就是判斷point落在這個彈幕view范圍內(nèi)了
if ([v.layer.presentationLayer hitTest:point]) {
//處理點(diǎn)擊事件
break;
}
}
}
}
這樣子,我們就完成了一個移動彈幕view的點(diǎn)擊事件了。按照這種方式運(yùn)行后,我們發(fā)現(xiàn),事件是響應(yīng)了,但是因?yàn)槭莻€tapGesture事件,并不能像button那樣給用一個hilighted的效果,如何是好呢??既然這樣,我們就給用戶一個體驗(yàn)(代碼好多時候就是用來騙人的~~~),在下邊這個方法中,添加幾行代碼。
if ([v.layer.presentationLayer hitTest:point]) {
//處理點(diǎn)擊效果
v.backgroundColor = [UIColor blueColor];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
v.backgroundColor = [UIColor redColor];
});
//處理點(diǎn)擊事件
break;
}
寫到這里,我們應(yīng)該舉杯慶祝了吧,終于可以響應(yīng)點(diǎn)擊事件了,然而,這樣子真的結(jié)束了嗎?程序猿的世界就是這樣子,解決了一個問題,我們又發(fā)現(xiàn)了另一個問題~~~~~~~
高興的太早了
實(shí)際項(xiàng)目當(dāng)中,我們必定會遇到這樣一種情況,就是在BulletBackgroundView的下一層可能還會有響應(yīng)的視圖在,比如:

對于新聞類的app,彈幕會飄在新聞詳情頁面上,然而我們需要實(shí)現(xiàn)在彈幕空白區(qū)域,可以去響應(yīng)后邊view的其他操作,比如點(diǎn)擊、滑動等。但按照我們上邊實(shí)現(xiàn)的機(jī)制,我們將tapGesture加在了BulletBackgroundView上,意味著點(diǎn)擊事件就不會傳到下一層view上了,想好了開頭,卻沒有想到結(jié)局,淚奔~~~
還有像這種的,在彈幕下邊還有一個按鈕button,那么當(dāng)沒有彈幕飄過時,需要響應(yīng)button事件,當(dāng)彈幕飄過,彈幕view和按鈕重合時,需要響應(yīng)彈幕的事件而屏蔽掉button的事件,

針對以上問題,我們要進(jìn)一步對程序進(jìn)行改進(jìn)。
完美方案
根據(jù)以上分析,我們要解決兩個問題:
- 如何讓下層的view也能響應(yīng)事件,這個很簡單,我們只要把tapGesture加到最下邊的view上就可以保證事件不會被BulletBackgroundView吃掉了。
- 在下層事件和彈幕view事件沖突時,如何保證執(zhí)行的是彈幕點(diǎn)擊事件,而不是下層view的。這里我們就需要用到view的一個重載方法了
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
如果return nil,事件將向下層傳遞,如果return self,事件將被本身view攔截掉不會向下層傳遞。解決了上邊兩個問題,我們來重新整理一下我們的代碼(《iOS彈幕的原理分析與實(shí)現(xiàn)》中代碼):
第一步,在UIViewController的View上添加BulletBackgroundView,并且將彈幕添加到這個BulletBackgroundView上。
//UIViewController.m
- (void)viewDidLoad {
//……
[self.view addSubview:bulletView];
//……
}
- (void)addBulletView:(BulletView *)bulletView {
bulletView.frame = CGRectMake(CGRectGetWidth(self.view.frame)+50, 20 + 34 * bulletView.trajectory, CGRectGetWidth(bulletView.bounds), CGRectGetHeight(bulletView.bounds));
[self.bulletBgView addSubview:bulletView];
[bulletView startAnimation];
}
第二步,給UIViewController的View添加TapGesture事件。
//UIViewController.m
- (void)viewDidLoad {
//……
[self.view addSubview:bulletView];
//綁定tap事件
[self addTapGesture];
//……
}
- (void)addTapGesture {
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapHandler:)];
tap.cancelsTouchesInView = NO;
[self.view addGestureRecognizer:tap];
}
- (void)tapHandler:(UITapGestureRecognizer *)gesture {
//將處理的邏輯放到BulletBackgroundView中實(shí)現(xiàn)
[self.bulletBgView dealTapGesture:gesture block:^(BulletView *bulletView){
NSLog(@"%@", bulletView.lbComment.text);
}];
}
第三步,在BulletBackgroundView中處理點(diǎn)擊事件判斷邏輯。
//BulletBackgroundView.m
//如果在當(dāng)前View中判斷了point落在的彈幕view范圍內(nèi),則事件不在向下傳遞
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if ([self findClickBulletView:point]) {
return self;
}
return nil;
}
- (BulletView *)findClickBulletView:(CGPoint)point {
BulletView *bulletView = nil;
for (UIView *v in [self subviews]) {
if ([v isKindOfClass:[BulletView class]]) {
//返回point的最上層的layer,其實(shí)就是判斷point落在這個彈幕view范圍內(nèi)了
if ([v.layer.presentationLayer hitTest:point]) {
bulletView = (BulletView *)v;
break;
}
}
}
return bulletView;
}
//處理TapGesture事件
- (void)dealTapGesture:(UITapGestureRecognizer *)gesture block:(void (^)(BulletView *bulletView))block {
CGPoint clickPoint = [gesture locationInView:self];
BulletView *bulletView = [self findClickBulletView:clickPoint];
//找到了點(diǎn)擊的彈幕view,處理點(diǎn)擊效果,并將bulletView傳回Controller進(jìn)行處理
if (bulletView) {
bulletView.backgroundColor = [UIColor blueColor];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
bulletView.backgroundColor = [UIColor redColor];
});
if (block) {
block(bulletView);
}
}
}
查看完成代碼,下載地址。