Android直播间的多种弹幕效果,集成B站弹幕源码
可自定义多种样式,B站是提供了一个全局属性定义“缓存填充器”,这个“缓存填充器”可指定纯文本样式,或图文混排样式
实际上我们的需求可能会要求多种弹幕样式要求,那么就需要我们自定义这个缓存填充器样式了,定义多种样式。
以下是写的大概3种样式,没有具体的封装和扩展,这个根据自己需要看是否需要要进行封装啦。
先看下效果:
public class CustomActivity extends Activity {
//弹幕控件
private DanmakuSurfaceView mDanmakuView;
//弹幕的上下文
private DanmakuContext mContext;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_boke_stuff);
initDanmuKu();
}
private void initDanmuKu() {
//初始化控件
mDanmakuView = findViewById(R.id.dv);
//对SurfaceView的设置,否则是全黑不透明的
mDanmakuView.setZOrderOnTop(true);
mDanmakuView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
//设置最大显示行数
HashMap<Integer, Integer> maxLInesPair = new HashMap<>(16);
maxLInesPair.put(BaseDanmaku.TYPE_SCROLL_RL, 3);
//设置是否禁止重叠
HashMap<Integer, Boolean> overlappingEnablePair = new HashMap<>(16);
overlappingEnablePair.put(BaseDanmaku.TYPE_SCROLL_RL, true);
overlappingEnablePair.put(BaseDanmaku.TYPE_FIX_TOP, true);
//创建弹幕上下文
mContext = DanmakuContext.create();
//设置一些相关的配置
mContext.setDuplicateMergingEnabled(false)
//是否重复合并
.setScrollSpeedFactor(1.5f)
//设置文字的比例
.setScaleTextSize(1.2f)
//图文混排的时候使用!
.setCacheStuffer(new CustomDanmuCacheStuff(this), mBackgroundCacheStuffer)
//设置显示最大行数
.setMaximumLines(maxLInesPair)
//设置防,null代表可以重叠
.preventOverlapping(overlappingEnablePair).setDanmakuMargin(DensityUtil.dip2px(BokeActivity.this,3));
//设置解析器
if (mDanmakuView != null) {
BaseDanmakuParser defaultDanmakuParser = getDefaultDanmakuParser();
//相应的回掉
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(defaultDanmakuParser, mContext);
mDanmakuView.enableDanmakuDrawingCache(true);
}
}
private BaseCacheStuffer.Proxy mBackgroundCacheStuffer = new BaseCacheStuffer.Proxy() {
@Override
public void prepareDrawing(BaseDanmaku danmaku, boolean fromWorkerThread) {
// 根据你的条件检查是否需要需要更新弹幕
}
@Override
public void releaseResource(BaseDanmaku danmaku) {
//清理相应的数据
danmaku.tag = null;
}
};
//添加图片类型的弹幕
public void addImageType(View view){
loadImage("https://profile.csdnimg.cn/4/7/B/1_q2368465644",
"http://img4.imgtn.bdimg.com/it/u=3764728511,3721048833&fm=26&gp=0.jpg");
}
//去下载图片
private void loadImage(String avatarString, final String iconString) {
Glide.with(BokeActivity.this).asBitmap().load(avatarString)
.into(new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(Bitmap resource, Transition<? super Bitmap> transition) {
if (resource == null) {
resource = BitmapFactory.decodeResource(getResources(), R.drawable.avatar_default);
}
loadTwoImage(resource, iconString);
}
@Override
public void onLoadFailed(Drawable errorDrawable) {
Bitmap resource = BitmapFactory.decodeResource(getResources(), R.drawable.avatar_default);
loadTwoImage(resource, iconString);
}
});
}
private void loadTwoImage(final Bitmap avatarBitmap, final String iconString) {
Glide.with(BokeActivity.this).asBitmap().load(iconString)
.into(new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(Bitmap resource, Transition<? super Bitmap> transition) {
if (resource == null) {
resource = BitmapFactory.decodeResource(getResources(), R.drawable.live_icon_header);
}
addImageTypeDanmu(avatarBitmap, resource);
}
@Override
public void onLoadFailed(Drawable errorDrawable) {
Bitmap resource = BitmapFactory.decodeResource(getResources(), R.drawable.live_icon_header);
addImageTypeDanmu(avatarBitmap, resource);
}
});
}
//添加图片类型弹幕
private void addImageTypeDanmu(Bitmap userimage, Bitmap iconbitmap){
BaseDanmaku danmaku = mContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL);
if (danmaku == null || mDanmakuView == null) {
return;
}
Map<String, Object> map = new HashMap<>(16);
map.put("type", "imageType");
map.put("name", "小纠结");
map.put("content", "在绝地求生房间送出礼物");
map.put("bgbitmap", BitmapFactory.decodeResource(getResources(), R.drawable.live_danmu_bg_one));
map.put("userimage", userimage);
map.put("iconbitmap", iconbitmap);
danmaku.tag = map;
danmaku.text = "";
danmaku.priority = 0;
//是否是直播弹幕
danmaku.setTime(mDanmakuView.getCurrentTime() + 1200);
mDanmakuView.addDanmaku(danmaku);
}
//添加纯文本弹幕 - 自己的
public void addTextType(View view){
String name = "小纠结";
String content = "艾瑞巴蒂666";
BaseDanmaku danmaku = mContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL,mContext);
if (danmaku == null || mDanmakuView == null) {
return;
}
Map<String, Object> map = new HashMap<>(16);
map.put("type", "textNameType");
map.put("content", content);
map.put("name", name);
map.put("shadowLayer", true);
danmaku.tag = map;
danmaku.text = name + content;
danmaku.setTime(mDanmakuView.getCurrentTime());
mDanmakuView.addDanmaku(danmaku);
}
//添加纯文本弹幕 - 他人的
public void addTextTypeOther(View view){
String name = "豆豆儿";
String content = "吉利服吉利服吉利服空投空投SKS";
BaseDanmaku danmaku = mContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL,mContext);
if (danmaku == null || mDanmakuView == null) {
return;
}
Map<String, Object> map = new HashMap<>(16);
map.put("type", "textNameType");
map.put("content", content);
map.put("name", name);
map.put("shadowLayer", false);
danmaku.tag = map;
danmaku.text = name + content;
danmaku.setTime(mDanmakuView.getCurrentTime());
mDanmakuView.addDanmaku(danmaku);
}
public static BaseDanmakuParser getDefaultDanmakuParser() {
return new BaseDanmakuParser() {
@Override
protected IDanmakus parse() {
return new Danmakus();
}
};
}
//清空屏幕上的弹幕
public void clear(View view){
if (mDanmakuView != null) {
mDanmakuView.clearDanmakusOnScreen();
}
}
//释放弹幕
public void release(View view){
if (mDanmakuView != null) {
mDanmakuView.release();
}
}
@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;
}
}
}
以上是我们集成B站的属性初始化,以及弹幕内容的解析(拼接)。接下来就是把这些内容绘制出来了。
public class CustomDanmuCacheStuff extends BaseCacheStuffer {
//文本弹幕
private float TEXTDANMU_HEIGHT;
private float TEXTDANMU_PADDINGLEFT;
private float TEXTDANMU_PADDINGTOP;
private float TEXTDANMU_BGHEIGHT;
//图片类型弹幕
private float IMAGETYPE_HEIGHT;
private float IMAGETYPE_PADDING_TOP;
private float IMAGETYPE_PADDING_LEFT;
private float IMAGETYPE_IMAGE_SIZE;
private float IMAGETYPE_MARGIN_NAME; //name左间距
private float IMAGETYPE_MARGIN;
//字体大小 弹幕所有字体大小一致
private float DANMU_TEXT_SIZE;
private float DANMU_STROKE_WIDTH;
//文字描边radius
private float SHADOW_RADIUS = 3.0f;
private float SHADOW_DXY = 1f;
private String SHADOW_COLOR = "#99000000";
//边框
private float SHADOW_BGRADIUS = 12f;
private float SHADOW_BGDXY = 0.5f;
private String SHADOW_BGCOLOR = "#FFF974";
private Context mContext;
//private Typeface mTypefaceflty;
public BokeDanmuCacheStuff(Context context) {
this.mContext = context;
//纯文本类型弹幕 初始化
TEXTDANMU_HEIGHT = DensityUtil.dip2px(mContext, 25);
TEXTDANMU_PADDINGLEFT = DensityUtil.dip2px(mContext, 12);
TEXTDANMU_PADDINGTOP = DensityUtil.dip2px(mContext, 3);
TEXTDANMU_BGHEIGHT = DensityUtil.dip2px(mContext, 24);
DANMU_STROKE_WIDTH = DensityUtil.dip2px(mContext, 1);
DANMU_TEXT_SIZE = DensityUtil.dip2px(mContext, 16);
//指定字体
//mTypefaceflty = Typeface.createFromAsset(mContext.getAssets(), "fonnts/xxxx.TTF");
//图片类型弹幕 初始化
IMAGETYPE_HEIGHT = DensityUtil.dip2px(mContext, 33);
IMAGETYPE_PADDING_TOP = DensityUtil.dip2px(mContext, 4);
IMAGETYPE_PADDING_LEFT = DensityUtil.dip2px(mContext, 6);
IMAGETYPE_IMAGE_SIZE = DensityUtil.dip2px(mContext, 24);
IMAGETYPE_MARGIN_NAME = DensityUtil.dip2px(mContext, 7);
IMAGETYPE_MARGIN = DensityUtil.dip2px(mContext, 10);
}
@Override
public void measure(BaseDanmaku danmaku, TextPaint paint, boolean fromWorkerThread) {
// 初始化数据
Map<String, Object> map = (Map<String, Object>) danmaku.tag;
if (map == null) return;
String type = (String) map.get("type");
if (type.equals("imageType")) {
measureImageType(danmaku, paint, map);
} else if (type.equals("textNameType")) {
measureTextName(danmaku, paint, map);
}
}
//指定要测量的弹幕的宽高 - 有图片的弹幕类型
private void measureImageType(BaseDanmaku danmaku, TextPaint paint, Map<String, Object> map){
String username = (String) map.get("name");
String content = (String) map.get("content");
Bitmap bgbitmap = (Bitmap) map.get("bgbitmap");
//画笔
paint.setTextSize(DANMU_TEXT_SIZE);
//指定字体
//paint.setTypeface(mTypefaceflty);
float usernameLength = paint.measureText(username);
float contentLength = paint.measureText(content);
//弹幕区域的宽度
danmaku.paintWidth = Math.max(IMAGETYPE_PADDING_LEFT + IMAGETYPE_IMAGE_SIZE +
IMAGETYPE_MARGIN_NAME + usernameLength + contentLength +
IMAGETYPE_IMAGE_SIZE +
IMAGETYPE_MARGIN, bgbitmap.getWidth());
//弹幕区域的高度
danmaku.paintHeight = IMAGETYPE_HEIGHT;
}
//指定要测量的弹幕的宽高 - 纯文本的弹幕类型 可指定不同内容不同色值;区分本人与他人
private void measureTextName(BaseDanmaku danmaku, TextPaint paint, Map<String, Object> map){
String userName = (String) map.get("name");
String content = (String) map.get("content");
//画笔
paint.setTextSize(DANMU_TEXT_SIZE);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(DANMU_STROKE_WIDTH);
//指定字体
//paint.setTypeface(mTypefaceflty);
float usernameLength = paint.measureText(userName);
float contentLength = paint.measureText(content);
//弹幕区域的宽度
danmaku.paintWidth = TEXTDANMU_PADDINGLEFT + usernameLength + contentLength + TEXTDANMU_PADDINGLEFT;
//弹幕区域的高度
danmaku.paintHeight = TEXTDANMU_HEIGHT;
}
@Override
public void clearCaches() {
}
@Override
public void drawDanmaku(BaseDanmaku danmaku, Canvas canvas, float left, float top, boolean fromWorkerThread, AndroidDisplayer.DisplayerConfig displayerConfig) {
Map<String, Object> map = (Map<String, Object>) danmaku.tag;
if (map == null) return;
String type = (String) map.get("type");
if (type.equals("imageType")) {
drawImageType(canvas, left, top, map);
} else if (type.equals("textNameType")) {
drawaTextNameType(canvas, left, top, map);
}
}
private void drawImageType(Canvas canvas, float left, float top, Map<String, Object> map) {
String username = (String) map.get("name");
String content = (String) map.get("content");
Bitmap bgbitmap = (Bitmap) map.get("bgbitmap");
Bitmap userimage = (Bitmap) map.get("userimage");
Bitmap iconbitmap = (Bitmap) map.get("iconbitmap");
//画笔
Paint paint = new Paint();
paint.setTextSize(DANMU_TEXT_SIZE);
//指定字体
//paint.setTypeface(mTypefaceflty);
//绘制背景
canvas.drawBitmap(bgbitmap, left, top, paint);
//绘制头像
float avatorLeft = left + IMAGETYPE_PADDING_LEFT;
float avatorTop = top + IMAGETYPE_PADDING_TOP;
float avatorRight = left + IMAGETYPE_PADDING_LEFT + IMAGETYPE_IMAGE_SIZE;
float avatorBottom = top + IMAGETYPE_PADDING_TOP + IMAGETYPE_IMAGE_SIZE;
canvas.drawBitmap(userimage, null, new RectF(avatorLeft, avatorTop, avatorRight, avatorBottom), paint);
//绘制name,内容
paint.setColor(Color.WHITE);
paint.setStyle(Paint.Style.FILL);
float usernameLength = paint.measureText(username);
float contentLength = paint.measureText(content);
//内容
float contentLeft = left + IMAGETYPE_IMAGE_SIZE + IMAGETYPE_MARGIN_NAME + IMAGETYPE_MARGIN_NAME + usernameLength;
//计算文字的相应偏移量
Paint.FontMetrics fontMetrics = paint.getFontMetrics();
//为基线到字体上边框的距离
float textTop = fontMetrics.top;
//为基线到字体下边框的距离
float textBottom = fontMetrics.bottom;
float contentBottom = top + IMAGETYPE_HEIGHT / 2;
//基线中间点的y轴计算公式
int baseLineY = (int) (contentBottom - textTop / 2 - textBottom / 2);
//绘制内容
paint.setColor(Color.parseColor("#FFFFFF"));
canvas.drawText(content, contentLeft, baseLineY, paint);
//绘制用户名字
paint.setColor(Color.parseColor("#000000"));
canvas.drawText(username, contentLeft - usernameLength, baseLineY, paint);
float imageLeft = left + IMAGETYPE_IMAGE_SIZE + IMAGETYPE_MARGIN_NAME + IMAGETYPE_MARGIN_NAME + usernameLength + contentLength;
float imageTop = top + IMAGETYPE_PADDING_TOP;
float imageRight = imageLeft + IMAGETYPE_IMAGE_SIZE;
float imageBottom = imageTop + IMAGETYPE_IMAGE_SIZE;
canvas.drawBitmap(iconbitmap, null, new RectF(imageLeft, imageTop, imageRight, imageBottom), paint);
}
private void drawaTextNameType(Canvas canvas, float left, float top, Map<String, Object> map) {
String userName = (String) map.get("name");
String content = (String) map.get("content");
boolean isShadowLayer = (boolean) map.get("shadowLayer");
Paint paint = new Paint();
paint.setTextSize(DANMU_TEXT_SIZE);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(DANMU_STROKE_WIDTH);
//指定字体
//paint.setTypeface(mTypefaceflty);
float usernameLength = paint.measureText(userName);
float contentLength = paint.measureText(content);
if (isShadowLayer) {
//背景边框
paint.setColor(Color.parseColor("#3780C9"));
paint.setShadowLayer(SHADOW_BGRADIUS, SHADOW_BGDXY, SHADOW_BGDXY, Color.parseColor(SHADOW_COLOR));
//获取宽度
float rectBgLeft = left + DANMU_STROKE_WIDTH;
float rectBgTop = top + DANMU_STROKE_WIDTH;
float rectBgRight = left + usernameLength + contentLength + TEXTDANMU_PADDINGLEFT + TEXTDANMU_PADDINGLEFT;
float rectBgBottom = top + TEXTDANMU_BGHEIGHT;
canvas.drawRoundRect(new RectF(rectBgLeft, rectBgTop, rectBgRight, rectBgBottom), TEXTDANMU_HEIGHT / 2, TEXTDANMU_HEIGHT / 2, paint);
}
//绘制弹幕内容
paint.setColor(Color.parseColor("#3780C9"));
paint.setStyle(Paint.Style.FILL);
paint.setShadowLayer(SHADOW_RADIUS, SHADOW_DXY, SHADOW_DXY, Color.parseColor(SHADOW_COLOR));
float contentLeft = left + usernameLength + TEXTDANMU_PADDINGLEFT;
//计算文字的相应偏移量
Paint.FontMetrics fontMetrics = paint.getFontMetrics();
//为基线到字体上边框的距离,即上图中的top
float textTop = fontMetrics.top;
//为基线到字体下边框的距离,即上图中的bottom
float textBottom = fontMetrics.bottom;
float contentBottom = top + TEXTDANMU_HEIGHT / 2;
//基线中间点的y轴计算公式
int baseLineY = (int) (contentBottom - textTop / 2 - textBottom / 2);
//绘制文字
canvas.drawText(content, contentLeft, baseLineY, paint);
//绘制name
paint.setColor(Color.parseColor("#29C9FF"));
canvas.drawText(userName, contentLeft - usernameLength, baseLineY, paint);
}
}
总结:
1.B站提供的弹幕View有DanmakuView和DanmakuSurfaceView
DanmakuSurfaceView需要设置
mDanmakuView.setZOrderOnTop(true);
mDanmakuView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
否则SurfaceView背景将不会是透明的
如果指定弹幕现在在某个布局view的下方,建议是使用DanmakuView
2.danmaku.priority 弹幕优先显示属性
如果设置了这个属性,如果瞬间属于优先显示的弹幕很多,将不会受之前设置的最大显示行数的限制
意思就是:最大显示行数的限制设置了3行,但优先显示的弹幕越多,那么这些弹幕将会排列很多行,直到这些优先弹幕飘完
3.danmaku.setTime 设置弹幕时间
这个建议不要设置时间太相近,因为在弹幕源码中的过滤器会将那些相同弹幕的时间给过滤了,所以在短时间内的比如100条弹幕会发现出现在屏幕上的弹幕不够100条
4.以上多种弹幕效果其实是按照你想要的弹幕效果进行绘制,
先弹幕宽高设置,将显示的内容进行排列绘制
指定字体,name及色值,内容及色值等,
区分的边框绘制等