1. 程式人生 > >Android彈幕功能實現,模仿鬥魚直播的彈幕效果

Android彈幕功能實現,模仿鬥魚直播的彈幕效果

記得之前有位朋友在我的公眾號裡問過我,像直播的那種彈幕功能該如何實現?如今直播行業確實是非常火爆啊,大大小小的公司都要涉足一下直播的領域,用鬥魚的話來講,現在就是千播之戰。而彈幕則無疑是直播功能當中最為重要的一個功能之一,那麼今天,我就帶著大家一起來實現一個簡單的Android端彈幕效果。

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

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

那麼這樣的一個介面該如何實現呢?其實並不複雜,我們只需要首先在佈局中放置一個顯示遊戲介面的View,然後在遊戲介面的上方再覆蓋一個顯示彈幕的View就可以了。彈幕的View必須要做成完全透明的,這樣即使覆蓋在遊戲介面的上方也不會影響到遊戲的正常觀看,只有當有人發彈幕訊息時,再將訊息繪製到彈幕的View上面就可以了。原理示意圖如下所示:

但是我們除了要能看到彈幕之外也要能發彈幕才行,因此還要再在彈幕的View上面再覆蓋一個操作介面的View,然後我們就可以在操作介面上發彈幕、送禮物等。原理示意圖如下所示:

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

實現視訊播放 由於本篇文章的主題是實現彈幕效果,並不涉及直播的任何其他功能,因此這裡我們就簡單地使用VideoView播放一個本地視訊來模擬最底層的遊戲介面。

首先使用Android Studio新建一個DanmuTest專案,然後修改activity_main.xml中的程式碼,如下所示:

<RelativeLayout     xmlns:android="http://schemas.android.com/apk/res/android"     android:id="@+id/activity_main"     android:layout_width="match_parent"     android:layout_height="match_parent"     android:background="#000">

    <VideoView         android:id="@+id/video_view"         android:layout_width="match_parent"         android:layout_height="wrap_content"         android:layout_centerInParent="true"/>

</RelativeLayout> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 佈局檔案的程式碼非常簡單,只有一個VideoView,我們將它設定為居中顯示。  然後修改MainActivity中的程式碼,如下所示:

public class MainActivity extends AppCompatActivity {

    @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();     }

    @Override     public void onWindowFocusChanged(boolean hasFocus) {         super.onWindowFocusChanged(hasFocus);         if (hasFocus && Build.VERSION.SDK_INT >= 19) {             View decorView = getWindow().getDecorView();             decorView.setSystemUiVisibility(                     View.SYSTEM_UI_FLAG_LAYOUT_STABLE                             | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION                             | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN                             | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION                             | View.SYSTEM_UI_FLAG_FULLSCREEN                             | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);         }     }

} 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 上面的程式碼中使用了VideoView的最基本用法。在onCreate()方法中獲取到了VideoView的例項,給它設定了一個視訊檔案的地址,然後呼叫start()方法開始播放。當然,我事先已經在SD的根目錄中準備了一個叫Pixels.mp4的視訊檔案。

這裡使用到了SD卡的功能,但是為了程式碼簡單起見,我並沒有加入執行時許可權的處理,因此一定要記得將你的專案的targetSdkVersion指定成23以下。

另外,為了讓視訊播放可以有最好的體驗效果,這裡使用了沉浸式模式的寫法。對沉浸式模式還不理解的朋友可以參考我的上一篇文章 Android狀態列微技巧,帶你真正理解沉浸式模式 。

最後,我們在AndroidManifest.xml中將Activity設定為橫屏顯示並加入許可權宣告,如下所示:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"           package="com.example.guolin.danmutest">

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application         android:allowBackup="true"         android:icon="@mipmap/ic_launcher"         android:label="@string/app_name"         android:supportsRtl="true"         android:theme="@style/AppTheme">         <activity android:name=".MainActivity" android:screenOrientation="landscape"                   android:configChanges="orientation|keyboardHidden|screenLayout|screenSize">             <intent-filter>                 <action android:name="android.intent.action.MAIN"/>                 <category android:name="android.intent.category.LAUNCHER"/>             </intent-filter>         </activity>     </application>

</manifest> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 OK,現在可以執行一下專案了,程式啟動之後就會自動開始播放視訊,效果如下圖所示:

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

實現彈幕效果 接下來我們開始實現彈幕效果。彈幕其實也就是一個自定義的View,它的上面可以顯示類似於跑馬燈的文字效果。觀眾們發表的評論都會在彈幕上顯示出來,但又會很快地移出螢幕,既可以起到互動的作用,同時又不會影響視訊的正常觀看。

我們可以自己來編寫這樣的一個自定義View,當然也可以直接使用網上現成的開源專案。那麼為了能夠簡單快速地實現彈幕效果,這裡我就準備直接使用由嗶哩嗶哩開源的彈幕效果庫DanmakuFlameMaster了。

DanmakuFlameMaster庫的專案主頁地址是:https://github.com/Bilibili/DanmakuFlameMaster

話說現在使用Android Studio來引入一些開源庫真的非常方便,只需要在build.gradle檔案裡面新增開源庫的依賴就可以了。那麼我們修改app/build.gradle檔案,並在dependencies閉包中新增如下依賴:

