彈幕使用場景
- 直播(
實時性
)
彈幕是直播系統的核心功能之一。一段摘抄的直播彈幕描述(貌似美拍工程師寫的)
- 美拍直播彈幕系統從 2015 年 11 月到現在,經過了三個階段的演進,目前能支撐百萬用戶同時在線
- 前中期使用 HTTP 輪詢方案,中後期替換爲長連接方案
- 直播間消息,相對於 IM 的場景,有如下幾個特點:
- 消息要求及時,過時的消息對於用戶來說不重要
- 鬆散的羣聊,用戶隨時進羣,隨時退羣
- 用戶進羣后,離線期間(接聽電話)的消息不需要重發
- 視頻(
實時性
+彈幕跟幀時間點關聯
)
彈幕引擎(B站開源彈幕
)
GitHub:https://github.com/bilibili/DanmakuFlameMaster
DanmakuFlameMaster 是 Android 上開源彈幕解析繪製引擎項目,貌似Android 上最好的開源彈幕引擎·烈焰彈幕
DanmakuFlameMaster
開發包已被包括優酷土豆、開迅視頻、MissEvan、echo回聲、鬥魚TV、天天動聽、被窩聲次元、ACFUN 等 APP 使用
DanmakuFlameMaster 特點
- 使用多種方式(View/SurfaceView/TextureView)實現高效繪製
- B站xml彈幕格式解析
- 基礎彈幕精確還原繪製
- 支持
mode7
特殊彈幕 - 多核機型優化,高效的預緩存機制
- 支持多種顯示效果選項實時切換
- 實時彈幕顯示支持
- 換行彈幕支持/運動彈幕支持
- 支持自定義字體
- 支持多種彈幕參數設置
- 支持多種方式的彈幕屏蔽
DanmakuFlameMaster 細節API
- 模式在
BaseDanmaku
裏有聲明,總結一下就是
public final static int TYPE_SCROLL_RL = 1; // 水平右往左
public final static int TYPE_SCROLL_LR = 6; // 水平左往右
public final static int TYPE_FIX_TOP = 5; // 固定在頂部
public final static int TYPE_FIX_BOTTOM = 4; // 固定在底部
public final static int TYPE_SPECIAL = 7; // 特殊彈幕
-
添加一條圖文混合彈幕
請參考:DanmuKuDemo -
展示隱藏彈幕
// 實現了IDanmakuView接口的彈幕View,調用下面2個方法展示或隱藏彈幕
void show();
void hide();
- 如何監聽每一個彈幕的移動的x,y位置
查看一下BaseDanmaku.layout
方法的實現就明白了,彈幕庫每次繪製前
都會調用這個方法
集成B站彈幕
- 集成配置
repositories {
jcenter()
}
// 如果你不需要兼容x86和armv5就不用添加最下面兩行的內容了
dependencies {
api 'com.github.ctiao:DanmakuFlameMaster:0.9.25'
api 'com.github.ctiao:ndkbitmap-armv7a:0.9.21'
// Other ABIs: optional 這個是適配多種架構的,如果你用虛擬機建議加上
api 'com.github.ctiao:ndkbitmap-armv5:0.9.21'
api 'com.github.ctiao:ndkbitmap-x86:0.9.21'
}
DanmakuFlameMaster
使用多種方式(View
/SurfaceView
/TextureView
)實現高效繪製!其中分別對應(DanmakuView/DanmakuSurfaceView/DanmakuTextureView)
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<master.flame.danmaku.ui.widget.DanmakuView
android:id="@+id/sv_danmaku"
android:layout_width="match_parent"
android:layout_height="300dp" />
</RelativeLayout>
- 代碼配置
// 設置最大顯示行數(滾動彈幕最大顯示行數)
HashMap<Integer, Integer> maxLinesPair = new HashMap<>();
maxLinesPair.put(BaseDanmaku.TYPE_SCROLL_RL, 2);
// 設置是否禁止重疊
HashMap<Integer, Boolean> overlappingEnablePair = new HashMap<Integer, Boolean>();
overlappingEnablePair.put(BaseDanmaku.TYPE_SCROLL_RL, true);
overlappingEnablePair.put(BaseDanmaku.TYPE_FIX_TOP, true);
mContext = DanmakuContext.create();
mContext.setDanmakuStyle(IDisplayer.DANMAKU_STYLE_STROKEN, 3)
.setDuplicateMergingEnabled(false)
.setScrollSpeedFactor(1.2f)
// 設置文字的比例
.setScaleTextSize(1.2f)
// 圖文混排使用 SpannedCacheStuffer
.setCacheStuffer(new SpannedCacheStuffer(), mCacheStufferAdapter)
// 繪製背景使用 BackgroundCacheStuffer
.setCacheStuffer(new BackgroundCacheStuffer())
.setMaximumLines(maxLinesPair)
.preventOverlapping(overlappingEnablePair).setDanmakuMargin(40);
if (mDanmakuView != null) {
mParser = createParser(null);
mDanmakuView.setCallback(new master.flame.danmaku.controller.DrawHandler.Callback() {
@Override
public void updateTimer(DanmakuTimer timer) {
}
@Override
public void drawingFinished() {
// 彈幕繪製完成時回掉(即彈幕展示完成移出屏幕時調用)
Log.i(TAG, "drawingFinished");
}
@Override
public void danmakuShown(BaseDanmaku danmaku) {
Log.i(TAG, "danmakuShown " + danmaku.text);
}
@Override
public void prepared() {
// 彈幕準備好的時候回掉,這裏啓動彈幕
Log.i(TAG, "prepared");
mDanmakuView.start();
}
});
mDanmakuView.setOnDanmakuClickListener(new IDanmakuView.OnDanmakuClickListener() {
@Override
public boolean onDanmakuClick(IDanmakus danmakus) {
Log.i(TAG, "onDanmakuClick danmakus size: " + danmakus.size());
BaseDanmaku latest = danmakus.last();
if (null != latest) {
Log.i(TAG, "onDanmakuClick text of latest danmaku: " + latest.text);
return true;
}
return false;
}
@Override
public boolean onDanmakuLongClick(IDanmakus danmakus) {
return false;
}
@Override
public boolean onViewClick(IDanmakuView view) {
return false;
}
});
mDanmakuView.prepare(mParser, mContext);
mDanmakuView.showFPS(true);
mDanmakuView.enableDanmakuDrawingCache(true);
}
- 直播頁面按Home鍵退後臺或重新回到前臺,相關的生命週期方法必須設置
// View 實現 彈幕
private IDanmakuView mDanmakuView;
@Override
protected void onPause() {
super.onPause();
if (mDanmakuView != null && mDanmakuView.isPrepared()) {
mDanmakuView.pause();
}
}
@Override
protected void onResume() {
super.onResume();
if (mDanmakuView != null && mDanmakuView.isPrepared() && mDanmakuView.isPaused()) {
mDanmakuView.resume();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mDanmakuView != null) {
// dont forget release!
mDanmakuView.release();
mDanmakuView = null;
}
}
@Override
public void onBackPressed() {
super.onBackPressed();
if (mDanmakuView != null) {
// dont forget release!
mDanmakuView.release();
mDanmakuView = null;
}
}
- 添加一條普通文字彈幕
/**
* danmaku.isLive == true的情況下,請在非UI線程中使用此方法,避免可能卡住主線程
* @param item
*/
void addDanmaku(BaseDanmaku item);
private void addDanmaku(boolean isLive) {
// 創建一個彈幕對象,這裏後面的屬性是設置滾動方向的!
BaseDanmaku danmaku =
mContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL);
if (danmaku == null || mDanmakuView == null) {
return;
}
danmaku.text = "這是一條彈幕";
// 設置相應的邊距,這個設置的是四周的邊距
danmaku.padding = 5;
// 可能會被各種過濾器過濾並隱藏顯示,若是本機發送的彈幕,建議設置成1;
danmaku.priority = 0;
// 是否是直播彈幕
danmaku.isLive = isLive;
danmaku.setTime(mDanmakuView.getCurrentTime() + 1200);
danmaku.textSize = 25f * (mParser.getDisplayer().getDensity() - 0.6f);
// 設置文字顏色
danmaku.textColor = Color.RED;
// 設置陰影的顏色
danmaku.textShadowColor = Color.WHITE;
// danmaku.underlineColor = Color.GREEN;
danmaku.borderColor = Color.RED;
// 添加這條彈幕,也就相當於發送
mDanmakuView.addDanmaku(danmaku);
}