Android弹幕实现:基于B站弹幕开源系统(4)-重构

??

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

时间: 2024-11-05 23:24:13

Android弹幕实现:基于B站弹幕开源系统(4)-重构的相关文章

Android弹幕实现:基于B站弹幕开源系统(1)

?? Android弹幕实现:基于B站弹幕开源系统(1) 如今的视频播放,流行在视频上飘弹幕.这里面做的相对比较成熟.稳定.使用量较多的弹幕系统,当推B站的弹幕系统,B站的弹幕系统已经作为开源项目在github上,其项目地址:https://github.com/Bilibili/DanmakuFlameMaster 以B站开源的弹幕项目为基础,现给出一个简单的例子,实现发送简单的文本弹幕.第一步,首先要在Android的build.gradle文件中引入B站的项目: repositories

Android弹幕实现:基于B站弹幕开源系统(2)

?? Android弹幕实现:基于B站弹幕开源系统(2) 在附录1的基础上,模拟实现一种实际开发的应用场景:从网络中不间断的周期取弹幕数据,这些弹幕数据往往是批量的,然后把这些从网络中取到的批量数据逐个的显示出来.注意本例中的Handler和线程安全队列ConcurrentLinkedQueue的使用.Java代码: package zhangphil.danmaku; import android.app.Activity; import android.graphics.Color; imp

“垃圾分类”大家怎么说?用Python来分析b站弹幕

“垃圾分类”大家怎么说?用Python分析b站弹幕 目录 0 引言 1 环境 2 需求分析 3 代码实现 4 后记 0 引言 纸巾再湿也是干垃圾?瓜子皮再干也是湿垃圾??最近大家都被垃圾分类折磨的不行,傻傻的你是否拎得清?自2019.07.01开始,上海已率先实施垃圾分类制度,违反规定的还会面临罚款. 为了避免巨额损失,我决定来b站学习下垃圾分类的技巧.为什么要来b站,听说这可是当下年轻人最流行的学习途径之一. 打开b站,搜索了下垃圾分类,上来就被这个标题吓(吸)到(引)了:在上海丢人的正确姿势

《用python 玩转数据》项目——B站弹幕数据分析

1. 背景 在视频网站上,一边看视频一边发弹幕已经是网友的习惯.在B站上有很多种类的视频,也聚集了各种爱好的网友.本项目,就是对B站弹幕数据进行分析.选取分析的对象是B站上点播量过1.4亿的一部剧<Re:从零开始的异世界生活>. 2.       算法 分两部分:  第一部分: 2.1     在<Re:从零开始的异世界生活>的首页面,找到共25集的所有对应播放链接和剧名的格式,获取每一集的播放链接,并保存. 2.2     从每一集的播放页面中,通过正则re获取它的cid号,获得

基于Android平台的i-jetty网站智能农业监控系统

基于android平台i-jetty网站的智能农业监控系统 摘要:传统的监控系统,一般是基于PC的有线通信传输,其有很多不足之处,如功耗较高.布线成本高.难度大,适应性差,可扩展性不强,增加新的通信线路需要再次布线施工,而且维护起来也比较麻烦,一旦线路出问题,需要繁琐的检查.而嵌入式Web监控系统是基于物联网技术,其无线通信技术具有成本低廉.适应性强.扩展性强.信息安全.使用维护简单等优点. 智能农业中,种植大棚是通过大棚内安装温湿度以及光照传感器,来对农作物的环境参数进行实时采集,由Web监控

iOS基于B站的IJKPlayer框架的流媒体探究

学习交流及技术讨论可新浪微博关注:极客James 一.流媒体 流媒体技术从传输形式上可以分为:渐进式下载和实施流媒体. 1.渐进式下载 它是介于实时播放和本地播放之间的一种播放方式,渐进式下载不必等到全部下载完成后在播放,可以边下载边播放,播放完成后,整个文件会保存下来.从用户的体验上合播放方的效果来看,渐进式下载和实时流媒体没有什么区别,不过是渐进式下载保留有文件在本地.下面来介绍下渐进式下载的开发 渐进式下载的API和本地播放的API没有什么太大的区别,可以使用MediaPlayer框架中得

基于Java的四大开源测试工具

摘要:成功的应用程序离不开测试人员和QA团队反复地测试,应用程序在进行最后的部署之前,需要通过测试来确保它的负载管理能力以及在特殊情况下的工作条件和工作加载情况. %R[)vA t]N0 测试是应用程序生命周期里至关重要的一步,应用程序在进行最后的部署之前,需要通过测试来确保它的负载管理能力以及在特殊情况下的工作条件和工作加载情况. 51Testing软件测试网tN U%hG!]+L9gr 网络上许多开源的Java测试工具,然而真正经得起时间和实践考验的不多,本文例举了Java里的四大开源测试工

【源代码】基于Android和蓝牙的单片机温度採集系统

如需转载请标明出处:http://blog.csdn.net/itas109 QQ技术交流群:129518033 STC89C52单片机通过HC-06蓝牙模块与Android手机通信实例- 基于Android和蓝牙的单片机温度採集系统 整个project下载:http://download.csdn.net/detail/itas109/7539057 当中包含. 1.下位机电路原理图 2.下位机採集温度.控制发送.自己主动纠错代码 3.Android端接收温度并显示代码 文件截图 这个是我当年

基于WebRTC的MCU开源项目Licode的环境搭建

基于WebRTC的MCU开源项目Licode的环境搭建 由于项目需求,需要构建多人通讯,调研了多人通讯的三种常见结构: 1.前一篇博客已经基于codelab实现了三人聊天,这种多人系统基于Mesh结构.具体来说,假设有N+1个客户端,那么对于每一个客户端都需要与其他N个对象建立PeerConnection,这样消耗了大量的带宽和CPU资源.对于客户端数量较少的应用比较适合,延迟小.开发简单.画面无损失. 2.基于MCU的结构,该MCU的核心功能就是视频和音频的Mix.通过将多路信号混合成一路,达