寫(xiě)在前面
之前寫(xiě)了篇《彈幕的原理分析與實(shí)現(xiàn)》 的文章含末,最近有些閱讀的朋友提出了些疑問(wèn)佣盒,大家比較關(guān)注的一個(gè)問(wèn)題就是如何響應(yīng)彈幕的點(diǎn)擊事件,今天寫(xiě)這個(gè)續(xù)篇盯仪,主要是帶著大家一起實(shí)現(xiàn)一下功能全景。
上來(lái)開(kāi)搞
有些朋友可能覺(jué)得這太簡(jiǎn)單了牵囤,直接在彈幕的view上加一個(gè)tap的手勢(shì)不就完事了奔浅?于是乎汹桦,寫(xiě)了如下代碼:
//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)行程序,尼瑪钥弯,不起作用啊脆霎,是我寫(xiě)錯(cuò)了嗎,再次檢查一遍鹦马,全宇宙最通俗簡(jiǎn)單的幾行代碼荸频,怎么可能寫(xiě)錯(cuò)旭从,Why场仲?渠缕??付鹿?
iOS開(kāi)發(fā)機(jī)制蚜迅,當(dāng)view在animation的過(guò)程中谁不,是不會(huì)響應(yīng)任何事件的刹帕,所以移動(dòng)中的彈幕對(duì)我們這招免疫。作為一名合格的程序猿蹋辅,我們絕對(duì)不能在困難面前低頭侦另,此路不通我們換條路就行了。
言歸正傳
既然我們不能在移動(dòng)中的彈幕view上加點(diǎn)擊事件弃锐,那么我們是不是可以在彈幕view的父容器view中加點(diǎn)擊事件呢殿托,然后判斷這個(gè)點(diǎn)擊的點(diǎn)是否落在了這個(gè)彈幕view的范圍內(nèi)霹菊,如果是,我們就認(rèn)為你觸發(fā)了這個(gè)彈幕view的點(diǎn)擊事件支竹。
這么做還有一個(gè)好處就是我可以根據(jù)需求任意調(diào)整父容器view的位置旋廷,例如,默認(rèn)彈幕是顯示在屏幕下方的礼搁,如果點(diǎn)擊輸入框喚起了鍵盤(pán)柳洋,那么此時(shí)彈幕的位置應(yīng)該現(xiàn)在的鍵盤(pán)上方,所以我們只需要改變父容器view的y坐標(biāo)就可以達(dá)到目的叹坦。 尼瑪,果然程序猿的腦子天生就是用來(lái)解決問(wèn)題的募书。
第一步,如圖所示测蹲,我們?cè)贑ontroller的View上添加一個(gè)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];
}
此時(shí)我們要的效果是當(dāng)點(diǎn)擊A點(diǎn)的時(shí)候扣甲,不會(huì)有彈幕的點(diǎn)擊事件觸發(fā)篮赢,點(diǎn)擊B點(diǎn)時(shí)會(huì)響應(yīng)事件。說(shuō)起來(lái)容易琉挖,那么接下來(lái)如何實(shí)現(xiàn)呢启泣?
第二步,接受tapgesture事件并進(jìn)行點(diǎn)擊位置的判斷示辈,首先我們先看一下CALayer中有這么兩個(gè)方法寥茫,都可以達(dá)到我們的目的,
//返回包含某一點(diǎn)的最上層的子layer
- (nullable CALayer *)hitTest:(CGPoint)p;
//返回layer的bounds內(nèi)是否包含某一點(diǎn)
- (BOOL)containsPoint:(CGPoint)p;
這里邊我們選擇第一種方式矾麻,通過(guò)hitTest返回包含某個(gè)點(diǎn)的最上層的子layer纱耻,感興趣的同學(xué)也可以嘗試一下第二種方式。
這個(gè)點(diǎn)p我們可以通過(guò)gesture對(duì)象傳進(jìn)來(lái)险耀,代碼如下:
- (void)tapHandler:(UITapGestureRecognizer *)gesture {
CGPoint clickPoint = [gesture locationInView:self];
//遍歷backgroundview上的所有subviews弄喘,其實(shí)就是所有的移動(dòng)的彈幕view了
for (UIView *v in [self subviews]) {
if ([v isKindOfClass:[BulletView class]]) {
//返回point的最上層的layer,其實(shí)就是判斷point落在這個(gè)彈幕view范圍內(nèi)了
if ([v.layer.presentationLayer hitTest:point]) {
//處理點(diǎn)擊事件
break;
}
}
}
}
這樣子甩牺,我們就完成了一個(gè)移動(dòng)彈幕view的點(diǎn)擊事件了蘑志。按照這種方式運(yùn)行后,我們發(fā)現(xiàn),事件是響應(yīng)了卖漫,但是因?yàn)槭莻€(gè)tapGesture事件费尽,并不能像button那樣給用一個(gè)hilighted的效果,如何是好呢羊始?旱幼?既然這樣,我們就給用戶一個(gè)體驗(yàn)(代碼好多時(shí)候就是用來(lái)騙人的~~~)突委,在下邊這個(gè)方法中柏卤,添加幾行代碼。
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;
}
寫(xiě)到這里匀油,我們應(yīng)該舉杯慶祝了吧缘缚,終于可以響應(yīng)點(diǎn)擊事件了,然而敌蚜,這樣子真的結(jié)束了嗎桥滨?程序猿的世界就是這樣子,解決了一個(gè)問(wèn)題弛车,我們又發(fā)現(xiàn)了另一個(gè)問(wèn)題~~~~~~~
高興的太早了
實(shí)際項(xiàng)目當(dāng)中齐媒,我們必定會(huì)遇到這樣一種情況,就是在BulletBackgroundView的下一層可能還會(huì)有響應(yīng)的視圖在纷跛,比如:
對(duì)于新聞?lì)惖腶pp喻括,彈幕會(huì)飄在新聞詳情頁(yè)面上,然而我們需要實(shí)現(xiàn)在彈幕空白區(qū)域贫奠,可以去響應(yīng)后邊view的其他操作唬血,比如點(diǎn)擊、滑動(dòng)等唤崭。但按照我們上邊實(shí)現(xiàn)的機(jī)制拷恨,我們將tapGesture加在了BulletBackgroundView上,意味著點(diǎn)擊事件就不會(huì)傳到下一層view上了浩姥,想好了開(kāi)頭挑随,卻沒(méi)有想到結(jié)局,淚奔~~~
還有像這種的勒叠,在彈幕下邊還有一個(gè)按鈕button兜挨,那么當(dāng)沒(méi)有彈幕飄過(guò)時(shí),需要響應(yīng)button事件眯分,當(dāng)彈幕飄過(guò)拌汇,彈幕view和按鈕重合時(shí),需要響應(yīng)彈幕的事件而屏蔽掉button的事件弊决,
針對(duì)以上問(wèn)題噪舀,我們要進(jìn)一步對(duì)程序進(jìn)行改進(jìn)魁淳。
完美方案
根據(jù)以上分析,我們要解決兩個(gè)問(wèn)題:
- 如何讓下層的view也能響應(yīng)事件与倡,這個(gè)很簡(jiǎn)單界逛,我們只要把tapGesture加到最下邊的view上就可以保證事件不會(huì)被BulletBackgroundView吃掉了。
- 在下層事件和彈幕view事件沖突時(shí)纺座,如何保證執(zhí)行的是彈幕點(diǎn)擊事件息拜,而不是下層view的。這里我們就需要用到view的一個(gè)重載方法了
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
如果return nil净响,事件將向下層傳遞少欺,如果return self,事件將被本身view攔截掉不會(huì)向下層傳遞馋贤。解決了上邊兩個(gè)問(wèn)題赞别,我們來(lái)重新整理一下我們的代碼(《iOS彈幕的原理分析與實(shí)現(xiàn)》中代碼):
第一步,在UIViewController的View上添加BulletBackgroundView配乓,并且將彈幕添加到這個(gè)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落在這個(gè)彈幕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);
}
}
}
查看完成代碼,下載地址丈挟。