轉載出處: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>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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主頁上面去學習更多的用法。
那麼今天的文章到此結束。