美文网首页iOS 技术文集动画相关iOSer
iOS弹幕的原理分析与实现

iOS弹幕的原理分析与实现

作者: 43f8d00feb3b | 来源:发表于2016-02-22 20:04 被阅读12906次

    写在前边

    为便于大家学习,在视频网站上录了相应的视频
    想看视频的朋友点这里哟~~

    何为弹幕?~~

    随着90后的不断崛起,弹幕越来越受到年轻人的喜爱。所谓弹幕,我的理解就是评论的另一种表现形式,更能吸引用户眼球,增强用户体验,增加用户参与感和使用粘度。现在国内比较火的弹幕类视频网站A站B站,深受年轻人群的追捧。下边就是B站的一个展示效果。

    弹幕demo
    另外一些新闻资讯类的app也开始实现弹幕功能,为博得用户喜爱,比如唔哩、微在、橘子娱乐等等。下面我们就来一起分析一下,弹幕在iOS端是如何实现的呢?

    原理分析与实现~~

    首先我们来分析一下弹幕的特点。

    • 一般情况下弹幕都是从屏幕右侧进入并从屏幕左侧飞出。
    • 弹幕进入屏幕后按照一定轨迹来移动。
    • 弹幕移动速度根据内容长度决定,内容越长,移动速度越快。
    • 一个弹幕完全进入屏幕后,后边会继续飞入一个新的弹幕。
    • 弹幕是循环滚动播放的。

    基于以上特点,我们设计出来的弹幕形式大致如下图所示,默认只有三个弹道(弹幕飞行轨迹)来展示弹幕飞行效果。

    1. 初始化三个弹幕1、2、3准备进入屏幕,DataSource为弹幕资源的数据来源地。


      1.初始化弹幕
    2. 当弹幕陆陆续续进入屏幕,飞行速度与弹幕长度相关,每当其中一个弹道的弹幕完全进入屏幕后,则从数据池中取出一个弹幕在相应弹道进入屏幕,如弹幕4。


      2. 弹幕进入屏幕
    3. 如果某条弹幕已经完全飞出屏幕,则将此弹幕从屏幕中删除,如弹幕1和弹幕3。


      3. 弹幕飞出屏幕
    4. 当弹幕全部飞出屏幕,回到步骤1,重新滚动播放

    技术实现~~

    下面从技术层面来讨论一下实现细节,以下是部分核心代码,完整代码参见这里
    首先来看一下弹幕的生成过程,初始化三个弹幕轨迹,如果不足三个,创建2个或者1个轨迹,代码(BulletManager.m)如下:

    - (void)start {
        if (self.tmpComments.count == 0) {
            [self.tmpComments addObjectsFromArray:self.allComments];
        }
        self.bStarted = YES;
        self.bStopAnimation = NO;
        [self initBulletCommentView];
    }
    /**
      *  初始化弹幕
      */
    - (void)initBulletCommentView {
        //初始化三条弹幕轨迹
        NSMutableArray *arr = [NSMutableArray arrayWithArray:@[@(0), @(1), @(2)]];
        for (int i = 3; i > 0; i--) {
            NSString *comment = [self.tmpComments firstObject];
            if (comment) {
                [self.tmpComments removeObjectAtIndex:0];
                //随机生成弹道创建弹幕进行展示(弹幕的随机飞入效果)
                NSInteger index = arc4random()%arr.count;
                Trajectory trajectory = [[arr objectAtIndex:index] intValue];
                [arr removeObjectAtIndex:index];
                [self createBulletComment:comment trajectory:trajectory];
            } else {
                break;
            }
        }
    }
    

    创建弹幕view,对弹幕view的各种位置状态进行监听并做出相对应的处理。

     /**
      *  创建弹幕
      *
      *  @param comment    弹幕内容
      *  @param trajectory 弹道位置
      */
      - (void)createBulletComment:(NSString *)comment trajectory:(Trajectory)trajectory {
           if (self.bStopAnimation) {
               return;
           }
           //创建一个弹幕view
           BulletView *view = [[BulletView alloc] initWithContent:comment];
           //设置运行轨迹
           view.trajectory = trajectory;
           __weak BulletView *weakBulletView = view;
           __weak BulletManager *myself = self;
           /**
             *  弹幕view的动画过程中的回调状态
             *  Start:创建弹幕在进入屏幕之前
             *  Enter:弹幕完全进入屏幕
             *  End:弹幕飞出屏幕后  
             */             
           view.moveBlock = ^(CommentMoveStatus status) {
               if (myself.bStopAnimation) {
                   return ;
               }
               switch (status) {
                   case Start:
                       //弹幕开始……将view加入弹幕管理queue
                       [self.bulletQueue addObject:weakBulletView];
                       break;
                   case Enter: {
                       //弹幕完全进入屏幕,判断接下来是否还有内容,如果有则在该弹道轨迹对列中创建弹幕……
                       NSString *comment = [myself nextComment];
                       if (comment) {
                           [myself createBulletComment:comment trajectory:trajectory];
                       } else {
                           //说明到了评论的结尾了
                       }
                       break;
                   }
                   case End: {
                       //弹幕飞出屏幕后从弹幕管理queue中删除
                       if ([myself.bulletQueue containsObject:weakBulletView]) {
                           [myself.bulletQueue removeObject:weakBulletView];
                       }
                       if (myself.bulletQueue.count == 0) {
                           //说明屏幕上已经没有弹幕评论了,循环开始
                           [myself start];
                       }
                       break;
                   }
                   default:
                      break;
               }
           };    
           //弹幕生成后,传到viewcontroller进行页面展示
           if (self.generateBulletBlock) {
                self.generateBulletBlock(view);
           }
      }
      - (NSString *)nextComment {
         NSString *comment = [self.tmpComments firstObject];
         if (comment) {
             [self.tmpComments removeObjectAtIndex:0];
         }
         return comment;
      }
    

    弹幕view的动画执行,部分代码(BulletView.m)如下:

    - (void)startAnimation {
        //根据定义的duration计算速度以及完全进入屏幕的时间
        CGFloat wholeWidth = CGRectGetWidth(self.frame) + mWidth + 50;
        CGFloat speed = wholeWidth/mDuration;
        CGFloat dur = (CGRectGetWidth(self.frame) + 50)/speed;
        __block CGRect frame = self.frame;
        if (self.moveBlock) {
            //弹幕开始进入屏幕
            self.moveBlock(Start);
        }
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(dur * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
           //避免重复,通过变量判断是否已经释放了资源,释放后,不在进行操作。
           //在stopAnimation中 self.bDealloc = YES;
           if (self.bDealloc) {
                  return;
           }
            //dur时间后弹幕完全进入屏幕
            if (self.moveBlock) {
                self.moveBlock(Enter);
            }
        });  
        //弹幕完全离开屏幕
        [UIView animateWithDuration:mDuration delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
            frame.origin.x = -CGRectGetWidth(frame);
            self.frame = frame;
        } completion:^(BOOL finished) {
            if (self.moveBlock) {
                self.moveBlock(End);
            }
            [self removeFromSuperview];
        }];
    }
    

    在viewcontroller中直接调用以下代码:

    self.bulletManager = [[BulletManager alloc] init];
    __weak ViewController *myself = self;
    self.bulletManager.generateBulletBlock = ^(BulletView *bulletView) {
        [myself addBulletView:bulletView];
    };
    
    - (void)addBulletView:(BulletView *)bulletView {
        bulletView.frame = CGRectMake(CGRectGetWidth(self.view.frame)+50, 200 + 34 * bulletView.trajectory, CGRectGetWidth(bulletView.bounds), CGRectGetHeight(bulletView.bounds));
        [self.view addSubview:bulletView];
        [bulletView startAnimation];
    }
    //点击开始按钮,弹幕开始飞入屏幕
    - (void)clickStart:(UIButton *)btn {
        [self.bulletManager start];
    }
    

    最终展示效果如下:


    效果展示

    查看完整代码,下载地址
    如何响应弹幕的点击事件请看 《续篇》

    相关文章

      网友评论

      • 大海999999:老哥好, 我看了你的弹幕demo, 竖屏没有问题,但是横盘时, 弹幕出来的位置有误, 方便和你私聊吗, 我的q 495213605, 红包酬谢:grin:
        b6a56844743a:楼主,想问下这个怎么根据通知动态添加数据,我自己发通知加数据的话弹幕会重复的
        43f8d00feb3b:这块实现并没有考虑竖屏情况,判断一下变更一下位置就好了,也不难实现
      • 一座城漫天飞着蒲公英:你这个还是会存在碰撞。根本的主要的原因是 是这段
        //根据定义的duration计算速度以及完全进入屏幕的时间
        CGFloat wholeWidth = CGRectGetWidth(self.frame) + mWidth + 50;
        CGFloat speed = wholeWidth/mDuration;
        CGFloat dur = (CGRectGetWidth(self.frame) + 50)/speed;
        正如同 当时间固定, 路程和速度就成正比,由于路程越长,时间固定,速度就会越快。这个时候你就会看到越短的弹幕走在前面,这个越长的弹幕走在后面会超过短的弹幕。解决的方案让速度固定,动画时间= 路程/ 时间。应该就会不会有重叠现象,但是会出现一种情况每个弹幕之间的间距一样。
      • 不必luo嗦:你好!我想请问下,加入弹幕过多,内存和cpu消耗太大,怎么复用这些弹幕
      • 王大吉Rock:为什么每一个评论都无法触发点击事件
      • 90后大叔:present后 CPU太高了 爆炸了要
      • 谢谢生活:速度在哪里 ? CGFloat wholeWidth = CGRectGetWidth(self.frame) + mWidth + 50;
        CGFloat speed = wholeWidth/mDuration;
        CGFloat dur = (CGRectGetWidth(self.frame) + 50)/speed; 这些设成常数没有用啊
      • 黎希:楼主 如何设置弹幕与弹幕之间的间距呢
        黎希:@不死鸟fj 恩 前天已经解决了 谢谢了
        43f8d00feb3b:弹幕的距离可以在设置弹幕view的frame中设置啊
        bulletView.frame = CGRectMake(CGRectGetWidth(self.view.frame)+50, 200 + 34 * bulletView.trajectory, CGRectGetWidth(bulletView.bounds), CGRectGetHeight(bulletView.bounds));
        或者可以写到配置信息中。。。。这样可能更好一点
      • Empty_One:very very 有用 :smile:
      • 8d2824356cbb:楼主 这个demo如何动态添加数据源呢?
      • 小小夕舞:我用你封装的 在弹幕上添加点击事件 添加不了 试了很多种都无法添加
        小小夕舞:我也试了touchbegan 这方法也不走 加手势 加view 都不行,不知道为啥 都不响应
        小小夕舞:我加了手势,覆盖过一层button,但是都不走点击事件
        43f8d00feb3b:@袁小虫lucky
        可以尝试以下两种方式,我自己的项目中用到的是第二种。
        1. 可以为每一个view添加一个tapgesture,处理好回调
        2. 可以创建一个bulletbackgroundview,把弹幕的view加在这个view上,方便弹幕随着这个view的移动而移动,比如唤起键盘,只要将这个背景view向上移动就可以了。然后在给这个背景view上加一个tapgesture事件处理点击事件
        - (void)tapBulletView:(UITapGestureRecognizer *)gesture {
        CGPoint clickPoint = [gesture locationInView:self];
        for (UIView *v in [self subviews]) {
        if ([v isKindOfClass:[BulletView class]]) {
        if ([v.layer.presentationLayer hitTest:clickPoint]) {
        //处理点击事件
        }
        }
        }
        }
        不知道是否对你有帮助。
      • 小小夕舞:找了n个弹幕了 但愿楼主能救我七级浮屠 :pray:
        43f8d00feb3b:@袁小虫lucky :scream::scream::scream:
      • 35f2768e990d:楼主,我怎么可以让弹幕,不管弹幕长短以同样速度跑出屏幕啊
        35f2768e990d:@不死鸟fj 弹幕多的话还是会重叠啊
        43f8d00feb3b:v=s/t 要想保证速度不变,只要根据弹幕的长度算出对应的时间就可以了。
      • d9557f883fd8:想了一下,貌似你这样的思路是不用碰撞检测的~~~棒棒棒
      • 667e649ccf10:该弹幕 不能点击哦。动画时候的弹幕
        667e649ccf10:@不死鸟fj OK 谢谢楼主。。啦啦啦
        43f8d00feb3b:此功能已更新博客,可以看看
        43f8d00feb3b:@catKingCraig 嗯,这个功能文章里没写
      • 64fa1c3a614d:楼主,这个弹幕模型怎么弄呀
        43f8d00feb3b:@爱吐槽的人 弹幕模型?能说得具体一点吗
      • mf168:研究一下
      • 嫁给猿吧: case Enter: ..... 这段代码处理的有些问题,可能会导致弹幕重叠,还是需要检测下轨道后,然后在进行下一个弹幕
        思路不错,赞一个
        43f8d00feb3b:@Leesim 参见@嫁给猿吧 回复
        43f8d00feb3b:@嫁给猿吧 case enter中应该不会导致重叠的,因为这块创建的时候使用的弹道trajectory就是前一个弹道。重叠有可能发生在startanimation中,做的延迟执行,有可能已经执行了stopanimation后,在view还没有及时销毁后执行了下边的方法,增加了bDealloc控制变量应该会解决重叠问题 。
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(dur * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        //避免重复,通过变量判断是否已经释放了资源,释放后,不在进行操作
        if (self.bDealloc) {
        return;
        }
        //dur时间后弹幕完全进入屏幕
        if (self.moveBlock) {
        self.moveBlock(Enter);
        }
        });

        - (void)stopanimation {
        self.bDealloc = YES;
        .....
        }
        30ef57faea58:@嫁给猿吧 这边也遇到了同样的问题 请问你是怎样在case Enter 中做了检测轨道的处理?
      • 扛支枪:只是我改进了下,重用了弹幕效果会好点
        不必luo嗦:我想问下 你是怎么复用的?
        扛支枪:@不死鸟fj 嗯,卡的原因我找到了,是我的问题,与你的代码无关, :smile:
        43f8d00feb3b:嗯,理论上view创建后直接销毁应该不至于卡,不过重用的确会更好一点:+1:
      • 扛支枪:楼主这个我最终还是用了,写的不错,多谢分享
      • 扛支枪:楼主,你这个弹幕有问题,数据比较多的时候很卡的
      • 扛支枪:楼主你好,问一下,为什么停止之后再开始就不回循环滚动了呢
      • mark666:抄袭可耻,引用人家的注明出处!!!http://www.olinone.com/?p=186
        Miss_DQ:就算是把别人的拿过来,学习消化,我觉得也没什么吧,只是楼主的一个学习笔记,有什么好批评的,楼主,赞你一个!
        mark666: @不死鸟fj呵呵
        43f8d00feb3b:@mark666 你说的这片博客从来没看过,哪来的抄袭啊,而且明显实现的方式不一样。
      • 左饵ear:也可以只换数据就是吧,重用的话bug也比较容易出现
      • __Jason__:🐎个
      • 83283fa43493:先赞下,慢慢看

      本文标题:iOS弹幕的原理分析与实现

      本文链接:https://www.haomeiwen.com/subject/sakbkttx.html