Android弹幕框架 黑暗火焰使

著论准过秦,作赋拟子虚。这篇文章主要讲述Android弹幕框架 黑暗火焰使相关的知识,希望能为你提供帮助。
笑谈风云, 一语定乾坤。大家好, 我是皖江。
今天我将分享由BiliBili开源的android弹幕框架( DanmakuFlameMaster) 的学习经验。
【Android弹幕框架 黑暗火焰使】我是将整个框架以model的形式引入项目中的, 这样更方便的观察源码。也可以通过依赖的方式注入进来

dependencies { compile ' com.github.ctiao:DanmakuFlameMaster:0.5.3' }


先放一下我要做成的效果图:
Android弹幕框架 黑暗火焰使

文章图片


页面分析
从上图来看, 整个UI分成了三层。最下面是视频层, 中间是弹幕层, 顶层是控制层。现在市场上主流的视频直播软件大多都是这样分层的, 不同的是直播类的话, 可能还会再多一层交互层, 显示签到信息、礼物信息什么的。
既然是分层的话, 我就直接用FrameLayout帧布局来实现了。贴一下我的布局文件:

< FrameLayout xmlns:android= " http://schemas.android.com/apk/res/android" android:layout_width= " match_parent" android:layout_height= " match_parent" > < VideoView android:id= " @ + id/vv_video" android:layout_width= " match_parent" android:layout_height= " match_parent" /> < master.flame.danmaku.ui.widget.DanmakuView android:id= " @ + id/sv_danmaku" android:layout_width= " match_parent" android:layout_height= " match_parent" /> < include android:id= " @ + id/media_controller" android:layout_width= " match_parent" android:layout_height= " fill_parent" layout= " @ layout/media_controller" /> < /FrameLayout>

控制层的布局:

< FrameLayout xmlns:android= " http://schemas.android.com/apk/res/android" android:layout_width= " match_parent" android:layout_height= " match_parent" > < LinearLayout android:gravity= " center_vertical" android:layout_width= " match_parent" android:layout_height= " wrap_content" android:layout_gravity= " bottom" android:background= " #8acc22dd" > < Button android:layout_weight= " 1" android:id= " @ + id/rotate" android:layout_width= " 0dp" android:layout_height= " wrap_content" android:text= " @ string/rotate" /> < Button android:layout_width= " 0dp" android:layout_weight= " 1" android:id= " @ + id/btn_hide" android:layout_height= " wrap_content" android:text= " @ string/hide_danmaku" /> < Button android:id= " @ + id/btn_show" android:layout_width= " 0dp" android:layout_weight= " 1" android:layout_height= " wrap_content" android:text= " @ string/show_danmaku" /> < Button android:id= " @ + id/btn_pause" android:layout_width= " 0dp" android:layout_weight= " 1" android:layout_height= " wrap_content" android:text= " @ string/pause_danmaku" /> < Button android:id= " @ + id/btn_resume" android:layout_width= " 0dp" android:layout_weight= " 1" android:layout_height= " wrap_content" android:text= " @ string/resume_danmaku" /> < Button android:id= " @ + id/btn_send" android:layout_width= " 0dp" android:layout_weight= " 1" android:layout_height= " wrap_content" android:text= " @ string/send_danmaku" /> < Button android:id= " @ + id/btn_send_image_text" android:layout_width= " 0dp" android:layout_weight= " 1" android:layout_height= " wrap_content" android:text= " @ string/send_danmaku_image_text" /> < Button android:id= " @ + id/btn_send_danmakus" android:layout_width= " 0dp" android:layout_weight= " 1" android:layout_height= " wrap_content" android:text= " @ string/send_danmakus" /> < /LinearLayout> < /FrameLayout>


写完布局, 先写一个初始化的方法:

//设置最大行数 HashMap< Integer,Integer> maxLinesPair = new HashMap< > (); maxLinesPair.put(BaseDanmaku.TYPE_SCROLL_RL,5); //滚动弹幕最大显示5行 //设置是否禁止重叠 HashMap< Integer, Boolean> overlappingEnablePair = new HashMap< > (); overlappingEnablePair.put(BaseDanmaku.TYPE_SCROLL_RL, true); overlappingEnablePair.put(BaseDanmaku.TYPE_FIX_TOP, true); mDanmakuContext = DanmakuContext.create(); //初始化上下文 mDanmakuContext.setDanmakuStyle(IDisplayer.DANMAKU_STYLE_STROKEN,3); //设置弹幕类型 mDanmakuContext.setDuplicateMergingEnabled(false); //设置是否合并重复弹幕 mDanmakuContext.setScrollSpeedFactor(1.2f); //设置弹幕滚动速度 mDanmakuContext.setScaleTextSize(1.2f); //设置弹幕字体大小 mDanmakuContext.setCacheStuffer(new SpannedCacheStuffer(),mCacheStufferAdapter); //设置缓存绘制填充器 图文混排使用SpannedCacheStuffer mDanmakuContext.setMaximumLines(maxLinesPair); //设置最大行数 mDanmakuContext.preventOverlapping(overlappingEnablePair); //设置是否禁止重叠 mParser = createParser(this.getResources().openRawResource(R.raw.comments)); //加载弹幕资源文件 mDvDanmaku.prepare(mParser, mDanmakuContext); mDvDanmaku.showFPS(true); mDvDanmaku.enableDanmakuDrawingCache(true);



再贴一下绘制填充器的实现, 主要实现了将图片和文字排列在一起的效果

