DanmakuFlameMaster 是 Android 上开源弹幕解析绘制引擎项目,也是 Android 上最好的开源弹幕引擎·烈焰弹幕。其架构清晰,简单易用,支持多种高效率绘制方式选择,支持多种自定义功能设置上。
目前,DanmakuFlameMaster 开发包已被包括优酷土豆、开迅视频、MissEvan、echo回声、斗鱼TV、天天动听、被窝声次元、ACFUN 等 APP 使用。
Features
使用多种方式(View/SurfaceView/TextureView)实现高效绘制
B站xml弹幕格式解析
基础弹幕精确还原绘制
支持mode7特殊弹幕
多核机型优化,高效的预缓存机制
支持多种显示效果选项实时切换
实时弹幕显示支持
换行弹幕支持/运动弹幕支持
支持自定义字体
支持多种弹幕参数设置
支持多种方式的弹幕屏蔽
TODO:
继续精确/稳定绘帧周期
增加OpenGL ES绘制方式
改进缓存策略和效率
github地址:https://github.com/Bilibili/DanmakuFlameMaster
demo示例
build.gradle中添加
dependencies {
compile 'com.github.ctiao:dfm:0.4.2'
}
一、布局文件定义
<master.flame.danmaku.ui.widget.DanmakuView
android:id="@+id/sv_danmaku"
android:layout_width="match_parent"
android:layout_height="match_parent" />
二、初始化
private BaseDanmakuParser mParser;//解析器对象
private IDanmakuView mDanmakuView;
//实例化
mDanmakuView = (IDanmakuView) findViewById(R.id.sv_danmaku);
private DanmakuContext mContext;
mContext = DanmakuContext.create();
// 设置弹幕的最大显示行数
HashMap<Integer, Integer> maxLinesPair = new HashMap<Integer, Integer>();
maxLinesPair.put(BaseDanmaku.TYPE_SCROLL_RL, 3); // 滚动弹幕最大显示3行
// 设置是否禁止重叠
HashMap<Integer, Boolean> overlappingEnablePair = new HashMap<Integer, Boolean>();
overlappingEnablePair.put(BaseDanmaku.TYPE_SCROLL_LR, true);
overlappingEnablePair.put(BaseDanmaku.TYPE_FIX_BOTTOM, true);
mContext.setDanmakuStyle(IDisplayer.DANMAKU_STYLE_STROKEN, 3) //设置描边样式
.setDuplicateMergingEnabled(false)
.setScrollSpeedFactor(1.2f) //是否启用合并重复弹幕
.setScaleTextSize(1.2f) //设置弹幕滚动速度系数,只对滚动弹幕有效
.setCacheStuffer(new SpannedCacheStuffer(), mCacheStufferAdapter) // 图文混排使用SpannedCacheStuffer 设置缓存绘制填充器,默认使用{@link SimpleTextCacheStuffer}只支持纯文字显示, 如果需要图文混排请设置{@link SpannedCacheStuffer}如果需要定制其他样式请扩展{@link SimpleTextCacheStuffer}|{@link SpannedCacheStuffer}
.setMaximumLines(maxLinesPair) //设置最大显示行数
.preventOverlapping(overlappingEnablePair); //设置防弹幕重叠,null为允许重叠
if (mDanmakuView != null) {
mParser = createParser(this.getResources().openRawResource(R.raw.comments)); //创建解析器对象,从raw资源目录下解析comments.xml文本
mDanmakuView.setCallback(new master.flame.danmaku.controller.DrawHandler.Callback() {
@Override
public void updateTimer(DanmakuTimer timer) {
}
@Override
public void drawingFinished() {
}
@Override
public void danmakuShown(BaseDanmaku danmaku) {
}
@Override
public void prepared() {
mDanmakuView.start();
}
});
mDanmakuView.prepare(mParser, mContext);
mDanmakuView.showFPS(false); //是否显示FPS
mDanmakuView.enableDanmakuDrawingCache(true);
三、创建解析器对象
/**
* 创建解析器对象,解析输入流
* @param stream
* @return
*/
private BaseDanmakuParser createParser(InputStream stream) {
if (stream == null) {
return new BaseDanmakuParser() {
@Override
protected Danmakus parse() {
return new Danmakus();
}
};
}
ILoader loader = DanmakuLoaderFactory.create(DanmakuLoaderFactory.TAG_BILI);
try {
loader.load(stream);
} catch (IllegalDataException e) {
e.printStackTrace();
}
BaseDanmakuParser parser = new BiliDanmukuParser();
IDataSource<?> dataSource = loader.getDataSource();
parser.load(dataSource);
return parser;
}
注:
DanmakuLoaderFactory.create(DanmakuLoaderFactory.TAG_BILI) //xml解析
DanmakuLoaderFactory.create(DanmakuLoaderFactory.TAG_ACFUN) //json文件格式解析
四、自定义弹幕背景和边距
private static class BackgroundCacheStuffer extends SpannedCacheStuffer {
// 通过扩展SimpleTextCacheStuffer或SpannedCacheStuffer个性化你的弹幕样式
final Paint paint = new Paint();
@Override
public void measure(BaseDanmaku danmaku, TextPaint paint) {
danmaku.padding = 10; // 在背景绘制模式下增加padding
super.measure(danmaku, paint);
}
@Override
public void drawBackground(BaseDanmaku danmaku, Canvas canvas, float left, float top) {
paint.setColor(0x8125309b); //弹幕背景颜色
canvas.drawRect(left + 2, top + 2, left + danmaku.paintWidth - 2, top + danmaku.paintHeight - 2, paint);
}
@Override
public void drawStroke(BaseDanmaku danmaku, String lineText, Canvas canvas, float left, float top, Paint paint) {
// 禁用描边绘制
}
}
五、添加文本弹幕
private void addDanmaku(boolean islive) {
BaseDanmaku danmaku = mContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL);
if (danmaku == null || mDanmakuView == null) {
return;
}
danmaku.text = "这是一条弹幕" + System.nanoTime();
danmaku.padding = 5;
danmaku.priority = 0; //0 表示可能会被各种过滤器过滤并隐藏显示 //1 表示一定会显示, 一般用于本机发送的弹幕
danmaku.isLive = islive; //是否是直播弹幕
danmaku.time = mDanmakuView.getCurrentTime() + 1200; //显示时间
danmaku.textSize = 25f * (mParser.getDisplayer().getDensity() - 0.6f);
danmaku.textColor = Color.RED;
danmaku.textShadowColor = Color.WHITE; //阴影/描边颜色
danmaku.borderColor = Color.GREEN; //边框颜色,0表示无边框
mDanmakuView.addDanmaku(danmaku);
}
六、添加图文混排弹幕
private void addDanmaKuShowTextAndImage(boolean islive) {
BaseDanmaku danmaku = mContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL);
Drawable drawable = getResources().getDrawable(R.drawable.ic_launcher);
drawable.setBounds(0, 0, 100, 100);
SpannableStringBuilder spannable = createSpannable(drawable);
danmaku.text = spannable;
danmaku.padding = 5;
danmaku.priority = 1; // 一定会显示, 一般用于本机发送的弹幕
danmaku.isLive = islive;
danmaku.time = mDanmakuView.getCurrentTime() + 1200;
danmaku.textSize = 25f * (mParser.getDisplayer().getDensity() - 0.6f);
danmaku.textColor = Color.RED;
danmaku.textShadowColor = 0; // 重要:如果有图文混排,最好不要设置描边(设textShadowColor=0),否则会进行两次复杂的绘制导致运行效率降低
danmaku.underlineColor = Color.GREEN;
mDanmakuView.addDanmaku(danmaku);
}
/**
* 创建图文混排模式
* @param drawable
* @return
*/
private SpannableStringBuilder createSpannable(Drawable drawable) {
String text = "bitmap";
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(text);
ImageSpan span = new ImageSpan(drawable);//ImageSpan.ALIGN_BOTTOM);
spannableStringBuilder.setSpan(span, 0, text.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
spannableStringBuilder.append("图文混排");
spannableStringBuilder.setSpan(new BackgroundColorSpan(Color.parseColor("#8A2233B1")), 0, spannableStringBuilder.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
return spannableStringBuilder;
}
七、弹幕的隐藏/显示,暂停/继续
mDanmakuView.hide();
mDanmakuView.show();
//暂停
if (mDanmakuView != null && mDanmakuView.isPrepared()) {
mDanmakuView.pause();
}
//继续
if (mDanmakuView != null && mDanmakuView.isPrepared() && mDanmakuView.isPaused()) {
mDanmakuView.resume();
}
八、弹幕的定时发送
Boolean b = (Boolean) mBtnSendDanmakus.getTag();
timer.cancel();
if (b == null || !b) {
mBtnSendDanmakus.setText(R.string.cancel_sending_danmakus);
timer = new Timer();
timer.schedule(new AsyncAddTask(), 0, 1000);
mBtnSendDanmakus.setTag(true);
} else {
mBtnSendDanmakus.setText(R.string.send_danmakus);
mBtnSendDanmakus.setTag(false);
}
Timer timer = new Timer();
class AsyncAddTask extends TimerTask {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
addDanmaku(true);
SystemClock.sleep(20);
}
}
}
九、创建图文混排的填充适配器
private BaseCacheStuffer.Proxy mCacheStufferAdapter = new BaseCacheStuffer.Proxy() {
private Drawable mDrawable;
/**
* 在弹幕显示前使用新的text,使用新的text
* @param danmaku
* @param fromWorkerThread 是否在工作(非UI)线程,在true的情况下可以做一些耗时操作(例如更新Span的drawblae或者其他IO操作)
* @return 如果不需重置,直接返回danmaku.text
*/
@Override
public void prepareDrawing(final BaseDanmaku danmaku, boolean fromWorkerThread) {
if (danmaku.text instanceof Spanned) { // 根据你的条件检查是否需要需要更新弹幕
// FIXME 这里只是简单启个线程来加载远程url图片,请使用你自己的异步线程池,最好加上你的缓存池
new Thread() {
@Override
public void run() {
String url = "http://www.bilibili.com/favicon.ico";
InputStream inputStream = null;
Drawable drawable = mDrawable;
if (drawable == null) {
try {
URLConnection urlConnection = new URL(url).openConnection();
inputStream = urlConnection.getInputStream();
drawable = BitmapDrawable.createFromStream(inputStream, "bitmap");
mDrawable = drawable;
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(inputStream);
}
}
if (drawable != null) {
drawable.setBounds(0, 0, 100, 100);
SpannableStringBuilder spannable = createSpannable(drawable);
danmaku.text = spannable;
if (mDanmakuView != null) {
mDanmakuView.invalidateDanmaku(danmaku, false);
}
return;
}
}
}.start();
}
}
@Override
public void releaseResource(BaseDanmaku danmaku) {
// TODO 重要:清理含有ImageSpan的text中的一些占用内存的资源 例如drawable
}
};
十、退出时释放资源
@Override
protected void onDestroy() {
super.onDestroy();
if (mDanmakuView != null) {
// dont forget release!
mDanmakuView.release();
mDanmakuView = null;
}
}