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.測試結論

  經一個小時的定時器測試,內存沒有增加。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM