Android弹幕实现:基于B站弹幕开源系统(4)-重构
弹幕在视频播放的APP中比较常见,但是逻辑比较复杂,现在在附录1,2,3的基础上,我再次对弹幕进行抽象和重构,把弹幕从底向上抽象成不同的层,便于复用。
第一步,抽象数据层。
通常弹幕的来源是来源于后台的数据接口请求,在实时直播时候,是通过网络的轮询机制获取数据,那么,我把这部分代码抽出来设计成一个MGDanmakuHttpController,该类专注于数据的获取与分发:
package zhangphil.danmaku;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.NonNull;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Callable;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.observers.DisposableObserver;
import io.reactivex.schedulers.Schedulers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
/**
* Created by Phil on 2017/3/31.
*/
public class MGDanmakuHttpController {
//private final String TAG = getClass().getName() + String.valueOf(UUID.randomUUID());
private int msgId = 0;
private DataMessageListener mDataMessageListener = null;
private OkHttpClient mOkHttpClient;
public MGDanmakuHttpController() {
mOkHttpClient = new OkHttpClient();
}
private final int WHAT_START = 0xff0a;
//private final int WHAT_STOP = WHAT_START + 1;
private boolean promise = false;
private int interval = 0;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == WHAT_START) {
handler.removeMessages(WHAT_START);
try {
if (promise)
startRequestDanmaku();
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
public void startRequestDanmaku() throws Exception {
promise = true;
Observable mObservable = Observable.fromCallable(new Callable<List<DanmakuMsg>>() {
@Override
public List<DanmakuMsg> call() throws Exception {
//同步方法返回观察者需要的数据结果
//在这里处理线程化的操作
return fetchData();
}
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
mObservable.subscribe(new DisposableObserver<List<DanmakuMsg>>() {
@Override
public void onNext(@NonNull List<DanmakuMsg> lists) {
if (mDataMessageListener != null && promise) {
mDataMessageListener.onDataMessageListener(lists);
}
}
@Override
public void onComplete() {
fireRequest();
}
@Override
public void onError(Throwable e) {
fireRequest();
}
});
}
public void stopRequestDanmaku() {
promise = false;
}
/**
* 设置轮询的间隔时间
*
* @param interval 单位毫秒 默认是0
*/
public void setHttpRequestInterval(int interval) {
this.interval = interval;
}
private void fireRequest() {
//这里将触发重启数据请求,在这里可以调节重启数据请求的节奏。
//比如可以设置一定的时延
handler.sendEmptyMessageDelayed(WHAT_START, interval);
}
private List<DanmakuMsg> fetchData() {
//同步方法返回观察者需要的数据结果
//在这里处理线程化的操作
// String url = "http://blog.csdn.net/zhangphil";
// try {
// Request request = new Request.Builder().url(url).build();
// Response response = mOkHttpClient.newCall(request).execute();
// if (response.isSuccessful()) {
// byte[] bytes = response.body().bytes();
// String data = new String(bytes, 0, bytes.length);
try {
Thread.sleep((int) (Math.random() * 500));
} catch (InterruptedException e) {
e.printStackTrace();
}
int count = (int) (Math.random() * 10);
//装配模拟数据
List<DanmakuMsg> danmakuMsgs = new ArrayList<>();
for (int i = 0; i < count; i++) {
DanmakuMsg danmakuMsg = new DanmakuMsg();
danmakuMsg.msg = String.valueOf(msgId++);
danmakuMsgs.add(danmakuMsg);
}
return danmakuMsgs;
// }
// } catch (Exception e) {
// e.printStackTrace();
// }
//
// return null;
}
public interface DataMessageListener {
void onDataMessageListener(@NonNull List<DanmakuMsg> lists);
}
public void setDataMessageListener(DataMessageListener listener) {
mDataMessageListener = listener;
}
}
第二步,通过一个模型把弹幕的view和数据用胶水粘合在一起,我写了一个MGDanmaku:
package zhangphil.danmaku;
import android.graphics.Color;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;
import master.flame.danmaku.danmaku.model.BaseDanmaku;
import master.flame.danmaku.danmaku.model.DanmakuTimer;
import master.flame.danmaku.danmaku.model.IDisplayer;
import master.flame.danmaku.danmaku.model.android.DanmakuContext;
import master.flame.danmaku.ui.widget.DanmakuView;
/**
* Created by Phil on 2017/4/1.
*/
public class MGDanmaku {
private final String TAG = getClass().getName() + UUID.randomUUID();
private MGDanmakuHttpController mMGDanmakuHttpController;
private DanmakuView mDanmakuView;
private AcFunDanmakuParser mParser;
private DanmakuContext mDanmakuContext;
private final int MAX_DANMAKU_LINES = 8; //弹幕在屏幕显示的最大行数
private ConcurrentLinkedQueue<DanmakuMsg> mQueue = null; //所有的弹幕数据存取队列,在这里做线程的弹幕取和存
private ArrayList<DanmakuMsg> danmakuLists = null;//每次请求最新的弹幕数据后缓存list
private final int WHAT_GET_LIST_DATA = 0xffab01;
private final int WHAT_DISPLAY_SINGLE_DANMAKU = 0xffab02;
/**
* 每次弹幕的各种颜色从这里面随机的选一个
*/
private final int[] colors = {
Color.RED,
Color.YELLOW,
Color.BLUE,
Color.GREEN,
Color.CYAN,
Color.DKGRAY};
//弹幕开关总控制
// true正常显示和请求
// false则取消
private boolean isDanmukuEnable = false;
public MGDanmaku(@NonNull DanmakuView view, @NonNull MGDanmakuHttpController controller) {
this.mDanmakuView = view;
this.mMGDanmakuHttpController = controller;
initDanmaku();
danmakuLists = new ArrayList<>();
mQueue = new ConcurrentLinkedQueue<>();
mMGDanmakuHttpController.setDataMessageListener(new MGDanmakuHttpController.DataMessageListener() {
@Override
public void onDataMessageListener(@NonNull List<DanmakuMsg> lists) {
danmakuLists = (ArrayList<DanmakuMsg>) lists;
//for (int i = 0; i < danmakuLists.size(); i++) {
//Log.d("获得数据", danmakuLists.get(i).msg);
//}
addListData();
}
});
Log.d(getClass().getName(), TAG);
}
private Handler mDanmakuHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case WHAT_GET_LIST_DATA:
addListData();
break;
case WHAT_DISPLAY_SINGLE_DANMAKU:
mDanmakuHandler.removeMessages(WHAT_DISPLAY_SINGLE_DANMAKU);
displayDanmaku();
break;
}
}
};
private void addListData() {
if (danmakuLists != null && !danmakuLists.isEmpty()) {
mDanmakuHandler.removeMessages(WHAT_GET_LIST_DATA);
mQueue.addAll(danmakuLists);
danmakuLists.clear();
mDanmakuHandler.sendEmptyMessage(WHAT_DISPLAY_SINGLE_DANMAKU);
}
}
private void initDanmaku() {
// 设置最大显示行数
HashMap<Integer, Integer> maxLinesPair = new HashMap<>();
maxLinesPair.put(BaseDanmaku.TYPE_SCROLL_RL, MAX_DANMAKU_LINES); // 滚动弹幕最大显示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, 10) //描边的厚度
.setDuplicateMergingEnabled(false)
.setScrollSpeedFactor(1.2f) //弹幕的速度。注意!此值越小,速度越快!值越大,速度越慢。// by phil
.setScaleTextSize(1.2f) //缩放的值
// .setCacheStuffer(new BackgroundCacheStuffer()) // 绘制背景使用BackgroundCacheStuffer
.setMaximumLines(maxLinesPair)
.preventOverlapping(overlappingEnablePair);
mParser = new AcFunDanmakuParser();
mDanmakuView.prepare(mParser, mDanmakuContext);
//mDanmakuView.showFPS(true);
mDanmakuView.enableDanmakuDrawingCache(true);
if (mDanmakuView != null) {
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) {
Log.d("弹幕文本", "显示 text=" + danmaku.text);
}
@Override
public void prepared() {
mDanmakuView.start();
}
});
}
}
/**
* 驱动弹幕显示机制重新运作起来
*/
private void startDanmaku() {
mDanmakuView.show();
//mDanmakuView.start();
mDanmakuHandler.sendEmptyMessage(WHAT_GET_LIST_DATA);
mDanmakuHandler.sendEmptyMessage(WHAT_DISPLAY_SINGLE_DANMAKU);
}
private void stopDanmaku() {
if (mDanmakuView != null) {
mDanmakuView.hide();
mDanmakuView.clearDanmakusOnScreen();
mDanmakuView.clear();
}
mDanmakuHandler.removeMessages(WHAT_GET_LIST_DATA);
mDanmakuHandler.removeMessages(WHAT_DISPLAY_SINGLE_DANMAKU);
danmakuLists.clear();
mQueue.clear();
}
public void setDanmakuRunning(boolean enable) {
//如果是重复设置,则跳过
if (isDanmukuEnable == enable) {
return;
}
this.isDanmukuEnable = enable;
//Log.d("isDanmukuEnable", String.valueOf(isDanmukuEnable));
if (isDanmukuEnable) {
startDanmaku();
try {
mMGDanmakuHttpController.startRequestDanmaku();
} catch (Exception e) {
e.printStackTrace();
}
} else {
stopDanmaku();
mMGDanmakuHttpController.stopRequestDanmaku();
}
}
public boolean getDanmakuRunning() {
return isDanmukuEnable;
}
public void sendMsg(@NonNull DanmakuMsg danmakuMsg) {
displayDanmaku(danmakuMsg);
}
public void onResume() {
if (mDanmakuView != null && mDanmakuView.isPrepared() && mDanmakuView.isPaused()) {
mDanmakuView.resume();
}
}
public void onPause() {
if (mDanmakuView != null && mDanmakuView.isPrepared()) {
mDanmakuView.pause();
}
}
public void onDestroy() {
if (mDanmakuView != null) {
// dont forget release!
mDanmakuView.release();
mDanmakuView = null;
}
stopDanmaku();
}
private void displayDanmaku(@NonNull DanmakuMsg dm) {
//如果当前的弹幕由于Android生命周期的原因进入暂停状态,那么不应该不停的消耗弹幕数据
//要知道,在这里发出一个handler消息,那么将会消费(删掉)ConcurrentLinkedQueue头部的数据
if (isDanmukuEnable) {
if (!TextUtils.isEmpty(dm.msg)) {
addDanmaku(dm.msg, dm.islive);
}
}
}
private void displayDanmaku() {
//如果当前的弹幕由于Android生命周期的原因进入暂停状态,那么不应该不停的消耗弹幕数据
//要知道,在这里发出一个handler消息,那么将会消费(删掉)ConcurrentLinkedQueue头部的数据
boolean b = !mQueue.isEmpty() && getDanmakuRunning();
if (b) {
DanmakuMsg dm = mQueue.poll();
if (!TextUtils.isEmpty(dm.msg)) {
addDanmaku(dm.msg, dm.islive);//可以在此之后再加一行代码,驱动弹幕继续显示单个弹幕 mDanmakuHandler.sendEmptyMessageDelayed(WHAT_DISPLAY_SINGLE_DANMAKU, (long) (Math.random() * 400) + 100);
}
}
}
private void addDanmaku(CharSequence cs, boolean islive) {
BaseDanmaku danmaku = mDanmakuContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL);
if (danmaku == null || mDanmakuView == null) {
return;
}
danmaku.text = cs;
danmaku.padding = 5;
danmaku.priority = 0; // 可能会被各种过滤器过滤并隐藏显示
danmaku.isLive = islive;
danmaku.setTime(mDanmakuView.getCurrentTime());
danmaku.textSize = 20f * (mParser.getDisplayer().getDensity() - 0.6f); //文本弹幕字体大小
danmaku.textColor = getRandomColor(); //文本的颜色
danmaku.textShadowColor = getRandomColor(); //文本弹幕描边的颜色
//danmaku.underlineColor = Color.DKGRAY; //文本弹幕下划线的颜色
danmaku.borderColor = getRandomColor(); //边框的颜色
mDanmakuView.addDanmaku(danmaku);
}
/**
* 从一系列颜色中随机选择一种颜色
*
* @return
*/
private int getRandomColor() {
int i = ((int) (Math.random() * 10)) % colors.length;
return colors[i];
}
}
第三步,直接拿来在上层的activity用:
package zhangphil.danmaku;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import master.flame.danmaku.ui.widget.DanmakuView;
public class MainActivity extends Activity {
private MGDanmaku mMGDanmaku;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d(getClass().getName(),"onCreate");
DanmakuView mDanmakuView = (DanmakuView) findViewById(R.id.danmakuView);
MGDanmakuHttpController mMGDanmakuHttpController = new MGDanmakuHttpController();
mMGDanmakuHttpController.setHttpRequestInterval(0);
mMGDanmaku = new MGDanmaku(mDanmakuView, mMGDanmakuHttpController);
CheckBox checkBox = (CheckBox) findViewById(R.id.checkBox);
checkBox.setChecked(mMGDanmaku.getDanmakuRunning());
checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
mMGDanmaku.setDanmakuRunning(isChecked);
}
});
Button sendText = (Button) findViewById(R.id.sendText);
sendText.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
DanmakuMsg msg = new DanmakuMsg();
msg.msg = "zhangphil: " + System.currentTimeMillis();
mMGDanmaku.sendMsg(msg);
}
});
}
@Override
protected void onResume() {
super.onResume();
mMGDanmaku.onResume();
}
@Override
protected void onPause() {
super.onPause();
mMGDanmaku.onPause();
}
@Override
protected void onDestroy() {
super.onDestroy();
mMGDanmaku.onDestroy();
}
}
一个简单的弹幕数据消息封装包:
package zhangphil.danmaku;
/**
* Created by Phil on 2017/3/31.
*/
import java.io.Serializable;
/**
* 弹幕数据封装的类(bean)
*/
public class DanmakuMsg implements Serializable {
public String id = "";
public String msg = null;
public boolean islive = true;
public String point = "";
}
测试的MainActivity布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<CheckBox
android:id="@+id/checkBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="弹幕开关" />
<Button
android:id="@+id/sendText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="发送文本弹幕" />
<master.flame.danmaku.ui.widget.DanmakuView
android:id="@+id/danmakuView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
注意!需要配置Activity在AndroidManifest.xml的属性configChanges和launchMode,以适应弹幕在横竖屏切换时的状态正确,配置如:
<activity android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|screenSize|fontScale"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
代码运行结果如图:


附录:
1,《Android弹幕实现:基于B站弹幕开源系统(1)》链接:http://blog.csdn.net/zhangphil/article/details/68067100
2,《Android弹幕实现:基于B站弹幕开源系统(2)》链接:http://blog.csdn.net/zhangphil/article/details/68114226
3,《Android弹幕实现:基于B站弹幕开源系统(3)-文本弹幕的完善和细节调整》链接:http://blog.csdn.net/zhangphil/article/details/68485505
4,《Java ConcurrentLinkedQueue队列线程安全操作》链接:http://blog.csdn.net/zhangphil/article/details/65936066