写在开篇
最近做了个视频直播项目,当用到弹幕时,找了很多网上弹幕demo。当时因为项目进度的原因,就随便选了一个漂亮的集成了,也没有去研究其中具体是如何实现的。如今项目完成,就利用空余的时间来研究了下。
在我寻找一个合适的demo集成的时候,就发现网上提供的demo都是将一个lable作为一条弹幕,然后控制lable做动画,因为项目UI会对弹幕做明确的样式规定,所以单纯的一个lable根本就满足不了项目的需求。为什么不能将一个自定义的cell作为一条弹幕,控制cell做动画,作为使用者,则只用关心cell的样式和在cell上展示的数据呢?于是,我就开始了这篇文章。
实现流程
在实现一个功能之前我们需要知道这个功能是做什么的,然后根据这个功能构建一个大概的思路流程,最后将思路流程细化、修改、直至功能实现。
弹幕的功能我就不必多说,而我实现这个思路流程与具体实现如下
思路流程:
1.收到弹幕消息数组,立即缓存
2.定时从缓存中取出n条消息模型用来展示
3.遍历取出的消息,为消息找一个合适的cell用来展示
4.遍历每一条轨道(弹幕运动的路径),为弹幕找一条合适的轨道
5.开始cell的动画,并清除缓存
具体实现:
1、消息缓存
当收到弹幕消息时就调用BarrageView对象的insertBarrages:immediatelyShow:方法将消息添加到缓存中,具体实现如下:
[BarrageView checkElementOfBarrages:barrages];
self.count = barrages.count;
if (barrages.count) {
[self assortDataArray:barrages];
}
if (flag == YES && isStart == NO) {
isStart = YES;
[self startAnimation];
}
在添加消息模型到缓存之前需要先对消息模型进行检测checkElementOfBarrages:因为在计算弹幕动画时间时必须要知道弹幕的宽度,所以消息模型必须要遵守BarrageModelAble协议,即告诉BarrageVIew弹幕的宽度
2.取出模型
从缓存中取模型subArrayWithNumber:方法具体实现如下:
NSMutableArray *array = [NSMutableArray array];
if (self.highPrioritys.count) {
[array addObjectsFromArray:self.highPrioritys];
}
if (self.mediumPrioritys.count) {
[array addObjectsFromArray:self.mediumPrioritys];
}
if (self.lowPrioritys.count) {
[array addObjectsFromArray:self.lowPrioritys];
}
if (array.count >= number) {
NSArray *subArray = [array subarrayWithRange:NSMakeRange(0, number)];
[self removeModels:subArray];
return subArray;
}else {
[self removeModels:array];
return array;
}
因为BarrageModelAble协议中要求对消息模型进行优先级(PriorityLevel)区分,我将三个不同优先级的数组按优先级顺序拼接成一个数组,然后从大数组中取出n个元素,并将这n个元素从缓存中删除
3.谁来展示
BarrageViewCell *currentCell = [self.dataSouce barrageView:self cellForRowAtIndex:[self.dataArray indexOfObject:obj]];
寻找一个合适的cell来展示,根据标识符从cell的缓存池中取,如果取不到就创建一个cell。
4.在哪展示
遍历所有的轨道,有的轨道上没有弹幕--空闲轨道,有的轨道上有弹幕--非空闲轨道。所以需要对可用的轨道进行一个分类,具体实现如下:
for (int row = 0; row < numbers; row++) {
if (showCells.count == 0) {
[freeOrbits addObject:[NSNumber numberWithInt:row]];
}else {
BOOL flag = NO;//标记row轨道上是否有cell正在执行动画
for (int index = (int)showCells.count - 1; index >= 0; index--) {
BarrageViewCell *cell = showCells[index];
if (cell.row == row) {//找到row轨道上正在滚动的最后一条弹幕
flag = YES;
if ([self examineColide:cell] == NO) {
[orbits addObject:[NSNumber numberWithInt:row]];
}
break;
}
}
if (flag == NO) {
[freeOrbits addObject:[NSNumber numberWithInt:row]];
}
}
}
空闲轨道一定可用,但非空闲轨道还需要还需要进行碰撞检测examineColide:才能确定其是否可用,碰撞检测具体实现如下:
- (BOOL)examineColide:(BarrageViewCell *)cell{
NSTimeInterval t1 = self.duration - self.cellWidth / self.speed;
NSDate *nowDate = [NSDate date];
if (_stopDate) {
nowDate = _stopDate;
}
NSDate *date1 = [nowDate dateByAddingTimeInterval:t1];
NSDate *date2 = [cell.startTime dateByAddingTimeInterval:cell.duration];
if ([date1 compare:date2] == NSOrderedDescending) {
return NO;
}else {
return YES;
}
}
碰撞检测主要是根据当前轨道上最后一个正在滚动的弹幕的结束时间与当前需要开始滚动的弹幕从现在开始滚动的结束时间进行一个对比。
选出可用轨道之后,就从可用轨道中随机出一个轨道,用于弹幕展示,当然先从空闲轨道中随。具体实现如下:
if (orbits.count == 0 && freeOrbits.count == 0) {
_row = -1;
}else {
if (freeOrbits.count > 0) {
int index = arc4random_uniform((u_int32_t)freeOrbits.count);
_row = freeOrbits[index].integerValue;
}else {
int index = arc4random_uniform((u_int32_t)orbits.count);
_row = orbits[index].integerValue;
}
}
5.如何展示
至于弹幕的动画就非常简单了,即控制cell让其从屏幕右边滚动到屏幕左边,具体实现如下:
CABasicAnimation *move = [CABasicAnimation animation];
move.keyPath = @"position";
CGRect frame = self.frame;
CGPoint fromPoint = CGPointMake(frame.origin.x, frame.origin.y);
CGPoint toPoint = CGPointMake(- CGRectGetWidth(frame), frame.origin.y);
move.fromValue = [NSValue valueWithCGPoint:fromPoint];
move.toValue = [NSValue valueWithCGPoint:toPoint];
move.duration = duration;
move.delegate = self;
move.removedOnCompletion = YES;
[self.layer addAnimation:move forKey:nil];
动画是在异步线程中执行,动画添加到图层中之后就应该重复到第二步,从缓存中取新的消息模型用来展示,直到没有缓存数据为止。
至此,就大功告成了,下面是实现的效果图
功能如何使用可以参照demo,最后附上Demo地址。
写在结尾
因为个人能力有限,demo中难免有些BUG,如果遇到请在留言中指出,方便我好及时修改。或者你有什么更好的意见或建议,也可以提出来,我们相互交流、相互学习。又或者你将这个功能集成在项目中时,因为项目原因需要增加其他的功能,你也可以提出,我也会依据个人能力添加新的功能,或是给你一些建议。总之你可以在留言区发表你的想法,最后希望这篇文章能真正的帮助到大家。