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

轉載請註明出處:http://blog.csdn.net/sinyu890807/article/details/51933728

本文同步發表於我的微信公衆號,掃一掃文章底部的二維碼或在微信搜索 郭霖 即可關注,每天都有文章更新。

大家好,感覺好像已經很久沒更新博客了。前段時間主要是忙於新書的事情,時間比較緊張。而現在新書已經完稿,剩下的事情就都是出版社的工作了,那麼我又可以抽出時間來寫寫博客了。

記得之前有位朋友在我的公衆號裏問過我,像直播的那種彈幕功能該如何實現?如今直播行業確實是非常火爆啊,大大小小的公司都要涉足一下直播的領域,用鬥魚的話來講,現在就是千播之戰。而彈幕則無疑是直播功能當中最爲重要的一個功能之一,那麼今天,我就帶着大家一起來實現一個簡單的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>

佈局文件的代碼非常簡單,只有一個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);
        }
    }

}

上面的代碼中使用了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>

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'
}

這樣我們就將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>

可以看到,這裏在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;
        }
    }

    ......

}

可以看到,在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>

可以看到,這裏我們加入了一個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);
                }
            }
        });
    }
    ......

}

這裏的邏輯還是比較簡單的,我們先是給DanmakuView設置了一個點擊事件,當點擊屏幕時就會觸發這個點擊事件。然後進行判斷,如果操作界面是隱藏的就將它顯示出來,如果操作界面是顯示的就將它隱藏掉,這樣就可以簡單地通過點擊屏幕來實現操作界面的隱藏和顯示了。

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

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

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

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

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


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

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

源碼下載,請點擊這裏

關注我的技術公衆號,每天都有優質技術文章推送。關注我的娛樂公衆號,工作、學習累了的時候放鬆一下自己。

微信掃一掃下方二維碼即可關注:

        

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章