弹幕制作
一、需求分析:
1.首先计算在指定区域你需要几行弹幕
2.对使用过的label进行缓存
3.每行弹幕进入屏幕多少、这一行就可以进入下一条弹幕。
4.如果这一行弹幕满了就从第二行弹幕开始.以此类推。
5.如果最后所有行都满了则加快弹幕播放速度
7.对弹幕上的文字以及图片点击时手势的识别和添加
6.此外如果弹幕中有图片头像这些需要提前缓存下载、以及其它性能优化当然本案列不会写这么多。
二、设计如下:
总体采用面向对象的方式进行逻辑的划分、案列使用三行弹幕以队列的方式保存每条消息的进入与销毁、通过字典保存每一行的状态方便查找空闲行、每条弹幕内部使用路径layer动画实现layer移动、通过系统定时器CADisplayLink记录位置变化和改变状态.
1、重要类如下:
BarrageLabel 用来显示具体弹幕内容以及执行内部的动画等
BarrageLine 记录某一行是否处于空闲状态
BarrageVC 控制业务逻辑
如需扩展建议添加一些消息类以及消息管理类。
2、重要属性如下
//负责消息进入与取出
@property(nonatomic, strong) NSMutableArray <NSMutableAttributedString *>* textQueue;
//保存当前行状态
@property(nonatomic, strong) NSMutableDictionary <NSNumber *, BarrageLine*>*barrageLineDic;
//缓存所有已创建的的label
@property(nonatomic, strong) NSMutableArray <BarrageLabel *>* totalLabels;
//正在屏幕上显示的label
@property(nonatomic, strong) NSMutableArray<BarrageLabel *>* currentUsingLabels;
//目前处于空闲中未被使用的label
@property(nonatomic, strong) NSMutableArray<BarrageLabel *>* currentIdleLabels;
三、代码讲解
1、N条假数据消息,富文本实现图文混排。并开启启动定时器。由于弹幕目前是下一条紧跟上一条、如果某段时间没有消息但是突然推送过来了消息将无法启动下一条继续进入加了此定时器几秒检测一次,当然你也可以其它优化方案。
-(void)loadData
{
for (int i =0 ; i< 10; i++) {
NSMutableAttributedString *text = [NSMutableAttributedString new];
//添加图片
NSTextAttachment *attchment = [[NSTextAttachment alloc]init];
UIImage *image = [UIImage imageNamed:BBDefaultHeder];
attchment.image = image;
// 设置图片大小
attchment.bounds = CGRectMake(0, 0, 20, 20);
NSAttributedString *stringImage = [NSAttributedString attributedStringWithAttachment:attchment];
[text appendAttributedString:stringImage];
// [text insertAttributedString:attachment atIndex:2];
NSString *contentString = @"hello i come frome china.";
NSMutableAttributedString *stingAttri = [[NSMutableAttributedString alloc]initWithString:contentString];
//设置这一行向上移动5
[stingAttri addAttribute:NSBaselineOffsetAttributeName value:@(5) range:NSMakeRange(0, contentString.length)];
[text appendAttributedString:stingAttri];
//再添加一个图片
[text appendAttributedString:stringImage];
//加入队列
[self.textQueue addObject:text];
}
//开启弹幕添加定时器
[self.barrageTimer setFireDate:[NSDate distantPast]];
}
2、对屏幕上的消息存入进入缓存池、目前一般长度的消息亲测8个label以内。消息越短需要缓存的将会多些.
//添加一条弹幕
-(void)addABarrageUI
{
if (self.textQueue.count == 0) {
//没有足够的文字了--不需要显示
NSLog(@"Barrage- not enough text.!");
//停掉定时器
[self.barrageTimer setFireDate:[NSDate distantPast]];
return;
}
int idleLine = [self getIdelLine];
if (idleLine == -1) {
//没有空闲的行 ----进行等待
NSLog(@"Barrage- no enough idle line.!");
return;
}
NSLog(@"Barrage- will add a barrage.");
NSAttributedString *text = [self.textQueue firstObject];
BarrageLabel *label;
//计算文字的宽
CGSize size0 = [self calculationTextSize:text.string cgSize:CGSizeMake(CGFLOAT_MAX, 30) font:16.0];//206.28
CGFloat width =size0.width+20*2;//20是图片的宽
if (self.currentIdleLabels.count >0) {
//有空闲缓存的label直接拿来使用
label = [self.currentIdleLabels firstObject];
NSLog(@"Barrage- get a cache idle label. count=%lu",self.currentIdleLabels.count);
}else{
//没有空闲缓存的label-重新创建
label = [[BarrageLabel alloc]initWithFrame:CGRectMake(-width, 0, width, 30)];
[self.view addSubview:label];
[self.totalLabels addObject:label];
}
label.frame = CGRectMake(-width, 0, width, 30);//更新尺寸宽
label.attributedText = text;
// label.backgroundColor = [UIColor greenColor];
label.delegete = self;
[self.textQueue removeObjectAtIndex:0];
//开始动画
[label startAnimationAtLine:idleLine];
}
3.代理实现label状态回调记录空闲行状态变化、以及记录可用label变化
-(void)visibleDidChange:(BarrageLabel *)label
{
// NSLog(@"visibleDidChange");
if (label.currentVisibleType == BarrageLabelVisibleTypeTailInScreen) {
//可以添加标记多了一个空闲位置
BarrageLine *line = [self.barrageLineDic objectForKey:@(label.currentLine)];
line.currentStatus = BarrageLineStatusIdle;
if (self.isRuning) {
//添加一个新的弹幕进来了
[self addABarrageUI];
}
}
}
-(void)stauesDidChange:(BarrageLabel *)label
{
// NSLog(@"stauesDidChange");
if (label.currentStatus != BarrageLabelStatusUsing) {
//此label处于空闲状态
if ([self.currentUsingLabels containsObject:label]) {
[self.currentUsingLabels removeObject:label];
}
if (self.currentIdleLabels.count > 20) {
//开始清除多余的缓存label
}
}else{
//此label正在被使用
if (![self.currentUsingLabels containsObject:label]) {
[self.currentUsingLabels addObject:label];
NSLog(@"Barrage- current using label count is =%lu",self.currentUsingLabels.count);
}
}
}
4.移动动画实现
UIBezierPath *path = [self creatPathPoints:array];
//创建动画
CAAnimation *animation = [self creatAnimationPath:path];
//开始动画
[self.layer addAnimation:animation forKey:@"LLAnimationPosition"];
-(UIBezierPath *)creatPathPoints:(NSArray <NSValue *>*)pointValues
{
UIBezierPath* path = [UIBezierPath bezierPath];
path.lineWidth = 1.0;
path.lineCapStyle = kCGLineCapRound; //线条拐角
path.lineJoinStyle = kCGLineJoinRound; //终点处理
for (int i = 0; i<pointValues.count; i++) {
if (i==0) {
//起点
[path moveToPoint:pointValues[i].CGPointValue];
}else{
//连线
[path addLineToPoint:pointValues[i].CGPointValue];
}
}
return path;
}
-(CAKeyframeAnimation *)creatAnimationPath:(UIBezierPath *)path
{
//添加动画
CAKeyframeAnimation * animation;
animation=[CAKeyframeAnimation animationWithKeyPath:@"position"];
animation.path = path.CGPath;
animation.duration = 8.0;
animation.repeatCount=0;
// 结束保持最后状态
// animation.fillMode = kCAFillModeForwards;
//线性
[animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]];
[animation setDelegate:self];
//动画执行完不移除和fillmode都要设置
// [animation setRemovedOnCompletion:NO];
return animation;
}
源码
有疑问的小伙伴欢迎加交流讨论QQ:206931384