dependencies {     compile fileTree(dir: 'libs', include: ['*.jar'])     compile 'com.android.support:appcompat-v7:24.2.1'     testCompile 'junit:junit:4.12'     compile 'com.github.ctiao:DanmakuFlameMaster:0.5.3' } 1 2 3 4 5 6 這樣我們就將DanmakuFlameMaster庫引入到當前專案中了。然後修改activity_main.xml中的程式碼,如下所示:

<RelativeLayout     xmlns:android="http://schemas.android.com/apk/res/android"     android:id="@+id/activity_main"     android:layout_width="match_parent"     android:layout_height="match_parent"     android:background="#000">

    <VideoView         android:id="@+id/video_view"         android:layout_width="match_parent"         android:layout_height="wrap_content"         android:layout_centerInParent="true"/>

    <master.flame.danmaku.ui.widget.DanmakuView         android:id="@+id/danmaku_view"         android:layout_width="match_parent"         android:layout_height="match_parent" />

</RelativeLayout> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 可以看到,這裡在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 IDanmakus parse() {             return new Danmakus();         }     };

    @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中新增一條彈幕      * @param content      *          彈幕的具體內容      * @param  withBorder      *          彈幕是否有邊框      */     private void addDanmaku(String content, boolean withBorder) {         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 void onResume() {         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;         }     }

    ......

} 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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 可以看到,在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中的程式碼,如下所示:

<RelativeLayout     xmlns:android="http://schemas.android.com/apk/res/android"     android:id="@+id/activity_main"     android:layout_width="match_parent"     android:layout_height="match_parent"     android:background="#000">

    ......

    <LinearLayout         android:id="@+id/operation_layout"         android:layout_width="match_parent"         android:layout_height="50dp"         android:layout_alignParentBottom="true"         android:background="#fff"         android:visibility="gone">

        <EditText             android:id="@+id/edit_text"             android:layout_width="0dp"             android:layout_height="match_parent"             android:layout_weight="1"             />

        <Button             android:id="@+id/send"             android:layout_width="wrap_content"             android:layout_height="match_parent"             android:text="Send" />     </LinearLayout>

</RelativeLayout> 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 可以看到,這裡我們加入了一個LinearLayout來作為操作介面。LinearLayout中並沒有什麼複雜的控制元件,只有一個EditText用於輸入內容,一個Button用於傳送彈幕。注意我們一開始是將LinearLayout隱藏的,因為不能讓這個操作介面一直遮擋著VideoView,只有使用者想要發彈幕的時候才應該將它顯示出來。

接下來修改MainActivity中的程式碼,在這裡面加入傳送彈幕的邏輯,如下所示:

public class MainActivity extends AppCompatActivity {

    ......

    @Override     protected void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         ......         final  LinearLayout operationLayout = (LinearLayout) findViewById(R.id.operation_layout);         final Button send = (Button) findViewById(R.id.send);         final EditText editText = (EditText) findViewById(R.id.edit_text);         danmakuView.setOnClickListener(new View.OnClickListener() {             @Override             public void onClick(View view) {                 if (operationLayout.getVisibility() == View.GONE) {                     operationLayout.setVisibility(View.VISIBLE);                 } else {                     operationLayout.setVisibility(View.GONE);                 }             }         });         send.setOnClickListener(new View.OnClickListener() {             @Override             public void onClick(View view) {                 String content = editText.getText().toString();                 if (!TextUtils.isEmpty(content)) {                     addDanmaku(content, true);                     editText.setText("");                 }             }         });         getWindow().getDecorView().setOnSystemUiVisibilityChangeListener (new View.OnSystemUiVisibilityChangeListener() {             @Override             public void onSystemUiVisibilityChange(int visibility) {                 if (visibility == View.SYSTEM_UI_FLAG_VISIBLE) {                     onWindowFocusChanged(true);                 }             }         });     }     ......

} 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 這裡的邏輯還是比較簡單的,我們先是給DanmakuView設定了一個點選事件,當點選螢幕時就會觸發這個點選事件。然後進行判斷,如果操作介面是隱藏的就將它顯示出來,如果操作介面是顯示的就將它隱藏掉,這樣就可以簡單地通過點選螢幕來實現操作介面的隱藏和顯示了。

接下來我們又給傳送按鈕註冊了一個點選事件,當點擊發送時,獲取EditText中的輸入內容,然後呼叫addDanmaku()方法將這條訊息新增到DanmakuView上。另外,這條彈幕是由我們自己傳送的,因此addDanmaku()方法的第二個引數要傳入true。

最後,由於系統輸入法彈出的時候會導致焦點丟失,從而退出沉浸式模式,因此這裡還對系統全域性的UI變化進行了監聽,保證程式一直可以處於沉浸式模式。

這樣我們就將所有的程式碼都完成了,現在可以執行一下看看最終效果了。由於電影播放的同時進行GIF截圖生成的檔案太大了,無法上傳,因此這裡我是在電影暫停的情況進行操作的。效果如下圖所示:

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

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

雖說現在我們已經成功實現了非常不錯的彈幕效果,但其實這只是DanmakuFlameMaster庫提供的最基本的功能而已。嗶哩嗶哩提供的這個彈幕開源庫中擁有極其豐富的功能,包含各種不同的彈幕樣式、特效等等。不過本篇文章的主要目標是帶大家瞭解彈幕效果實現的思路,並不是要對DanmakuFlameMaster這個庫進行全面的解析。如果你對這個庫非常感興趣,可以到它的github主頁上面去學習更多的用法。

那麼今天的文章到此結束。