• iOS 视频直播弹幕的实现


      弹幕,并不是一个多么复杂的功能。

    1.弹幕的实现性分析

      首先,从视觉上明确当前弹幕所具有的功能

    • 从屏幕右侧滑入左侧,直至完全消失
    • 不管是长的弹幕,还是短的弹幕,速度一致(可能有的需求是依据弹幕长度,调整速度)
    • 有弹幕轨道,不是随机产生的弹幕
    • 弹幕不会进行重叠

      接下来从功能角度思考需要做什么

    • 重用机制,类似tableView有一个重用池,每个弹幕就是一个cell,当有弹幕发送的时候,如果当前的重用池没有控件,则创建一个新的控件,如果重用池里面有控件,则拿出这个控件,开始做动画,在动画结束后重新将该控件重归重用池。
    • 速度要求一致的话,需要考虑几点,首先如下图所示,红色代表弹幕起始位置蓝色代表弹幕终止位置,长度代表它们的实际长度。当我们设定动画的时候,采用[UIView animationWithDuration.....]这个动画,设定duration为3s的话那么弹幕1的速度为(屏幕宽度+弹幕1宽度)/3,弹幕2的速度为(屏幕宽度+弹幕2宽度)/3,因为弹幕2长度大于弹幕1的长度,所以弹幕2的速度大于弹幕1的速度。(对于依据弹幕长度调整速度的需求来说,这里相对简单一些,不需要专门去计算速度,唯一麻烦的是需要考虑速度不一致带来的重叠问题)

    2.开始准备

      精通数学公式V=S/t (V代表速度,S代表路程,t代表时间)(*^__^*) 

    3.正式开始

      创建一个View,命名为BarrageView,以及存储弹幕数据的对象BarrageModel

      以下为BarrageModel.h的内容,存储弹幕的头像,昵称,和消息内容

    @interface BarrageModel : NSObject
    /** 用户昵称 */
    @property(nonatomic,copy)NSString *userName;
    /** 消息内容 */
    @property(nonatomic,copy)NSString *userMsg;
    /** 用户头像 */
    @property(nonatomic,copy)NSString *userHeadImageUrl;
    @end

       接下来对BarrageView内容进行编辑,注释已经尽可能的详细,因此不多做介绍

       在.h文件中

    #import <UIKit/UIKit.h>
    @class BarrageModel;
    @interface BarrageView : UIView
    
    /**
     *  记录当前最后一个弹幕View,通过这个View来计算是显示在哪个弹幕轨道上
     */
    @property(nonatomic,retain) UIView *lastAnimateView;
    /**
     * 发送弹幕
     *
     *  @param msgModel 弹幕数据Model
     */
    -(void)barrageSendMsg:(BarrageModel *)msgModel;
    @end

      在.m文件中

    #import "BarrageView.h"
    #import "BarrageModel.h"
    
    //屏幕的尺寸
    #define SCREEN_FRAME   [[UIScreen mainScreen] bounds]
    //屏幕的高度
    #define SCREEN_HEIGHT  CGRectGetHeight(SCREEN_FRAME)
    //屏幕的宽度
    #define SCREEN_WIDTH  CGRectGetWidth(SCREEN_FRAME)
    
    @interface BarrageView()
    {
        CGFloat _minSpaceTime;  /** 最小间距时间 */
    }
    /** 数据源 */
    @property (nonatomic,retain)NSMutableArray *dataArr;
    
    /** 弹幕UI的重用池 */
    @property (nonatomic,retain)NSMutableArray *resuingArr;
    @end
    
    @implementation BarrageView
    
    - (instancetype)initWithFrame:(CGRect)frame
    {
        self = [super initWithFrame:frame];
        if (self) {
            [self setInterface];
        }
        return self;
    }
    -(void)setInterface
    {
        //初始化弹幕数据源,以及重用池
        self.dataArr = [NSMutableArray array];
        self.resuingArr = [NSMutableArray array];
        
        //创建第一个弹幕加入重用池作为备用
        UIView *view =  [self createUI];
        [self.resuingArr addObject:view];
        
        //设置弹幕数据的初始轮询时间
        _minSpaceTime = 1;
        
        //检查是否可以取弹幕数据进行动画
        [self checkStartAnimatiom];
    }
    -(void)checkStartAnimatiom
    {
        //当有数据信息的时候
        if (self.dataArr.count>0) {
    
            if (self.resuingArr.count>0) { //当重用池里面有备用的弹幕UI时
                
                //在重用池中,取出第一个弹幕UI
                UIView *view = [self.resuingArr firstObject];
                [self.resuingArr removeObject:view];
                //取出的这个弹幕UI开始动画
                [self startAnimationWithView:view];
                
            }else{ //当重用池没有备用的弹幕UI时
                
                //重新创建一个弹幕UI
                UIView *view = [self createUI];
                //拿着这个弹幕UI开始动画
                [self startAnimationWithView:view];
            }
        }
        //延迟执行,在主线程中不能调用sleep()进行延迟执行
        //调用自身方法,构成一个无限循环,不停的轮询检查是否有弹幕数据
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_minSpaceTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [self checkStartAnimatiom];
        });
        //于2017年12月29日更新,以上代码会导致内存泄漏,修改如下
        __weak typeof(self) weakSelf = self;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_minSpaceTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            __strong typeof(self) strongSelf = weakSelf;
            [strongSelf checkStartAnimatiom];
        });
       }
    -(void)startAnimationWithView:(UIView *)view { //取出第一条数据 BarrageModel *barrageModel = [self.dataArr firstObject]; //计算昵称的长度 CGSize nameSize = [barrageModel.userName boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, 14) options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading attributes:@{ NSFontAttributeName:[UIFont systemFontOfSize:14] } context:nil].size; //计算消息的长度 CGSize msgSize = [barrageModel.userMsg boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, 14) options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading attributes:@{ NSFontAttributeName:[UIFont systemFontOfSize:14] } context:nil].size; UIImageView *headImageView; //头像 UILabel *userNameLabel; //昵称 UILabel *userMsgLabel; //消息内容 //进行赋值,宽度适应 for (UIView *subView in view.subviews) { if (subView.tag == 1000) { headImageView = (UIImageView *)subView; headImageView.image = [UIImage imageNamed:@""]; }else if (subView.tag == 1001){ userNameLabel = (UILabel *)subView; userNameLabel.text = barrageModel.userName; //重新设置名称Label宽度 CGRect nameRect = userNameLabel.frame; nameRect.size.width = nameSize.width; userNameLabel.frame = nameRect; }else{ userMsgLabel = (UILabel *)subView; userMsgLabel.text = barrageModel.userMsg; //重新设置消息内容Label宽度 CGRect msgRect = userMsgLabel.frame; msgRect.size.width = msgSize.width; userMsgLabel.frame = msgRect; } } //重新设置弹幕的总体宽度 = 头像宽度 + 头像左右两侧距离 + (如果名字宽度大于消息内容宽度,以名字宽度为基准,如果名字宽度小于消息内容宽度,以消息内容宽度为基准) view.frame = CGRectMake(SCREEN_WIDTH, 0, CGRectGetWidth(headImageView.frame) + 4 + (CGRectGetWidth(userNameLabel.frame)>CGRectGetWidth(userMsgLabel.frame)?CGRectGetWidth(userNameLabel.frame):CGRectGetWidth(userMsgLabel.frame)), CGRectGetHeight(self.frame)); //不管弹幕长短,速度要求一致。 V(速度) 为固定值 = 100(可根据实际自己调整) // S = 屏幕宽度+弹幕的宽度 V = 100(可根据实际自己调整) // V(速度) = S(路程)/t(时间) -------> t(时间) = S(路程)/V(速度); CGFloat duration = (view.frame.size.width+SCREEN_WIDTH)/100; //最小间距运行时间为:弹幕从屏幕外完全移入屏幕内的时间 + 间距的时间 _minSpaceTime = (view.frame.size.width + 30)/100; //最后做动画的view _lastAnimateView = view; //弹幕UI开始动画 [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveLinear animations:^{ //运行至左侧屏幕外 CGRect frame = view.frame; view.frame = CGRectMake(-frame.size.width, 0, frame.size.width, frame.size.height); } completion:^(BOOL finished) { //动画结束重新回到右侧初始位置 view.frame = CGRectMake(SCREEN_WIDTH, 0, 0, CGRectGetHeight(self.frame)); //重新加入重用池 [self.resuingArr addObject:view]; }]; //将这个弹幕数据移除 [self.dataArr removeObject:barrageModel]; } #pragma mark public method -(void)barrageSendMsg:(BarrageModel *)msgModel{ //添加弹幕数据 [self.dataArr addObject:msgModel]; } #pragma mark 创建控件 -(UIView *)createUI { UIView *view = [[UIView alloc] initWithFrame:CGRectMake(SCREEN_WIDTH, 0, 0, CGRectGetHeight(self.frame))]; view.backgroundColor = [UIColor colorWithWhite:0 alpha:0.3]; UIImageView *headImageView = [[UIImageView alloc] initWithFrame:CGRectMake(2, 2, CGRectGetHeight(self.frame)-4, CGRectGetHeight(self.frame)-4)]; headImageView.layer.cornerRadius = headImageView.frame.size.width/2; headImageView.layer.masksToBounds = YES; headImageView.tag = 1000; headImageView.backgroundColor = [UIColor redColor]; [view addSubview:headImageView]; UILabel *userNameLabel = [[UILabel alloc] initWithFrame:CGRectMake(CGRectGetMaxX(headImageView.frame) + 2, 0, 0,14)]; userNameLabel.font = [UIFont systemFontOfSize:14]; userNameLabel.tag = 1001; [view addSubview:userNameLabel]; UILabel *userMsgLabel = [[UILabel alloc] initWithFrame:CGRectMake(CGRectGetMaxX(headImageView.frame)+2, CGRectGetMaxY(userNameLabel.frame), 0, 14)]; userMsgLabel.font = [UIFont systemFontOfSize:14]; userMsgLabel.tag = 1002; [view addSubview:userMsgLabel]; [self addSubview:view]; return view; }

      最后在vc里面

    #import "ViewController.h"
    #import "BarrageView.h"
    #import "BarrageModel.h"
    
    //屏幕的尺寸
    #define SCREEN_FRAME   [[UIScreen mainScreen] bounds]
    //屏幕的高度
    #define SCREEN_HEIGHT  CGRectGetHeight(SCREEN_FRAME)
    //屏幕的宽度
    #define SCREEN_WIDTH  CGRectGetWidth(SCREEN_FRAME)
    
    @interface ViewController ()
    /** 第一个弹幕轨道 */
    @property (nonatomic,retain)BarrageView *barrageViewOne;
    /** 第二个弹幕轨道 */
    @property (nonatomic,retain)BarrageView *barrageViewTwo;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        //创建第一个弹幕轨道
        _barrageViewOne = [[BarrageView alloc]initWithFrame:CGRectMake(0,200, SCREEN_WIDTH, 34)];
        [self.view addSubview:_barrageViewOne];
        //创建第二个弹幕轨道
        _barrageViewTwo = [[BarrageView alloc]initWithFrame:CGRectMake(0,300, SCREEN_WIDTH, 34)];
        [self.view addSubview:_barrageViewTwo];
    
    }
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(sendMessage) userInfo:nil repeats:YES];
        [timer fire];
    }
    -(void)sendMessage
    {
        BarrageModel *model = [[BarrageModel alloc]init];
        model.userName = @[@"张三",@"李四",@"王五",@"赵六",@"七七",@"八八",@"九九",@"十十",@"十一",@"十二",@"十三",@"十四"][arc4random()%12];
        model.userMsg = @[@"阿达个人",@"都是vsqe12qwe",@"胜多负少的凡人歌",@"委屈翁二群二",@"12312",@"热帖柔荑花",@"发彼此彼此",@"OK泼墨",@"人体有图图",@"额外热无若无",@"微软将围"][arc4random()%11];
        //计算当前做动画的弹幕UI的位置
        CGFloat onePositon = _barrageViewOne.lastAnimateView.layer.presentationLayer.frame.size.width + _barrageViewOne.lastAnimateView.layer.presentationLayer.frame.origin.x;
        //计算当前做动画的弹幕UI的位置
        CGFloat twoPositon = _barrageViewTwo.lastAnimateView.layer.presentationLayer.frame.size.width + _barrageViewTwo.lastAnimateView.layer.presentationLayer.frame.origin.x;
        if ( onePositon < twoPositon ) {
            [_barrageViewOne barrageSendMsg:model];
        }else{
            [_barrageViewTwo barrageSendMsg:model];
        }
    }
    @end

    4.测试结论

      经一个小时的定时器测试,内存没有增加。

  • 相关阅读:
    字符串转数字的hash函数-布隆过滤器
    javascript实现字符查询之kmp算法
    毫秒查询9位数qq号码是否存在-BitMap算法应用
    bitMap算法将字符串映射成数字,同时可以将数字映射成字符串-javascript
    js数字格式化为千分位
    浅谈BST(二叉查找树)
    CSP2019 游记
    2019.10.20模拟赛总结
    P2827 蚯蚓
    原生js解决简单轮播图的切换
  • 原文地址:https://www.cnblogs.com/ChengYing-Freedom/p/8025210.html
Copyright © 2020-2023  润新知