彈幕實現原理


如今直播行業確實是非常火爆啊,

大大小小的公司都要涉足一下直播的領域,

用斗魚的話來講,現在就是千播之戰。

而彈幕則無疑是直播功能當中最為重要的一個功能之一,

那么今天,我就帶着大家一起來實現一個簡單的Android端彈幕效果。

分析

首先我們來看一下斗魚上的彈幕效果,如下圖所示:

 
 

這是一個Dota2游戲直播的界面,我們可以看到,在游戲界面的上方有很多的彈幕,看直播的觀眾們就是在這里進行討論的。

那么這樣的一個界面該如何實現呢?其實並不復雜,我們只需要首先在布局中放置一個顯示游戲界面的View,

然后在游戲界面的上方再覆蓋一個顯示彈幕的View就可以了。彈幕的View必須要做成完全透明的,

這樣即使覆蓋在游戲界面的上方也不會影響到游戲的正常觀看,只有當有人發彈幕消息時,

再將消息繪制到彈幕的View上面就可以了。原理示意圖如下所示:

 
 

但是我們除了要能看到彈幕之外也要能發彈幕才行,因此還要再在彈幕的View上面再覆蓋一個操作界面的View,

然后我們就可以在操作界面上發彈幕、送禮物等。原理示意圖如下所示:

 
 

這樣我們就把基本的實現原理分析完了,下面就讓我們開始一步步實現吧。

實現視頻播放

由於本篇文章的主題是實現彈幕效果,並不涉及直播的任何其他功能,

因此這里我們就簡單地使用VideoView播放一個本地視頻來模擬最底層的游戲界面。

首先使用Android Studio新建一個DanmuTest項目,然后修改activity_main.xml中的代碼,如下所示:

 
 

布局文件的代碼非常簡單,只有一個VideoView,我們將它設置為居中顯示。

然后修改MainActivity中的代碼,如下所示:

 
 

上面的代碼中使用了VideoView的最基本用法。

在onCreate()方法中獲取到了VideoView的實例,

給它設置了一個視頻文件的地址,然后調用start()方法開始播放。

當然,我事先已經在SD的根目錄中准備了一個叫Pixels.mp4的視頻文件。

這里使用到了SD卡的功能,但是為了代碼簡單起見,我並沒有加入運行時權限的處理,

因此一定要記得將你的項目的targetSdkVersion指定成23以下。

另外,為了讓視頻播放可以有最好的體驗效果,這里使用了沉浸式模式的寫法。

對沉浸式模式還不理解的朋友可以參考我的上一篇文章Android狀態欄微技巧,帶你真正理解沉浸式模式

最后,我們在AndroidManifest.xml中將Activity設置為橫屏顯示並加入權限聲明,如下所示:

 
 

OK,現在可以運行一下項目了,程序啟動之后就會自動開始播放視頻,效果如下圖所示:

 
 

這樣我們就把第一步的功能實現了。

實現彈幕效果

接下來我們開始實現彈幕效果。彈幕其實也就是一個自定義的View,

它的上面可以顯示類似於跑馬燈的文字效果。觀眾們發表的評論都會在彈幕上顯示出來,

但又會很快地移出屏幕,既可以起到互動的作用,同時又不會影響視頻的正常觀看。

我們可以自己來編寫這樣的一個自定義View,當然也可以直接使用網上現成的開源項目。

那么為了能夠簡單快速地實現彈幕效果,

這里我就准備直接使用由嗶哩嗶哩開源的彈幕效果庫DanmakuFlameMaster了。

DanmakuFlameMaster庫的項目主頁地址是:https://github.com/Bilibili/DanmakuFlameMaster

話說現在使用Android Studio來引入一些開源庫真的非常方便,

只需要在build.gradle文件里面添加開源庫的依賴就可以了。

那么我們修改app/build.gradle文件,並在dependencies閉包中添加如下依賴:

 
 

