文章目录
一、前言
Android上开源弹幕解析绘制引擎项目。
GitHub 地址:DanmakuFlameMaster
二、功能
-
使用多种方式(View/SurfaceView/TextureView)实现高效绘制
-
B站xml弹幕格式解析
-
基础弹幕精确还原绘制
-
支持mode7特殊弹幕
-
多核机型优化,高效的预缓存机制
-
支持多种显示效果选项实时切换
-
实时弹幕显示支持
-
换行弹幕支持/运动弹幕支持
-
支持自定义字体
-
支持多种弹幕参数设置
-
支持多种方式的弹幕屏蔽
三、实例
1. 效果图
2. 提前准备
- 在 build.gradle 中添加如下依赖:
repositories {
jcenter()
}
--------------------------------------------------------------
dependencies {
//弹幕
implementation 'com.github.ctiao:DanmakuFlameMaster:0.9.25'
implementation 'com.github.ctiao:ndkbitmap-armv7a:0.9.21'
}
- 弹幕数据文件
可以在 GitHub 上下在资源:
3. 布局文件
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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" android:background="@mipmap/ic_main_bg" tools:context=".MainActivity">
<VideoView android:id="@+id/videoview" android:visibility="gone" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@mipmap/ic_main_bg"/>
<ImageView android:src="@mipmap/ic_huake" android:layout_width="240dp" android:layout_height="120dp"/>
<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="match_parent" layout="@layout/media_controller" />
<Button android:id="@+id/btn_lottery_start" android:layout_width="200dp" android:layout_height="40dp" android:textSize="24sp" android:layout_gravity="center|bottom" android:layout_marginBottom="40dp" android:textColor="@color/white" android:background="@drawable/xui_config_bg_blue_btn" android:text="开始抽奖" />
<Button android:id="@+id/btn_lottery_stop" android:layout_width="200dp" android:layout_height="40dp" android:textSize="24sp" android:layout_gravity="center|bottom" android:layout_marginBottom="40dp" android:textColor="@color/white" android:visibility="gone" android:background="@drawable/xui_config_bg_blue_btn" android:text="停止抽奖" />
<com.hkt.hklottery.widget.BottomItemView android:id="@+id/bottom_exit" android:layout_width="79dp" android:layout_height="wrap_content" android:layout_gravity="bottom" android:layout_margin="20dp" app:bt_img_width="38dp" app:bt_img_height="38dp" app:bt_img_src="@mipmap/more_exit" app:bt_txt_text="退出"/>
</FrameLayout>
4. 初始化弹幕数据
private void initDanmaku() {
// 设置最大显示行数
HashMap<Integer, Integer> maxLinesPair = new HashMap<Integer, Integer>();
maxLinesPair.put(BaseDanmaku.TYPE_SCROLL_RL, 10); // 滚动弹幕最大显示5行
// 设置是否禁止重叠
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)
.setCacheStuffer(new SpannedCacheStuffer(), mCacheStufferAdapter) // 图文混排使用SpannedCacheStuffer
// .setCacheStuffer(new BackgroundCacheStuffer()) // 绘制背景使用BackgroundCacheStuffer
.setMaximumLines(maxLinesPair)
.preventOverlapping(overlappingEnablePair).setDanmakuMargin(40);
if (mDanmakuView != null) {
mParser = createParser(this.getResources().openRawResource(R.raw.comments));
mDanmakuView.setCallback(new DrawHandler.Callback() {
@Override
public void updateTimer(DanmakuTimer timer) {
}
@Override
public void drawingFinished() {
}
@Override
public void danmakuShown(BaseDanmaku danmaku) {
// Log.d("DFM", "danmakuShown(): text=" + danmaku.text);
}
@Override
public void prepared() {
loadData();
mDanmakuView.start();
}
});
mDanmakuView.setOnDanmakuClickListener(new IDanmakuView.OnDanmakuClickListener() {
@Override
public boolean onDanmakuClick(IDanmakus danmakus) {
Log.d("DFM", "onDanmakuClick: danmakus size:" + danmakus.size());
BaseDanmaku latest = danmakus.last();
if (null != latest) {
Log.d("DFM", "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) {
// mMediaController.setVisibility(View.VISIBLE);
return false;
}
});
mDanmakuView.prepare(mParser, mContext);
mDanmakuView.showFPS(false);
mDanmakuView.enableDanmakuDrawingCache(true);
}
}
5. 创建解析器对象
/** * Created on 2022/8/17 11:20 * * @author Gong Youqiang */
public class HuaKeDanmukuParser extends BaseDanmakuParser {
static {
System.setProperty("org.xml.sax.driver", "org.xmlpull.v1.sax2.Driver");
}
protected float mDispScaleX;
protected float mDispScaleY;
@Override
public Danmakus parse() {
if (mDataSource != null) {
AndroidFileSource source = (AndroidFileSource) mDataSource;
try {
XMLReader xmlReader = XMLReaderFactory.createXMLReader();
XmlContentHandler contentHandler = new XmlContentHandler();
xmlReader.setContentHandler(contentHandler);
xmlReader.parse(new InputSource(source.data()));
return contentHandler.getResult();
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
public class XmlContentHandler extends DefaultHandler {
private static final String TRUE_STRING = "true";
public Danmakus result;
public BaseDanmaku item = null;
public boolean completed = false;
public int index = 0;
public Danmakus getResult() {
return result;
}
@Override
public void startDocument() throws SAXException {
result = new Danmakus(ST_BY_TIME, false, mContext.getBaseComparator());
}
@Override
public void endDocument() throws SAXException {
completed = true;
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes)
throws SAXException {
String tagName = localName.length() != 0 ? localName : qName;
tagName = tagName.toLowerCase(Locale.getDefault()).trim();
if (tagName.equals("d")) {
// <d p="23.826000213623,1,25,16777215,1422201084,0,057075e9,757076900">我从未见过如此厚颜无耻之猴</d>
// 0:时间(弹幕出现时间)
// 1:类型(1从右至左滚动弹幕|6从左至右滚动弹幕|5顶端固定弹幕|4底端固定弹幕|7高级弹幕|8脚本弹幕)
// 2:字号
// 3:颜色
// 4:时间戳 ?
// 5:弹幕池id
// 6:用户hash
// 7:弹幕id
String pValue = attributes.getValue("p");
// parse p value to danmaku
String[] values = pValue.split(",");
if (values.length > 0) {
long time = (long) (parseFloat(values[0]) * 1000); // 出现时间
int type = parseInteger(values[1]); // 弹幕类型
float textSize = parseFloat(values[2]); // 字体大小
int color = (int) ((0x00000000ff000000 | parseLong(values[3])) & 0x00000000ffffffff); // 颜色
// int poolType = parseInteger(values[5]); // 弹幕池类型(忽略
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;
}
}
}
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
if (item != null && item.text != null) {
if (item.duration != null) {
String tagName = localName.length() != 0 ? localName : qName;
if (tagName.equalsIgnoreCase("d")) {
item.setTimer(mTimer);
item.flags = mContext.mGlobalFlagValues;
Object lock = result.obtainSynchronizer();
synchronized (lock) {
result.addItem(item);
}
}
}
item = null;
}
}
@Override
public void characters(char[] ch, int start, int length) {
if (item != null) {
DanmakuUtils.fillText(item, decodeXmlString(new String(ch, start, length)));
item.index = index++;
// initial specail danmaku data
String text = String.valueOf(item.text).trim();
if (item.getType() == BaseDanmaku.TYPE_SPECIAL && text.startsWith("[")
&& text.endsWith("]")) {
//text = text.substring(1, text.length() - 1);
String[] textArr = null;//text.split(",", -1);
try {
JSONArray jsonArray = new JSONArray(text);
textArr = new String[jsonArray.length()];
for (int i = 0; i < textArr.length; i++) {
textArr[i] = jsonArray.getString(i);
}
} catch (JSONException e) {
e.printStackTrace();
}
if (textArr == null || textArr.length < 5 || TextUtils.isEmpty(textArr[4])) {
item = null;
return;
}
DanmakuUtils.fillText(item, textArr[4]);
float beginX = parseFloat(textArr[0]);
float beginY = parseFloat(textArr[1]);
float endX = beginX;
float endY = beginY;
String[] alphaArr = textArr[2].split("-");
int beginAlpha = (int) (AlphaValue.MAX * parseFloat(alphaArr[0]));
int endAlpha = beginAlpha;
if (alphaArr.length > 1) {
endAlpha = (int) (AlphaValue.MAX * parseFloat(alphaArr[1]));
}
long alphaDuraion = (long) (parseFloat(textArr[3]) * 1000);
long translationDuration = alphaDuraion;
long translationStartDelay = 0;
float rotateY = 0, rotateZ = 0;
if (textArr.length >= 7) {
rotateZ = parseFloat(textArr[5]);
rotateY = parseFloat(textArr[6]);
}
if (textArr.length >= 11) {
endX = parseFloat(textArr[7]);
endY = parseFloat(textArr[8]);
if (!"".equals(textArr[9])) {
translationDuration = parseInteger(textArr[9]);
}
if (!"".equals(textArr[10])) {
translationStartDelay = (long) (parseFloat(textArr[10]));
}
}
if (isPercentageNumber(textArr[0])) {
beginX *= DanmakuFactory.BILI_PLAYER_WIDTH;
}
if (isPercentageNumber(textArr[1])) {
beginY *= DanmakuFactory.BILI_PLAYER_HEIGHT;
}
if (textArr.length >= 8 && isPercentageNumber(textArr[7])) {
endX *= DanmakuFactory.BILI_PLAYER_WIDTH;
}
if (textArr.length >= 9 && isPercentageNumber(textArr[8])) {
endY *= DanmakuFactory.BILI_PLAYER_HEIGHT;
}
item.duration = new Duration(alphaDuraion);
item.rotationZ = rotateZ;
item.rotationY = rotateY;
mContext.mDanmakuFactory.fillTranslationData(item, beginX,
beginY, endX, endY, translationDuration, translationStartDelay, mDispScaleX, mDispScaleY);
mContext.mDanmakuFactory.fillAlphaData(item, beginAlpha, endAlpha, alphaDuraion);
if (textArr.length >= 12) {
// 是否有描边
if (!TextUtils.isEmpty(textArr[11]) && TRUE_STRING.equalsIgnoreCase(textArr[11])) {
item.textShadowColor = Color.TRANSPARENT;
}
}
if (textArr.length >= 13) {
//TODO 字体 textArr[12]
}
if (textArr.length >= 14) {
// Linear.easeIn or Quadratic.easeOut
((SpecialDanmaku) item).isQuadraticEaseOut = ("0".equals(textArr[13]));
}
if (textArr.length >= 15) {
// 路径数据
if (!"".equals(textArr[14])) {
String motionPathString = textArr[14].substring(1);
if (!TextUtils.isEmpty(motionPathString)) {
String[] pointStrArray = motionPathString.split("L");
if (pointStrArray.length > 0) {
float[][] points = new float[pointStrArray.length][2];
for (int i = 0; i < pointStrArray.length; i++) {
String[] pointArray = pointStrArray[i].split(",");
if (pointArray.length >= 2) {
points[i][0] = parseFloat(pointArray[0]);
points[i][1] = parseFloat(pointArray[1]);
}
}
mContext.mDanmakuFactory.fillLinePathData(item, points, mDispScaleX,
mDispScaleY);
}
}
}
}
}
}
}
private String decodeXmlString(String title) {
if (title.contains("&")) {
title = title.replace("&", "&");
}
if (title.contains(""")) {
title = title.replace(""", "\"");
}
if (title.contains(">")) {
title = title.replace(">", ">");
}
if (title.contains("<")) {
title = title.replace("<", "<");
}
return title;
}
}
private boolean isPercentageNumber(String number) {
//return number >= 0f && number <= 1f;
return number != null && number.contains(".");
}
private float parseFloat(String floatStr) {
try {
return Float.parseFloat(floatStr);
} catch (NumberFormatException e) {
return 0.0f;
}
}
private int parseInteger(String intStr) {
try {
return Integer.parseInt(intStr);
} catch (NumberFormatException e) {
return 0;
}
}
private long parseLong(String longStr) {
try {
return Long.parseLong(longStr);
} catch (NumberFormatException e) {
return 0;
}
}
@Override
public BaseDanmakuParser setDisplayer(IDisplayer disp) {
super.setDisplayer(disp);
mDispScaleX = mDispWidth / DanmakuFactory.BILI_PLAYER_WIDTH;
mDispScaleY = mDispHeight / DanmakuFactory.BILI_PLAYER_HEIGHT;
return this;
}
}
6. 添加文本弹幕
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);
}
7. 添加图文混排弹幕
private void addDanmaKuShowTextAndImage(boolean islive) {
BaseDanmaku danmaku = mContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL);
Drawable drawable = getResources().getDrawable(R.mipmap.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.setTime(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);
}
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;
}
8. 弹幕的隐藏/显示,暂停/继续
mDanmakuView.hide();
mDanmakuView.show();
//暂停
if (mDanmakuView != null && mDanmakuView.isPrepared()) {
mDanmakuView.pause();
}
//继续
if (mDanmakuView != null && mDanmakuView.isPrepared() && mDanmakuView.isPaused()) {
mDanmakuView.resume();
}
9. 释放资源
@Override
protected void onDestroy() {
super.onDestroy();
if (mDanmakuView != null) {
// dont forget release!
mDanmakuView.release();
mDanmakuView = null;
}
}
说明:更多使用请参考 sample
今天的文章安卓开源代码库_开源代码网站github分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/85384.html