彈幕,國內流行於視訊網站A站和B站。網上關於彈幕的實現方法有很多,目前Android平臺已經有比較成熟的解決方案DanmakuFlameMaster 。而iOS平臺尚無比較成熟的開源庫,在借鑑DanmakuFlameMaster的實現思想後,特分享iOS平臺彈幕解決方案HJDanmakuDemo。本文將介紹彈幕的大致實現原理。
看過DanmakuFlameMaster原始碼的朋友都知道,彈幕實現主要需要解決以下幾個問題
- 彈幕繪製方式
- 彈幕時間控制
- 彈幕碰撞檢測原理
- 彈幕暫停及恢復
本文主要從以上4個方面介紹彈幕的詳細實現原理。首先是彈幕繪製方式,在DanmakuFlameMaster庫中,它主要通過view的自定義draw一幀一幀的繪製來完成彈幕的顯示,這種方式最大的問題在於效能以及動畫的不流暢。彈幕流暢的前提要求每秒繪製的幀數在30幀以上,而移動裝置效能千差萬別,當同一時刻需要繪製大量彈幕的時候,對於低端裝置就會出現卡幀不流暢的情況,這會大大降低使用者的體驗。因此,在本專案中放棄採用自定義繪製幀的方式,而是採用系統動畫的方式來實現彈幕文字的滾動。
1 2 3 |
[UIView animateWithDuration:danmaku.remainTime delay:0 options:UIViewAnimationOptionCurveLinear animations:^{ danmaku.label.frame = CGRectMake(-danmaku.size.width, danmaku.py, danmaku.size.width, danmaku.size.height); } completion:nil]; |
其次,就是彈幕時間的控制。由於採用系統動畫的方式,所以不需要時刻計算每一個彈幕的顯示時間以及其X座標(假設彈幕橫向滾動),我們需要做的就是在彈幕需要出現的時候建立它,然後設定彈幕存活的時間,剩餘滾動動畫交給系統負責,當然,彈幕剩餘時間需要我們來更新。本專案中,建立了一個0.5s間隔的定時器,主要負責建立新的彈幕並更新已顯示彈幕的剩餘時間,也就是說0.5s執行一次計算,如果需要,可以將重新整理間隔設定成5s或者更長。
1 2 3 |
_timer = [NSTimer timerWithTimeInterval:_frameInterval target:self selector:@selector(onTimeCount) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes]; [_timer fire]; |
然後,就是彈幕碰撞檢測的問題。碰撞檢測主要難點在於檢測橫向滾動彈幕之間的碰撞,彈幕存活時間由其顯示時間和存活長短決定,因此,彈幕之間是否碰撞只需檢測開始和消失是否碰撞即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
- (BOOL)checkIsWillHitWithWidth:(float)width DanmakuL:(DanmakuBaseModel *)danmakuL DanmakuR:(DanmakuBaseModel *)danmakuR { if (danmakuL.remainTime<=0) { return NO; } if (danmakuL.px+danmakuL.size.width>danmakuR.px) { return YES; } float minRemainTime = MIN(danmakuL.remainTime, danmakuR.remainTime); float px1 = [danmakuL pxWithScreenWidth:width RemainTime:(danmakuL.remainTime-minRemainTime)]; float px2 = [danmakuR pxWithScreenWidth:width RemainTime:(danmakuR.remainTime-minRemainTime)]; if (px1+danmakuL.size.width>px2) { return YES; } return NO; } |
最後,彈幕的暫停及恢復。由於彈幕滾動採用系統動畫,所以在解決彈幕暫停前需要先了解系統動畫的實現原理,有興趣的朋友可以參考動畫解釋這篇文章,在此就不做過多介紹。暫停的基本原理就是通過view的presentationLayer獲取物件的當前座標並賦給其frame,然後移除layer動畫,考慮到緩衝,可以在當前座標基礎上-1
1 2 3 4 5 6 7 8 9 10 11 12 13 |
- (void)pauseRenderer { for (DanmakuBaseModel *danmaku in _drawArray.objectEnumerator) { CALayer *layer = danmaku.label.layer; CGRect rect = danmaku.label.frame; if (layer.presentationLayer) { rect = ((CALayer *)layer.presentationLayer).frame; rect.origin.x-=1; } danmaku.label.frame = rect; [danmaku.label.layer removeAllAnimations]; } } |
有興趣的童鞋可以下載HJDanmakuDemo檢視具體使用方法,有什麼疑問可以在後面留言。 如果你喜歡,希望能在github上為本demo點上一讚,感謝你的來訪!