写在前边
为便于大家学习,在视频网站上录了相应的视频
想看视频的朋友点这里哟~~
何为弹幕?~~
随着90后的不断崛起,弹幕越来越受到年轻人的喜爱。所谓弹幕,我的理解就是评论的另一种表现形式,更能吸引用户眼球,增强用户体验,增加用户参与感和使用粘度。现在国内比较火的弹幕类视频网站A站和B站,深受年轻人群的追捧。下边就是B站的一个展示效果。
另外一些新闻资讯类的app也开始实现弹幕功能,为博得用户喜爱,比如唔哩、微在、橘子娱乐等等。下面我们就来一起分析一下,弹幕在iOS端是如何实现的呢?
原理分析与实现~~
首先我们来分析一下弹幕的特点。
- 一般情况下弹幕都是从屏幕右侧进入并从屏幕左侧飞出。
- 弹幕进入屏幕后按照一定轨迹来移动。
- 弹幕移动速度根据内容长度决定,内容越长,移动速度越快。
- 一个弹幕完全进入屏幕后,后边会继续飞入一个新的弹幕。
- 弹幕是循环滚动播放的。
基于以上特点,我们设计出来的弹幕形式大致如下图所示,默认只有三个弹道(弹幕飞行轨迹)来展示弹幕飞行效果。
-
初始化三个弹幕1、2、3准备进入屏幕,DataSource为弹幕资源的数据来源地。
1.初始化弹幕 -
当弹幕陆陆续续进入屏幕,飞行速度与弹幕长度相关,每当其中一个弹道的弹幕完全进入屏幕后,则从数据池中取出一个弹幕在相应弹道进入屏幕,如弹幕4。
2. 弹幕进入屏幕 -
如果某条弹幕已经完全飞出屏幕,则将此弹幕从屏幕中删除,如弹幕1和弹幕3。
3. 弹幕飞出屏幕 - 当弹幕全部飞出屏幕,回到步骤1,重新滚动播放
技术实现~~
下面从技术层面来讨论一下实现细节,以下是部分核心代码,完整代码参见这里。
首先来看一下弹幕的生成过程,初始化三个弹幕轨迹,如果不足三个,创建2个或者1个轨迹,代码(BulletManager.m)如下:
- (void)start {
if (self.tmpComments.count == 0) {
[self.tmpComments addObjectsFromArray:self.allComments];
}
self.bStarted = YES;
self.bStopAnimation = NO;
[self initBulletCommentView];
}
/**
* 初始化弹幕
*/
- (void)initBulletCommentView {
//初始化三条弹幕轨迹
NSMutableArray *arr = [NSMutableArray arrayWithArray:@[@(0), @(1), @(2)]];
for (int i = 3; i > 0; i--) {
NSString *comment = [self.tmpComments firstObject];
if (comment) {
[self.tmpComments removeObjectAtIndex:0];
//随机生成弹道创建弹幕进行展示(弹幕的随机飞入效果)
NSInteger index = arc4random()%arr.count;
Trajectory trajectory = [[arr objectAtIndex:index] intValue];
[arr removeObjectAtIndex:index];
[self createBulletComment:comment trajectory:trajectory];
} else {
break;
}
}
}
创建弹幕view,对弹幕view的各种位置状态进行监听并做出相对应的处理。
/**
* 创建弹幕
*
* @param comment 弹幕内容
* @param trajectory 弹道位置
*/
- (void)createBulletComment:(NSString *)comment trajectory:(Trajectory)trajectory {
if (self.bStopAnimation) {
return;
}
//创建一个弹幕view
BulletView *view = [[BulletView alloc] initWithContent:comment];
//设置运行轨迹
view.trajectory = trajectory;
__weak BulletView *weakBulletView = view;
__weak BulletManager *myself = self;
/**
* 弹幕view的动画过程中的回调状态
* Start:创建弹幕在进入屏幕之前
* Enter:弹幕完全进入屏幕
* End:弹幕飞出屏幕后
*/
view.moveBlock = ^(CommentMoveStatus status) {
if (myself.bStopAnimation) {
return ;
}
switch (status) {
case Start:
//弹幕开始……将view加入弹幕管理queue
[self.bulletQueue addObject:weakBulletView];
break;
case Enter: {
//弹幕完全进入屏幕,判断接下来是否还有内容,如果有则在该弹道轨迹对列中创建弹幕……
NSString *comment = [myself nextComment];
if (comment) {
[myself createBulletComment:comment trajectory:trajectory];
} else {
//说明到了评论的结尾了
}
break;
}
case End: {
//弹幕飞出屏幕后从弹幕管理queue中删除
if ([myself.bulletQueue containsObject:weakBulletView]) {
[myself.bulletQueue removeObject:weakBulletView];
}
if (myself.bulletQueue.count == 0) {
//说明屏幕上已经没有弹幕评论了,循环开始
[myself start];
}
break;
}
default:
break;
}
};
//弹幕生成后,传到viewcontroller进行页面展示
if (self.generateBulletBlock) {
self.generateBulletBlock(view);
}
}
- (NSString *)nextComment {
NSString *comment = [self.tmpComments firstObject];
if (comment) {
[self.tmpComments removeObjectAtIndex:0];
}
return comment;
}
弹幕view的动画执行,部分代码(BulletView.m)如下:
- (void)startAnimation {
//根据定义的duration计算速度以及完全进入屏幕的时间
CGFloat wholeWidth = CGRectGetWidth(self.frame) + mWidth + 50;
CGFloat speed = wholeWidth/mDuration;
CGFloat dur = (CGRectGetWidth(self.frame) + 50)/speed;
__block CGRect frame = self.frame;
if (self.moveBlock) {
//弹幕开始进入屏幕
self.moveBlock(Start);
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(dur * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//避免重复,通过变量判断是否已经释放了资源,释放后,不在进行操作。
//在stopAnimation中 self.bDealloc = YES;
if (self.bDealloc) {
return;
}
//dur时间后弹幕完全进入屏幕
if (self.moveBlock) {
self.moveBlock(Enter);
}
});
//弹幕完全离开屏幕
[UIView animateWithDuration:mDuration delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
frame.origin.x = -CGRectGetWidth(frame);
self.frame = frame;
} completion:^(BOOL finished) {
if (self.moveBlock) {
self.moveBlock(End);
}
[self removeFromSuperview];
}];
}
在viewcontroller中直接调用以下代码:
self.bulletManager = [[BulletManager alloc] init];
__weak ViewController *myself = self;
self.bulletManager.generateBulletBlock = ^(BulletView *bulletView) {
[myself addBulletView:bulletView];
};
- (void)addBulletView:(BulletView *)bulletView {
bulletView.frame = CGRectMake(CGRectGetWidth(self.view.frame)+50, 200 + 34 * bulletView.trajectory, CGRectGetWidth(bulletView.bounds), CGRectGetHeight(bulletView.bounds));
[self.view addSubview:bulletView];
[bulletView startAnimation];
}
//点击开始按钮,弹幕开始飞入屏幕
- (void)clickStart:(UIButton *)btn {
[self.bulletManager start];
}
最终展示效果如下:
效果展示
网友评论
//根据定义的duration计算速度以及完全进入屏幕的时间
CGFloat wholeWidth = CGRectGetWidth(self.frame) + mWidth + 50;
CGFloat speed = wholeWidth/mDuration;
CGFloat dur = (CGRectGetWidth(self.frame) + 50)/speed;
正如同 当时间固定, 路程和速度就成正比,由于路程越长,时间固定,速度就会越快。这个时候你就会看到越短的弹幕走在前面,这个越长的弹幕走在后面会超过短的弹幕。解决的方案让速度固定,动画时间= 路程/ 时间。应该就会不会有重叠现象,但是会出现一种情况每个弹幕之间的间距一样。
CGFloat speed = wholeWidth/mDuration;
CGFloat dur = (CGRectGetWidth(self.frame) + 50)/speed; 这些设成常数没有用啊
bulletView.frame = CGRectMake(CGRectGetWidth(self.view.frame)+50, 200 + 34 * bulletView.trajectory, CGRectGetWidth(bulletView.bounds), CGRectGetHeight(bulletView.bounds));
或者可以写到配置信息中。。。。这样可能更好一点
可以尝试以下两种方式,我自己的项目中用到的是第二种。
1. 可以为每一个view添加一个tapgesture,处理好回调
2. 可以创建一个bulletbackgroundview,把弹幕的view加在这个view上,方便弹幕随着这个view的移动而移动,比如唤起键盘,只要将这个背景view向上移动就可以了。然后在给这个背景view上加一个tapgesture事件处理点击事件
- (void)tapBulletView:(UITapGestureRecognizer *)gesture {
CGPoint clickPoint = [gesture locationInView:self];
for (UIView *v in [self subviews]) {
if ([v isKindOfClass:[BulletView class]]) {
if ([v.layer.presentationLayer hitTest:clickPoint]) {
//处理点击事件
}
}
}
}
不知道是否对你有帮助。
思路不错,赞一个
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(dur * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//避免重复,通过变量判断是否已经释放了资源,释放后,不在进行操作
if (self.bDealloc) {
return;
}
//dur时间后弹幕完全进入屏幕
if (self.moveBlock) {
self.moveBlock(Enter);
}
});
- (void)stopanimation {
self.bDealloc = YES;
.....
}