新弹幕系统 chenqihui/QHDanmu2Demo,它在 iOS 飞屏的实现之路 | RyuukuSpace 里面的也提到过,与飞屏在展示与运动逻辑的实现上基本相似,区别可能在于弹幕系统会更加注重弹幕在轨道的选择,因此本次也着重介绍下弹幕系统的轨道选择部分。
轨道选择
弹幕直白点说就是在视频上显示文本,而如何显示文本,以至于它们之间不会相互遮拦。这就需要根据情况判断弹幕的出现轨道。下面介绍已飘屏的弹幕实现(这也是大部分弹幕的效果,也有时渐隐类的弹幕等等)
碰撞
碰撞是弹幕需要避免的计算,特别是在大量弹幕显示的时候,如何控制在不碰撞或者更低的碰撞率下显示更多的弹幕。
错误 ❌ |
正确 ⭕️ |
深度优先 & 广度优先
- 深度优先:从上往下,优先选择可满足运动的轨道
- 广度优先:从上往下,优先选择空闲轨道(即无弹幕),没有才选择可满足运动的轨道
- 满足可运动:新弹幕加入后,在最后弹幕运动完成过程中不存在碰撞,即新弹幕不会与最后弹幕重叠。
深度优先 |
广度优先 |
深度优先的逻辑大致为:
运动的总时间 t,屏幕宽度 ws,而最后弹幕 d1 长度 w1 & 速度 v1((w1+ws)/t),而新弹幕 d2 长度 w2 & 速度 v2((w2+ws)/t)。它们分别加入的时间为s1、s2,而时间差 bt(s2-s1)。运动都是匀速运动。
- 1、当 d1 在 dt 时间内是否运行了 w1 的距离,否的话则该轨道不可使用(w1 > dt*v1),是则往下继续判断
- 2、当 d1 在 dt 时间内已运行了 >=w1 的距离,此时看 d2 是否能在该轨道碰撞 d1
- 2.1、d2 速度小于等于 d1(v2 <= v1),则不会碰撞,该轨道可使用
- 2.2、否的话,看 d2 在 ws 运行的时间是否大于 d1 运动完需要的剩余时间(ws/v2 >= (t-bt)),是的话则不会碰撞,该轨道可使用,否则不可使用
- 3、其余其他情况都是会碰撞
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| - (QHDanmuCellParam)p_danmuParamWithPlayUseTime:(CGFloat)playUseTime { NSDictionary *data = _danmuDataList.firstObject; QHDanmuCellParam newParam; newParam.pathwayNumber = -1; if (_searchPathwayMode == QHDanmuViewSearchPathwayModeBreadthFirst) { for (int i = 0; i < _pathwayCount; i++) { NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0]; id obj = [_cachedCellsParam objectForKey:indexPath]; if ([obj isKindOfClass:[NSNull class]] == YES) { break; } } if (newParam.pathwayNumber >= 0) { return newParam; } } for (int i = 0; i < _pathwayCount; i++) { NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0]; id obj = [_cachedCellsParam objectForKey:indexPath]; if ([obj isKindOfClass:[NSNull class]] == YES) { } else if ([obj isKindOfClass:[NSValue class]] == YES) { CFTimeInterval spaceTime = newParam.startTime - lastParam.startTime; if (spaceTime * lastParam.speed >= lastParam.width) { if (lastParam.speed >= newParam.speed) { newParam.pathwayNumber = lastParam.pathwayNumber; } else { CGFloat useTimeInScreen = self.frame.size.width / newParam.speed; if (useTimeInScreen >= (playUseTime - spaceTime)) { newParam.pathwayNumber = lastParam.pathwayNumber; } } } else { } } if (newParam.pathwayNumber >= 0) { break; } } return newParam; }
|
QHDanmuView & QHDanmuViewCell
参考 UITableView & UITableViewCell 的接口、逻辑的设计
- View 的创建 & 添加
- Cell 的注册 & 获取
- Cells 的复用
- DataSource & Delegate 实现轨道数量 & 轨道高度等的配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @property (nonatomic, strong) NSMutableArray *reusableCells;
@property (nonatomic, strong) NSMutableDictionary *reusableCellsIdentifierDic;
@property (nonatomic, strong) NSMutableDictionary *cachedCellsParam;
- (void)registerClass:(nullable Class)cellClass forCellReuseIdentifier:(nonnull NSString *)identifier;
- (nullable __kindof QHDanmuViewCell *)dequeueReusableCellWithIdentifier:(nonnull NSString *)identifier;
- (void)p_danmuAnimationOfFlyWithCell:(QHDanmuViewCell *)cell param:(QHDanmuCellParam)param playUseTime:(CFTimeInterval)playUseTime;
|
参考
基础操作
1 2 3 4 5 6 7 8 9 10 11
| // 弹幕加入 - (void)insertData:(NSArray<NSDictionary *> *)data; - (void)insertDataInFirst:(NSDictionary *)data; // 清空 - (void)cleanData;
// 弹幕的开关、恢复 & 暂停 - (void)start; - (void)stop; - (void)resume; - (void)pause;
|
Manager
前面是直播使用的弹幕(即加入后显示,实时显示)。但对于点播的弹幕,其会提前加载一段时间点的弹幕,然后再根据弹幕的时间戳进行显示。因此 Manager 就是用来管理这些弹幕,根据与点播视频的时间进度匹配相应的弹幕,然后加入弹幕系统的总控制。
- 新弹幕进行排序
- 加入到总弹幕池进行合并排序
- 通过播放的时间点来匹配符合(即小于等于当前时间点)的弹幕
- 批量加入弹幕系统
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| - (void)setMediaPlayAbsoluteTime:(CFAbsoluteTime)mediaPlayAbsoluteTime { _mediaPlayAbsoluteTime = mediaPlayAbsoluteTime; int index = -1; if (_danmuDataList.count > 0) { for (int i = 0; i < _danmuDataList.count; i++) { NSDictionary *danmuData = _danmuDataList[i]; CFTimeInterval startTime = 0; // 获取时间戳 startTime = _startTimeBlock(danmuData); // 与当前时间点比较 if (startTime <= mediaPlayAbsoluteTime) { index = i; } else { break; } } if (index >= 0) { NSArray *inData = [_danmuDataList subarrayWithRange:NSMakeRange(0, index + 1)]; [_danmuView insertData:inData]; [_danmuDataList removeObjectsInArray:inData]; } } }
|
其他
以上是弹幕系统在单一弹幕展示上基础的功能实现。项目的 dev-0.3 分支正在增加弹幕系统支持更多运动模式(如渐隐渐显)。思路是通过不同 section 来区分,类似 UITableView 的 section,不过还得思考如果有指定运动区域(如上中下)的通用性。