iOS彈幕的原理分析與實(shí)現(xiàn)(續(xù)篇)

寫在前面

之前寫了篇《彈幕的原理分析與實(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á)到目的。 尼瑪,果然程序猿的腦子天生就是用來解決問題的。

手勢點(diǎn)擊.png

第一步,如圖所示,我們在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)的視圖在,比如:

滑動屏幕.png

對于新聞類的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的事件,

帶有按鈕.png

針對以上問題,我們要進(jìn)一步對程序進(jìn)行改進(jìn)。

完美方案

根據(jù)以上分析,我們要解決兩個問題:

  1. 如何讓下層的view也能響應(yīng)事件,這個很簡單,我們只要把tapGesture加到最下邊的view上就可以保證事件不會被BulletBackgroundView吃掉了。
  2. 在下層事件和彈幕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);
        }
    }

}

查看完成代碼,下載地址。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,765評論 25 709
  • 2017.02.22 可以練習(xí),每當(dāng)這個時候,腦袋就犯困,我這腦袋真是神奇呀,一說讓你做事情,你就犯困,你可不要太...
    Carden閱讀 1,490評論 0 1
  • 版本記錄 前言 在我們做直播等視頻類app的時候,總是有顯示和發(fā)送彈幕的要求,彈幕可以方便用戶進(jìn)行溝通和互動,增加...
    刀客傳奇閱讀 5,463評論 8 16
  • 思念如火,荼靡西天,無緣無故的想起你,知道我還是在懷念你,懷念你的陪伴! 一直以來,不喜歡有人打擾自己,每天最開心...
    梅園遺珠閱讀 274評論 0 1
  • 腦海中循環(huán)著傳播學(xué)老師的一句話,就是,當(dāng)角色發(fā)生了變化時,雙方的關(guān)系將會發(fā)生改變。只是,有時候我不知道怎么處理~ ...
    惠木子小姐閱讀 210評論 0 0

友情鏈接更多精彩內(nèi)容