??
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); } } } 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