好久沒(méi)寫(xiě)博客了,也是因?yàn)樽罱卷?xiàng)目挺忙的!正好這次迭代之后有點(diǎn)時(shí)間,所以寫(xiě)寫(xiě)博客,消磨一下時(shí)間!
現(xiàn)在很多直播軟件都有相應(yīng)的彈幕功能,以前也沒(méi)怎么關(guān)注,最近正好公司的項(xiàng)目中用到了關(guān)于彈幕的內(nèi)容,所以這里正好記錄一下相關(guān)的知識(shí)!
B站DanmakuFlameMaster彈幕的相關(guān)鏈接
本文知識(shí)點(diǎn)
- DanmakuFlameMaster的集成與簡(jiǎn)單使用
- DanmakuFlameMaster的進(jìn)階使用
1. DanmakuFlameMaster的集成與簡(jiǎn)單使用
其實(shí)我這個(gè)人真的很笨,最初學(xué)習(xí)這個(gè)的時(shí)候,在網(wǎng)上找了很多文章!但是我都沒(méi)怎么看懂,基本上都是把gitHub里面的內(nèi)容直接粘貼過(guò)來(lái)的,后來(lái)知道怎么弄才大概看明白!或許自己太笨了吧!
1.1 DanmakuFlameMaster的集成
這個(gè)問(wèn)題挺簡(jiǎn)單的,沒(méi)有什么好說(shuō)的,按照github上面集成就可以了!如果你不需要兼容x86和armv5就不用添加最下面兩行的內(nèi)容了!
repositories {
jcenter()
}
dependencies {
compile 'com.github.ctiao:DanmakuFlameMaster:0.9.25'
compile 'com.github.ctiao:ndkbitmap-armv7a:0.9.21'
# Other ABIs: optional 這個(gè)是適配多種架構(gòu)的,如果你用虛擬機(jī)建議加上
compile 'com.github.ctiao:ndkbitmap-armv5:0.9.21'
compile 'com.github.ctiao:ndkbitmap-x86:0.9.21'
}
1.2 DanmakuFlameMaster的簡(jiǎn)單使用
開(kāi)始的時(shí)候需要設(shè)置的內(nèi)容還是很多的,我們一個(gè)一個(gè)來(lái)講解!
1.2.1 布局文件
DanmakuFlameMaster使用多種方式(View/SurfaceView/TextureView)實(shí)現(xiàn)高效繪制!其中分別對(duì)應(yīng)(DanmakuView/DanmakuSurfaceView/DanmakuTextureView)等相關(guān)的View,由于項(xiàng)目中集成的是最簡(jiǎn)單的彈幕功能,所有就沒(méi)有使用關(guān)于SurfaceView類的彈幕控件!
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="addDanmaku"
android:text="添加彈幕"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<master.flame.danmaku.ui.widget.DanmakuView
android:id="@+id/dv"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn"
app:layout_constraintVertical_weight="1" />
</android.support.constraint.ConstraintLayout>
布局基本上就是看你們項(xiàng)目中的需求,從而確定相應(yīng)的位置!沒(méi)有什么好說(shuō)的?。?!
1.2.2 設(shè)置相應(yīng)的屬性
一些簡(jiǎn)單的配置,都是DEMO上面有的,注解寫(xiě)的基本上很清楚了,沒(méi)有什么太多好說(shuō)的!
//設(shè)置最大顯示行數(shù)
HashMap<Integer, Integer> maxLInesPair = new HashMap<>(16);
maxLInesPair.put(BaseDanmaku.TYPE_SCROLL_RL, 8);
//設(shè)置是否禁止重疊
HashMap<Integer, Boolean> overlappingEnablePair = new HashMap<>(16);
overlappingEnablePair.put(BaseDanmaku.TYPE_SCROLL_RL, true);
overlappingEnablePair.put(BaseDanmaku.TYPE_FIX_TOP, true);
//創(chuàng)建彈幕上下文
mContext = DanmakuContext.create();
//設(shè)置一些相關(guān)的配置
mContext.setDuplicateMergingEnabled(false)
//是否重復(fù)合并
.setScrollSpeedFactor(1.2f)
//設(shè)置文字的比例
.setScaleTextSize(1.2f)
//圖文混排的時(shí)候使用!這里可以不用
.setCacheStuffer(new MyCacheStuffer(mActivity), mBackgroundCacheStuffer)
//設(shè)置顯示最大行數(shù)
.setMaximumLines(maxLInesPair)
//設(shè)置防,null代表可以重疊
.preventOverlapping(overlappingEnablePair);
//設(shè)置解析器
BaseDanmakuParser defaultDanmakuParser = getDefaultDanmakuParser();
其實(shí)最開(kāi)始我們項(xiàng)目中使用這個(gè)的時(shí)候,所有彈幕都是直接服務(wù)器返回的。所以開(kāi)始的時(shí)候,我的想法是通過(guò)解析器去處理,但是后來(lái)我放棄了!為什么?首先json的解析規(guī)則是很復(fù)雜的,代碼我簡(jiǎn)單看了看,說(shuō)實(shí)話,能力有限真的沒(méi)看懂相應(yīng)的json結(jié)構(gòu),而且即使看懂我,我還要把服務(wù)器的數(shù)據(jù),處理成可用的json結(jié)構(gòu)。我覺(jué)得這樣沒(méi)有必要,所以就開(kāi)啟了一個(gè)線程,添加相應(yīng)的彈幕了!是不是很機(jī)智。。。但是即便是這樣上面那個(gè)設(shè)置解析器的步驟也是不能省略的!
public static BaseDanmakuParser getDefaultDanmakuParser() {
return new BaseDanmakuParser() {
@Override
protected IDanmakus parse() {
return new Danmakus();
}
};
}
因?yàn)檫@里解析器沒(méi)有什么作用,所以這里直接按照最簡(jiǎn)單的方法寫(xiě)了一個(gè)解析器!代碼如上:
基本上上面就涵蓋了所有關(guān)于彈幕的配置內(nèi)容了!這里面關(guān)于setCacheStuffer()這個(gè)屬性我之后會(huì)進(jìn)行相應(yīng)的講解!這里你就知道有這么個(gè)東西就行,它主要是處理非文字類型彈幕的!所以如果你要是純文字的話可以不設(shè)置這個(gè)東西!后面會(huì)詳細(xì)講解這個(gè)東西的!
1.2.3 啟動(dòng)相應(yīng)的彈幕
關(guān)于啟動(dòng)彈幕,基本上都走的是相應(yīng)的回調(diào),在彈幕準(zhǔn)備好的時(shí)候,直接調(diào)相應(yīng)的啟動(dòng)方法就好了!
if (mDanmakuView != null) {
BaseDanmakuParser defaultDanmakuParser = getDefaultDanmakuParser();
//相應(yīng)的回掉
mDanmakuView.setCallback(new master.flame.danmaku.controller.DrawHandler.Callback() {
@Override
public void updateTimer(DanmakuTimer timer) {
//定時(shí)器更新的時(shí)候回掉
}
@Override
public void drawingFinished() {
//彈幕繪制完成時(shí)回掉
}
@Override
public void danmakuShown(BaseDanmaku danmaku) {
//彈幕展示的時(shí)候回掉
}
@Override
public void prepared() {
//彈幕準(zhǔn)備好的時(shí)候回掉,這里啟動(dòng)彈幕
mDanmakuView.start();
}
});
mDanmakuView.prepare(defaultDanmakuParser, mContext);
mDanmakuView.enableDanmakuDrawingCache(true);
}
還有相關(guān)的生命周期方法必須設(shè)置!重要的事情說(shuō)三遍,三遍,三遍!
@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;
}
}
這個(gè)時(shí)候你會(huì)發(fā)現(xiàn)你的屏幕上沒(méi)有任何內(nèi)容,這就對(duì)了!為什么呢?因?yàn)槟氵€沒(méi)添加彈幕呢?。?!所有的準(zhǔn)備工作都做好了,那么我們就開(kāi)始添加彈幕吧!按照B站的指示我們這么配置相關(guān)的彈幕
private void addDanmaku(boolean islive) {
//創(chuàng)建一個(gè)彈幕對(duì)象,這里后面的屬性是設(shè)置滾動(dòng)方向的!
BaseDanmaku danmaku = mContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL);
if (danmaku == null || mDanmakuView == null) {
return;
}
//彈幕顯示的文字
danmaku.text = "這是一條彈幕" + System.nanoTime();
//設(shè)置相應(yīng)的邊距,這個(gè)設(shè)置的是四周的邊距
danmaku.padding = 5;
// 可能會(huì)被各種過(guò)濾器過(guò)濾并隱藏顯示,若果是本機(jī)發(fā)送的彈幕,建議設(shè)置成1;
danmaku.priority = 0;
//是否是直播彈幕
danmaku.isLive = islive;
danmaku.setTime(mDanmakuView.getCurrentTime() + 1200);
//設(shè)置文字大小
danmaku.textSize = 25f;
//設(shè)置文字顏色
danmaku.textColor = Color.RED;
//設(shè)置陰影的顏色
danmaku.textShadowColor = Color.WHITE;
// danmaku.underlineColor = Color.GREEN;
//設(shè)置背景顏色
danmaku.borderColor = Color.GREEN;
//添加這條彈幕,也就相當(dāng)于發(fā)送
mDanmakuView.addDanmaku(danmaku);
}
這樣就成功的發(fā)送了一條文字彈幕了,這里說(shuō)一個(gè)問(wèn)題,最開(kāi)始的時(shí)候,我在想頁(yè)面增加彈幕的時(shí)機(jī)?因?yàn)椴淮嬖诎粹o進(jìn)行添加彈幕,那么只有在mDanmakuView.start();之后進(jìn)行調(diào)用,無(wú)法在生命周期方法中調(diào)用,如果是在生命周期方法中調(diào)用的話,會(huì)存在彈幕為空,不能添加的問(wèn)題!切記。。。這樣整個(gè)流程就穿起來(lái)了!
2. DanmakuFlameMaster的進(jìn)階使用
2.1 實(shí)現(xiàn)自定義彈幕的顯示
上面那個(gè)顯示,一般只會(huì)用在視頻直播的內(nèi)容上,但是對(duì)于有頭像的那種彈幕!比如說(shuō),產(chǎn)品跑過(guò)來(lái)說(shuō)!要不添加一個(gè)頭像吧!再加點(diǎn)話術(shù),弄的好看點(diǎn)!就像下面這樣:
剛開(kāi)始我在網(wǎng)上找的時(shí)候,很多人都說(shuō)使用SpannableStringBuilder去實(shí)現(xiàn),但是我覺(jué)得如果使用SpannableStringBuilder實(shí)現(xiàn)這個(gè)內(nèi)容的話,很蛋疼的!而且還會(huì)特別費(fèi)盡,如果樣式在復(fù)雜一點(diǎn)的話,那么就更加困難了!下面我們就來(lái)說(shuō)說(shuō)關(guān)于這個(gè)內(nèi)容的實(shí)現(xiàn)!
還記得上面說(shuō)到的關(guān)于圖文有一個(gè)設(shè)置嗎?setCacheStuffer(BaseCacheStuffer cacheStuffer, BaseCacheStuffer.Proxy cacheStufferAdapter) 這個(gè)是針對(duì)非文字的一些顯示樣式的設(shè)置!因?yàn)轫?xiàng)目中要實(shí)現(xiàn)的就是上面這個(gè)樣式,所以我仔細(xì)研究了一下關(guān)于上面這種樣式的顯示方案!
其實(shí)也是很簡(jiǎn)單的!就是重寫(xiě)BaseCacheStuffer類的一些方法而已!怎么實(shí)現(xiàn)的呢?其實(shí)就是自己繪制每條彈幕所顯示的內(nèi)容,這里其實(shí)應(yīng)該是個(gè)策略模式的實(shí)現(xiàn),感興趣的童鞋可以看看!這里就考驗(yàn)Canvas的一些API的使用了!不會(huì)的童鞋可以百度一下!好了,閑扯了這么久了!我們開(kāi)始吧!
我在開(kāi)始的時(shí)候,講過(guò)說(shuō)setCacheStuffer(BaseCacheStuffer cacheStuffer, BaseCacheStuffer.Proxy cacheStufferAdapter)這個(gè)是實(shí)現(xiàn)非文字的方法!所以只要你把上面的兩個(gè)參數(shù)搞懂就可以了!
- 參數(shù)1:你可以理解為繪制的相應(yīng)處理
- 參數(shù)2:你可以理解為一個(gè)相應(yīng)繪制的回調(diào)
我們一個(gè)一個(gè)去處理:
2.1.1 實(shí)現(xiàn)相應(yīng)的繪制
繪制的相應(yīng)操作主要是實(shí)現(xiàn)BaseCacheStuffer這個(gè)抽象類,所有關(guān)于繪制的方法都是你自己進(jìn)行實(shí)現(xiàn)的!所以我說(shuō)這里之前最好理解一下相應(yīng)的Canvas這個(gè)類?。?!當(dāng)你繼承這個(gè)抽象類的時(shí)候,你必須實(shí)現(xiàn)三個(gè)相應(yīng)的方法:
public class MyCacheStuffer extends BaseCacheStuffer {
@Override
public void measure(BaseDanmaku danmaku, TextPaint paint, boolean fromWorkerThread) {
//測(cè)量的相應(yīng)方法
}
@Override
public void clearCaches() {
//用來(lái)釋放或者清除一些資源
}
@Override
public void drawDanmaku(BaseDanmaku danmaku, Canvas canvas, float left, float top, boolean fromWorkerThread, AndroidDisplayer.DisplayerConfig displayerConfig) {
//繪制的相應(yīng)方法
}
}
基本上就是上面的這三個(gè)方法,最主要的就是測(cè)量和繪制的兩個(gè)方法!下面就貼一個(gè)上面顯示內(nèi)容的實(shí)現(xiàn)!
public class MyCacheStuffer extends BaseCacheStuffer {
/**
* 文字右邊間距
*/
private float RIGHTMARGE;
/**
* 文字和頭像間距
*/
private float LEFTMARGE;
/**
* 文字和右邊線距離
*/
private int TEXT_RIGHT_PADDING;
/**
* 文字大小
*/
private float TEXT_SIZE;
/**
* 頭像的大小
*/
private float IMAGEHEIGHT;
public MyCacheStuffer(Activity activity) {
// 初始化固定參數(shù),這些參數(shù)可以根據(jù)自己需求自行設(shè)定
LEFTMARGE = activity.getResources().getDimension(R.dimen.DIMEN_13PX);
RIGHTMARGE = activity.getResources().getDimension(R.dimen.DIMEN_22PX);
IMAGEHEIGHT = activity.getResources().getDimension(R.dimen.DIMEN_60PX);
TEXT_SIZE = activity.getResources().getDimension(R.dimen.DIMEN_24PX);
}
@Override
public void measure(BaseDanmaku danmaku, TextPaint paint, boolean fromWorkerThread) {
// 初始化數(shù)據(jù)
Map<String, Object> map = (Map<String, Object>) danmaku.tag;
String content = (String) map.get("content");
Bitmap bitmap = (Bitmap) map.get("bitmap");
// 設(shè)置畫(huà)筆
paint.setTextSize(TEXT_SIZE);
// 計(jì)算名字和內(nèi)容的長(zhǎng)度,取最大值
float contentWidth = paint.measureText(content);
// 設(shè)置彈幕區(qū)域的寬度
danmaku.paintWidth = contentWidth + IMAGEHEIGHT + LEFTMARGE + RIGHTMARGE;
// 設(shè)置彈幕區(qū)域的高度
danmaku.paintHeight = IMAGEHEIGHT * 2;
}
@Override
public void clearCaches() {
}
@Override
public void drawDanmaku(BaseDanmaku danmaku, Canvas canvas, float left, float top, boolean fromWorkerThread, AndroidDisplayer.DisplayerConfig displayerConfig) {
// 初始化數(shù)據(jù)
Map<String, Object> map = (Map<String, Object>) danmaku.tag;
String content = (String) map.get("content");
Bitmap bitmap = (Bitmap) map.get("bitmap");
String color = (String) map.get("color");
// 設(shè)置畫(huà)筆
Paint paint = new Paint();
paint.setTextSize(TEXT_SIZE);
//繪制背景
int textLength = (int) paint.measureText(content);
//隨機(jī)數(shù),主要是為了生成不同顏色的背景的
paint.setColor(Color.parseColor(color));
//獲取圖片的寬度
float rectBgLeft = left;
float rectBgTop = top;
float rectBgRight = left + IMAGEHEIGHT + textLength + LEFTMARGE + RIGHTMARGE;
float rectBgBottom = top + IMAGEHEIGHT;
canvas.drawRoundRect(new RectF(rectBgLeft, rectBgTop, rectBgRight, rectBgBottom), IMAGEHEIGHT / 2, IMAGEHEIGHT / 2, paint);
// 繪制頭像
float avatorRight = left + IMAGEHEIGHT;
float avatorBottom = top + IMAGEHEIGHT;
canvas.drawBitmap(bitmap, null, new RectF(left, top, avatorRight, avatorBottom), paint);
// 繪制彈幕內(nèi)容,文字白色的
paint.setColor(Color.WHITE);
float contentLeft = left + IMAGEHEIGHT + LEFTMARGE;
//計(jì)算文字的相應(yīng)偏移量
Paint.FontMetrics fontMetrics = paint.getFontMetrics();
//為基線到字體上邊框的距離,即上圖中的top
float textTop = fontMetrics.top;
//為基線到字體下邊框的距離,即上圖中的bottom
float textBottom = fontMetrics.bottom;
float contentBottom = top + IMAGEHEIGHT / 2;
//基線中間點(diǎn)的y軸計(jì)算公式
int baseLineY = (int) (contentBottom - textTop / 2 - textBottom / 2);
//繪制文字
canvas.drawText(content, contentLeft, baseLineY, paint);
}
}
忘了說(shuō)明一下了:我感覺(jué)這里面有一個(gè)地方很巧妙,就是Tag的設(shè)置!可以把許多參數(shù)都攜帶過(guò)來(lái),很不錯(cuò)的想法,當(dāng)然不是我想到的!我也是借鑒別人的。。。一首《無(wú)地自容》-->送給自己!
當(dāng)你添加彈幕的時(shí)候也會(huì)有改動(dòng),但是改動(dòng)的地方很小,像下面這樣!??!
//創(chuàng)建一條彈幕
BaseDanmaku danmaku = mContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL);
if (danmaku == null || mDanmakuView == null) {
return;
}
//設(shè)置相應(yīng)的數(shù)據(jù)
Bitmap showBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);
showBitmap = BitmapUtils.getShowPicture(showBitmap);
Map<String, Object> map = new HashMap<>(16);
map.put("content", "這里是顯示的內(nèi)容");
map.put("bitmap", showBitmap);
Random random = new Random();
int randomNum = random.nextInt(mContentColorBg.length);
map.put("color", mContentColorBg[randomNum]);
//設(shè)置相應(yīng)的tag
danmaku.tag = map;
danmaku.textSize = 0;
danmaku.padding = 10;
danmaku.text = "";
// 一定會(huì)顯示, 一般用于本機(jī)發(fā)送的彈幕
danmaku.priority = 1;
danmaku.isLive = false;
danmaku.setTime(mDanmakuView.getCurrentTime());
danmaku.textColor = Color.WHITE;
// 重要:如果有圖文混排,最好不要設(shè)置描邊(設(shè)textShadowColor=0),否則會(huì)進(jìn)行兩次復(fù)雜的繪制導(dǎo)致運(yùn)行效率降低
danmaku.textShadowColor = 0;
//添加一條
mDanmakuView.addDanmaku(danmaku);
這樣就成功的設(shè)置了一條相應(yīng)的彈幕了!但是我發(fā)現(xiàn)一個(gè)問(wèn)題,就是當(dāng)你這么設(shè)置了之后,之前發(fā)送文字的邏輯就要重新制定了!其實(shí)就是多定義一個(gè)類型,根據(jù)不同類型進(jìn)行不同的繪制就好了!很好解決的!這里就不再這里展開(kāi)說(shuō)了!
2.2 相應(yīng)的監(jiān)聽(tīng)問(wèn)題
關(guān)于監(jiān)聽(tīng),其實(shí)就是實(shí)現(xiàn)相應(yīng)的方法,但是還是有必要說(shuō)明一下,怎么處理!
mDanmakuView.setOnDanmakuClickListener(new IDanmakuView.OnDanmakuClickListener() {
@Override
public boolean onDanmakuClick(IDanmakus danmakus) {
//點(diǎn)擊事件
BaseDanmaku latest = danmakus.last();
if (null != latest) {
Map<String, Object> map = (Map<String, Object>) latest.tag;
//獲取相應(yīng)的數(shù)據(jù)
String userId = (String) map.get("content");
return true;
}
return false;
}
@Override
public boolean onDanmakuLongClick(IDanmakus danmakus) {
//長(zhǎng)按事件
return false;
}
@Override
public boolean onViewClick(IDanmakuView view) {
//這個(gè)我沒(méi)有嘗試,但是應(yīng)該是內(nèi)部View的點(diǎn)擊事件吧!猜測(cè)
return false;
}
});
}
華麗的分割線
基本上就解決了我們項(xiàng)目中的問(wèn)題,其實(shí)還有很多問(wèn)題我沒(méi)有去處理,這里只是給大家一個(gè)簡(jiǎn)單的案例,如果有什么不對(duì)的還希望指出!我及時(shí)修改。。。如果有什么不懂的,也可以留言!我盡量幫你解決?。?!
忘了,附上代碼地址