private SpannableStringBuilder createSpannable(Drawable drawable) { String text = " bitmap" ; SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(text); ImageSpan span = new ImageSpan(drawable); 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; }



在BaseDanmaku这个类下, 定义了弹幕的基本运动形式: TYPE_SCROLL_RL、 TYPE_SCROLL_LR、TYPE_FIX_TOP、TYPE_FIX_BOTTOM和TYPE_SPECIAL。也就是从右入左出、左入右出( 逆向弹幕) 、顶部弹幕、底部弹幕以及高级弹幕。( 除此之外还有脚本弹幕)
在初始化的时候, 需要传入一个特有的弹幕上下文DanmukuContext, 通过上下文来初始化一些设置。弹幕资源是保存在xml文件下的, 大致格式如下:
.
< i> < chatserver> chat.bilibili.com< /chatserver> < chatid> 2962351< /chatid> < mission> 0< /mission> < maxlimit> 1500< /maxlimit> < source> k-v< /source> < d p= " 145.91299438477,1,25,16777215,1422201001,0,D6673695,757075520" > 我从未见过如此厚颜无耻之人< /d> < /i>

头信息不需要太多关注, 来看看d标签下p对应参数的具体意义:
第一个: 弹幕出现的时间
第二个: 弹幕类型( 1、从右至左; 6、从左至右; 5、顶部弹幕; 4、底部弹幕; 7、高级弹幕; 8、脚本弹幕’)
第三个: 字号
第四个: 颜色
第五个: 时间戳
第六个: 弹幕池ID
第七个: 用户hash值
第八个: 弹幕id
以下是弹幕具体解析代码



/** * 从弹幕文件中提起弹幕 * @ 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); //创建一个BiliDanmakuLoader实例来加载弹幕流文件 try { loader.load(stream); } catch (IllegalDataException e) { e.printStackTrace(); } BaseDanmakuParser parser = new BiliDanmukuParser(); //弹幕解析者 IDataSource< ?> dataSource = loader.getDataSource(); parser.load(dataSource); return parser; }


具体解析方案:

String tagName = localName.length() != 0 ? localName : qName; tagName = tagName.toLowerCase(Locale.getDefault()).trim(); if (tagName.equals(" d" )) { String pValue = attributes.getValue(" p" ); // parse p value to danmaku String[] values = pValue.split(" ," ); if (values.length > 0) { long time = (long) (Float.parseFloat(values[0]) * 1000); // 出现时间 int type = Integer.parseInt(values[1]); // 弹幕类型 float textSize = Float.parseFloat(values[2]); // 字体大小 int color = Integer.parseInt(values[3]) | 0xFF000000; // 颜色 item = mContext.mDanmakuFactory.createDanmaku(type, mContext); if (item != null) { item.setTime(time); item.textSize = textSize * (mDispDensity - 0.6f); item.textColor = color; item.textShadowColor = color < = Color.BLACK ? Color.WHITE : Color.BLACK; } } }

弹幕资源加载完毕后, 就调用mDvDanmuku的prepare( ) 方法, 执行准备。当准备完毕的时候, 就会调用DrawHandler.CallBack()回调中的prepared方法。然后在这个prepared方法中正式让弹幕启动。调用顺序如下:

mDvDanmaku.prepare(mParser, mDanmakuContext); //传入解析完成的弹幕和上下文


然后执行DanmukuView下的prepare()方法

private void prepare() { if (handler = = null) handler = new DrawHandler(getLooper(mDrawingThreadType), this, mDanmakuVisible); //创建一个Handler }

通过这个Handler来实现进程间的通讯

handler.setConfig(config); handler.setParser(parser); handler.setCallback(mCallback); handler.prepare(); -》会让handler发送一个message去执行正真的准备

DrawHandler中有一个回调
public interface Callback { public void prepared(); public void updateTimer(DanmakuTimer timer); public void danmakuShown(BaseDanmaku danmaku); public void drawingFinished(); }

真正的准备

mTimeBase = SystemClock.uptimeMillis(); if (mParser = = null || !mDanmakuView.isViewReady()) {//没有准备好, 延时0.1秒后再执行 sendEmptyMessageDelayed(PREPARE, 100); } else { prepare(new Runnable() { @ Override public void run() { pausedPosition = 0; mReady = true; if (mCallback != null) { mCallback.prepared(); } } }); }

以上是弹幕View的启动调用流程。
那么, 怎么添加弹幕捏? 元芳, 你怎么看? ( TM我怎么知道我怎么看) 看下面
private void addDanmaku(boolean islive) { BaseDanmaku danmaku = mDanmakuContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL); //添加一条从右到左的滚动弹幕 if (danmaku = = null || mDvDanmaku = = null) { return; } danmaku.text = " 这是一条弹幕" + System.nanoTime(); danmaku.padding = 5; danmaku.priority = 0; // 可能会被各种过滤器过滤并隐藏显示设置为1的话, 就一定会显示适用于本机发送的弹幕 danmaku.isLive = islive; danmaku.setTime(mDvDanmaku.getCurrentTime() + 1200); danmaku.textSize = 25f * (mParser.getDisplayer().getDensity() - 0.6f); danmaku.textColor = Color.RED; //默认设置为红色字体 danmaku.textShadowColor = Color.WHITE; danmaku.borderColor = Color.GREEN; //为了区别其他弹幕与自己发的弹幕, 再给自己发的弹幕加上边框 mDvDanmaku.addDanmaku(danmaku); }


以上, 就是 黑暗火焰使基本使用方法。


    推荐阅读