這樣我們就將DanmakuFlameMaster庫引入到當前項目中了。然后修改activity_main.xml中的代碼,如下所示:

 
 

可以看到,這里在RelativeLayout中加入了一個DanmakuView控件,這個控件就是用於顯示彈幕信息的了。

注意一定要將DanmakuView寫在VideoView的下面,因為RelativeLayout中后添加的控件會被覆蓋在上面。

接下來修改MainActivity中的代碼,我們在這里加入彈幕顯示的邏輯,如下所示:

public class MainActivity extends AppCompatActivity{
private boolean showDanmaku;
private DanmakuView danmakuView;
private DanmakuContext danmakuContext;
private BaseDanmakuParser parser =new BaseDanmakuParser() {
@Override
protected IDanmakusparse() {
return newDanmakus();

}

};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

VideoView videoView = (VideoView) findViewById(R.id.video_view);

videoView.setVideoPath(Environment.getExternalStorageDirectory() +"/Pixels.mp4");

videoView.start();

danmakuView = (DanmakuView) findViewById(R.id.danmaku_view);

danmakuView.enableDanmakuDrawingCache(true);

danmakuView.setCallback(new DrawHandler.Callback() {

@Override
public void prepared() {

showDanmaku =true;

danmakuView.start();

generateSomeDanmaku();

}

@Override
public void updateTimer(DanmakuTimer timer) {

}

@Override
public void danmakuShown(BaseDanmaku danmaku) {

}

@Override
public void drawingFinished() {

}

});

danmakuContext = DanmakuContext.create();

danmakuView.prepare(parser, danmakuContext);

}

/**
* 向彈幕View中添加一條彈幕   
*@paramcontent   
*          彈幕的具體內容   
*@paramwithBorder   
*          彈幕是否有邊框   
*/
private void addDanmaku(String content,booleanwithBorder) {

BaseDanmaku danmaku = danmakuContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL);

danmaku.text = content;

danmaku.padding =5;

danmaku.textSize = sp2px(20);

danmaku.textColor = Color.WHITE;

danmaku.setTime(danmakuView.getCurrentTime());if(withBorder) {

danmaku.borderColor = Color.GREEN;

}

danmakuView.addDanmaku(danmaku);

}

/**
* 隨機生成一些彈幕內容以供測試
*/
private void generateSomeDanmaku() {
new Thread(new Runnable() {

@Override
public void run() {
while(showDanmaku) {
int time =new Random().nextInt(300);

String content =""+ time + time;

addDanmaku(content,false);
try{

Thread.sleep(time);

}catch(InterruptedException e) {

e.printStackTrace();

}

}

}

}).start();

}

/**
* sp轉px的方法。
*/
public int sp2px(float spValue) {
final float fontScale = getResources().getDisplayMetrics().scaledDensity;
return(int) (spValue * fontScale +0.5f);

}

@Override
protected void onPause() {
super.onPause();
if(danmakuView !=null&& danmakuView.isPrepared()) {

danmakuView.pause();

}

}

@Override
protected voidon Resume() {
super.onResume();
if(danmakuView !=null&& danmakuView.isPrepared() && danmakuView.isPaused()) {

danmakuView.resume();

}

}

@Override
protected void onDestroy() {
super.onDestroy();

showDanmaku =false;if(danmakuView !=null) {

danmakuView.release();

danmakuView =null;

}

}

..

}

可以看到,

在onCreate()方法中我們先是獲取到了DanmakuView控件的實例,

然后調用了enableDanmakuDrawingCache()方法來提升繪制效率,

又調用了setCallback()方法來設置回調函數。

接着調用DanmakuContext.create()方法創建了一個DanmakuContext的實例,

DanmakuContext可以用於對彈幕的各種全局配置進行設定,

如設置字體、設置最大顯示行數等。這里我們並沒有什么特殊的要求,因此一切都保持默認。

另外我們還需要創建一個彈幕的解析器才行,這里直接創建了一個全局的BaseDanmakuParser。

有了DanmakuContext和BaseDanmakuParser,

接下來我們就可以調用DanmakuView的prepare()方法來進行准備,

准備完成后會自動調用剛才設置的回調函數中的prepared()方法,

然后我們在這里再調用DanmakuView的start()方法,這樣DanmakuView就可以開始正常工作了。

雖說DanmakuView已經在正常工作了,但是屏幕上沒有任何彈幕信息的話我們也看不出效果,

因此我們還要增加一個添加彈幕消息的功能。

觀察addDanmaku()方法,這個方法就是用於向DanmakuView中添加一條彈幕消息的。

其中首先調用了createDanmaku()方法來創建一個BaseDanmaku實例,

TYPE_SCROLL_RL表示這是一條從右向左滾動的彈幕,

然后我們就可以對彈幕的內容、字體大小、顏色、顯示時間等各種細節進行配置了。

注意addDanmaku()方法中有一個withBorder參數,這個參數用於指定彈幕消息是否帶有邊框,

這樣才好將自己發送的彈幕和別人發送的彈幕進行區分。

這樣我們就把最基本的彈幕功能就完成了,

現在只需要當在接收到別人發送的彈幕消息時,

調用addDanmaku()方法將這條彈幕添加到DanmakuView上就可以了。

但接收別人發送來的消息又涉及到了即時通訊技術,

顯然這一篇文章中不可能將復雜的即時通訊技術也進行講解,

因此這里我專門寫了一個generateSomeDanmaku()方法來隨機生成一些彈幕消息,

這樣就可以模擬出和斗魚類似的彈幕效果了。

除此之外,我們還需要在onPause()、onResume()、onDestroy()方法中進行一些邏輯處理,

以保證DanmakuView的資源可以得到釋放。

現在重新運行一下程序,效果如下圖所示:

 
 

這樣我們就把第二步的功能也實現了。

加入操作界面

那么下面我們開始進行第三步功能實現,加入發送彈幕消息的操作界面。

首先修改activity_main.xml中的代碼,如下所示:

 
 

這里的邏輯還是比較簡單的,

我們先是給DanmakuView設置了一個點擊事件,

當點擊屏幕時就會觸發這個點擊事件。然后進行判斷,

如果操作界面是隱藏的就將它顯示出來,如果操作界面是顯示的就將它隱藏掉,

這樣就可以簡單地通過點擊屏幕來實現操作界面的隱藏和顯示了。

接下來我們又給發送按鈕注冊了一個點擊事件,

當點擊發送時,獲取EditText中的輸入內容,

然后調用addDanmaku()方法將這條消息添加到DanmakuView上。

另外,這條彈幕是由我們自己發送的,因此addDanmaku()方法的第二個參數要傳入true。

最后,由於系統輸入法彈出的時候會導致焦點丟失,從而退出沉浸式模式,

因此這里還對系統全局的UI變化進行了監聽,保證程序一直可以處於沉浸式模式。

這樣我們就將所有的代碼都完成了,現在可以運行一下看看最終效果了。

由於電影播放的同時進行GIF截圖生成的文件太大了,無法上傳,

因此這里我是在電影暫停的情況進行操作的。效果如下圖所示:

 
 

可以看到,我們自己發送的彈幕是有一個綠色邊框包圍的,很容易和其他彈幕區分開。

這樣我們就把第三步的功能也實現了。

雖說現在我們已經成功實現了非常不錯的彈幕效果,

但其實這只是DanmakuFlameMaster庫提供的最基本的功能而已。

嗶哩嗶哩提供的這個彈幕開源庫中擁有極其豐富的功能,包含各種不同的彈幕樣式、特效等等。

不過本篇文章的主要目標是帶大家了解彈幕效果實現的思路,

並不是要對DanmakuFlameMaster這個庫進行全面的解析。

如果你對這個庫非常感興趣,可以到它的github主頁上面去學習更多的用法。

源碼下載,請點擊這里

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM