-
Android弹幕实现:基于B站弹幕开源系统(3)-文本弹幕的完善和细节调整
2017-03-30 12:33:50Android弹幕实现:基于B站弹幕开源系统(3)本文在附录1,2的基础上再次对异步获取弹幕并显示弹幕完善逻辑和代码,集中在上层Java代码部分:package zhangphil.danmaku; import android.app.Activity; import ...Android弹幕实现:基于B站弹幕开源系统(3)
本文在附录1,2的基础上再次对异步获取弹幕并显示弹幕完善逻辑和代码,集中在上层Java代码部分:
package zhangphil.danmaku; import android.app.Activity; import android.graphics.Color; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.text.TextUtils; import android.view.View; import android.widget.Button; import java.util.ArrayList; import java.util.HashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; 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; public class MainActivity extends Activity { private DanmakuView mDanmakuView; private DanmakuContext mContext; private AcFunDanmakuParser mParser; private final int MAX_DANMAKU_LINES = 8; //弹幕在屏幕显示的最大行数 private ScheduledThreadPoolExecutor mScheduledThreadPoolExecutor = null; private ConcurrentLinkedQueue<DanmakuMsg> mQueue = null; //所有的弹幕数据存取队列,在这里做线程的弹幕取和存 private ArrayList<DanmakuMsg> danmakuLists = null;//每次请求最新的弹幕数据后缓存list private final int WHAT_GET_LIST_DATA = 0xffa01; private final int WHAT_DISPLAY_SINGLE_DANMAKU = 0xffa02; private final int BASE_TIME = 400; private final int BASE_TIME_ADD = 100; //标志文本弹幕的序列号 //区别不同弹幕 private static int danmakuTextMsgId = 0; private final int[] colors = {Color.RED, Color.YELLOW, Color.BLUE, Color.GREEN, Color.CYAN, Color.DKGRAY}; private Handler mDanmakuHandler = new Handler() { @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what) { case WHAT_GET_LIST_DATA: mDanmakuHandler.removeMessages(WHAT_GET_LIST_DATA); if (danmakuLists != null && !danmakuLists.isEmpty()) { mQueue.addAll(danmakuLists); danmakuLists.clear(); if (!mQueue.isEmpty()) mDanmakuHandler.sendEmptyMessage(WHAT_DISPLAY_SINGLE_DANMAKU); } break; case WHAT_DISPLAY_SINGLE_DANMAKU: mDanmakuHandler.removeMessages(WHAT_DISPLAY_SINGLE_DANMAKU); displayDanmaku(); break; } } }; /** * 弹幕数据封装的类(bean) */ private class DanmakuMsg { public String msg; } private void displayDanmaku() { boolean p = mDanmakuView.isPaused(); //如果当前的弹幕由于Android生命周期的原因进入暂停状态,那么不应该不停的消耗弹幕数据 //要知道,在这里发出一个handler消息,那么将会消费(删掉)ConcurrentLinkedQueue头部的数据 if (!mQueue.isEmpty() && !p) { DanmakuMsg dm = mQueue.poll(); if (!TextUtils.isEmpty(dm.msg)) { addDanmaku(dm.msg, true); } mDanmakuHandler.sendEmptyMessageDelayed(WHAT_DISPLAY_SINGLE_DANMAKU, (long) (Math.random() * BASE_TIME) + BASE_TIME_ADD); } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); danmakuLists = new ArrayList<>(); mQueue = new ConcurrentLinkedQueue<>(); mDanmakuView = (DanmakuView) findViewById(R.id.danmakuView); initDanmaku(); mScheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1); GetDanmakuMessageTask mTask = new GetDanmakuMessageTask(); //延迟0秒执行,每隔若干秒周期执行一次任务 mScheduledThreadPoolExecutor.scheduleAtFixedRate(mTask, 0, 5, TimeUnit.SECONDS); Button show = (Button) findViewById(R.id.show); Button hide = (Button) findViewById(R.id.hide); Button sendText = (Button) findViewById(R.id.sendText); Button pause = (Button) findViewById(R.id.pause); Button resume = (Button) findViewById(R.id.resume); Button clear = (Button) findViewById(R.id.clear); show.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mDanmakuView.show(); } }); hide.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mDanmakuView.hide(); } }); sendText.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //每点击一次按钮发送一条弹幕 sendTextMessage(); } }); pause.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mDanmakuView.pause(); } }); resume.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mDanmakuView.resume(); } }); clear.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { clearDanmaku(); } }); } /** * 假设该线程任务模拟的就是从网络中取弹幕数据的耗时操作 * 假设这些弹幕数据序列是有序的。 */ private class GetDanmakuMessageTask implements Runnable { @Override public void run() { danmakuLists.clear(); int count = (int) (Math.random() * 50); for (int i = 0; i < count; i++) { DanmakuMsg message = new DanmakuMsg(); message.msg = "弹幕:" + danmakuTextMsgId; danmakuLists.add(message); danmakuTextMsgId++; } if (!danmakuLists.isEmpty()) { Message msg = mDanmakuHandler.obtainMessage(); msg.what = WHAT_GET_LIST_DATA; mDanmakuHandler.sendMessage(msg); } } } /** * 驱动弹幕显示机制重新运作起来 */ private void resumeDanmaku() { if (!mQueue.isEmpty()) mDanmakuHandler.sendEmptyMessageDelayed(WHAT_DISPLAY_SINGLE_DANMAKU, (int) (Math.random() * BASE_TIME) + BASE_TIME_ADD); } private void clearDanmaku() { if (danmakuLists != null && !danmakuLists.isEmpty()) { danmakuLists.clear(); } if (mQueue != null && !mQueue.isEmpty()) mQueue.clear(); mDanmakuView.clearDanmakusOnScreen(); mDanmakuView.clear(); } private void initDanmaku() { mContext = DanmakuContext.create(); // 设置最大显示行数 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); //普通文本弹幕也描边设置样式 //如果是图文混合编排编排,最后不要描边 mContext.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, mContext); //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("弹幕文本", "danmakuShown text=" + danmaku.text); } @Override public void prepared() { mDanmakuView.start(); } }); } } private void sendTextMessage() { addDanmaku("zhangphil@csdn: " + System.currentTimeMillis(), true); } private void addDanmaku(CharSequence cs, boolean islive) { BaseDanmaku danmaku = mContext.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() + 1200); danmaku.textSize = 20f * (mParser.getDisplayer().getDensity() - 0.6f); //文本弹幕字体大小 danmaku.textColor = getRandomColor(); //文本的颜色 danmaku.textShadowColor = getRandomColor(); //文本弹幕描边的颜色 //danmaku.underlineColor = Color.DKGRAY; //文本弹幕下划线的颜色 danmaku.borderColor = getRandomColor(); //边框的颜色 mDanmakuView.addDanmaku(danmaku); } @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(); //重新启动handler消息机制,触发弹幕显示 //如果没有这一个方法,那么显示弹幕的机制将失灵(失去驱动) //这个方法就是重新激发弹幕显示的handler机制。 resumeDanmaku(); } } private void closeGetDanmakuMessage() { if (mScheduledThreadPoolExecutor != null) mScheduledThreadPoolExecutor.shutdown(); } @Override protected void onDestroy() { super.onDestroy(); if (mDanmakuView != null) { // dont forget release! mDanmakuView.release(); mDanmakuView = null; } closeGetDanmakuMessage(); } /** * 从一系列颜色中随机选择一种颜色 * * @return */ private int getRandomColor() { int i = ((int) (Math.random() * 10)) % colors.length; return colors[i]; } }
代码运行结果如图:
附录:
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,《Java ConcurrentLinkedQueue队列线程安全操作》链接:http://blog.csdn.net/zhangphil/article/details/65936066 -
Android弹幕实现:基于B站弹幕开源系统(4)-重构
2017-04-01 18:58:58Android弹幕实现:基于B站弹幕开源系统(4)-重构弹幕在视频播放的APP中比较常见,但是逻辑比较复杂,现在在附录1,2,3的基础上,我再次对弹幕进行抽象和重构,把弹幕从底向上抽象成不同的层,便于复用。第一步,抽象...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 -
android实现简单弹幕
2019-03-26 15:15:03android端实现,简单的弹幕功能;可以自定义弹幕的样式和使用逻辑 -
弹幕的简单实现
2016-06-21 10:55:59前言 现在很多的视屏网站,像优酷、acfun,甚至直播网站都实现了弹(dàn)幕技术。弹幕的添加,使得观看视屏的实时评论可以被... 在使用的弹幕视频网中,我们可以简单的分析弹幕的基本逻辑。当用户发送一条文本消前言
现在很多的视屏网站,像优酷、acfun,甚至直播网站都实现了弹(dàn)幕技术。弹幕的添加,使得观看视屏的实时评论可以被同时观看这个视屏的其他用户所看到,并且可以加入到讨论当中。完全改变了以往看视屏的方式。相信大家在观看视屏时也有所体会。那么这篇博客将记录怎么实现简单弹幕的过程。分析
在使用的弹幕视频网中,我们可以简单的分析弹幕的基本逻辑。当用户发送一条文本消息时,这条文本消息就会出现在视频的上方,从右往左过渡。通过查看源代码可以知道,那些大型的弹幕视屏网站时通过Flash实现弹幕的。再看源代码,这条链接打开其实是一个Flash的播放器,连发送文本消息的文本框都是内嵌到播放器上的,所以想看它怎么处理文本看不了。如果熟悉弹幕这方面的大神看到我分析错了,请纠错,但请轻喷,我还只是个孩子~。既然看不了怎么处理文本的,没办法,就自己想办法解决咯。just do it,搞起。我想到的一个方案就是,在视屏上再覆盖一层透明的div,暂且称为"文字层",用来过渡弹幕。如果有其他方案欢迎提出~实现
弹幕的实现又分为两个小块实现,一个是播放视屏,一个是文本输入框。播放视屏直接用的是HTML5的新标签<video>,这个标签的用法这里就不再熬述了,想了解的可以查阅其他资料。播放视频上不要忘了,还要覆盖一层"文字层"。第二块,文本输入框用的是<textarer>具体的还是看代码。HTML:<div class="mv_play"> <video width="750px" height="" src="source/yilu.mp4" controls="controls" ></video> <div id="fontdiv" class="fontdiv"></div> </div> <div class="pushtext"> <textarea id="message" maxlength="15" ></textarea> <div οnclick="pushtext()" class="pushbtn">发送</div> </div>
CSS:.mv_play { margin-left: 20px; position: relative; } .fontdiv { position: absolute; z-index: 120; width: 750px; height: 386px; top: 0px; color: white; overflow: hidden; } .pushtext { margin-left: 20px; margin-top: 15px; height: 50px; } .pushtext > textarea { width: 678px; height: 16px; border: 1px solid #433B38; border-radius: 4px 0 0 4px; resize: none; outline: none; padding: 12px; padding-left: 15px; line-height: 15px; float: left; } .pushbtn { display: block; width: 43px; height: 42px; border-radius: 0 4px 4px 0; line-height: 45px; background: #433B38; color: #fff; font-size: 14px; float: left; text-align: center; cursor: pointer; } .changmessage{ transition:right linear 6s; right: 750px; animation:toleft 5.5s linear; } @keyframes toleft{ 0%{ transform:translateX(0); } 100%{ transform:translateX(-1200px); } } .oldp{ position: absolute; border: 2px solid cadetblue; margin-top: 5px; left: 750px; font-size: 20px; }
这里说明一下,要想文字层覆盖在视屏层上,在他们两个层的父级(这里就是class=mv_play的div)的positive要设置为relative。然后文字层positive属性设置的absolute才会根据父级相对调整位置,还要设置overflow为hidden,当弹幕还没出现在播放区域内时隐藏。然后就是用z-index来实现覆盖,这个属性设置的数值越大,所设置的这个层就表示越在上面。最后就是功能的实现了,这里只是实现简单的弹幕效果,并没有后端处理。大概的思路就是首先获取文本框的内容,放进<pre>标签内,然后往“文本层”添加这个<pre>标签。在添加的同时给<pre>标签添加一个移动的类changemessage,文本就会往左边移动。别忘了在移动完后还要隐藏掉这个文本,所以要设置一个时间调度器,在一定时间后隐藏文本。具体看以下代码。js:function messaction(ppp) { ppp.addClass("changmessage"); } function messstop(ppp) { ppp.hide(); } function pushtext(){ var message=$("#message").val(); if(message.length>0){ $("#fontdiv").append("<pre class='oldp' style='width: "+message.length+"em'>"+message+"</pre>"); $("#message").val(""); var ppp= $("pre").last(); //移动弹幕 setTimeout(function(){ messaction(ppp); },100); //隐藏弹幕 setTimeout(function(){ messstop(ppp); },8500); } }; document.οnkeydοwn=function(event){ var e = event || window.event || arguments.callee.caller.arguments[0]; if(e&&e.keyCode==13){ //要做的事情 e.preventDefault(); var messvalue=$("#message").val(); if(messvalue.length>0){ pushtext(); } } };
结语
最后还是上个效果图吧大概的思路就是以上所说,但实际用起来还是有问题的,比如文本多少时间后才隐藏、消耗内存、视屏没有播放弹幕也依然过渡等。如果真要用到实际开发,还可能需要用到ajax等技术就不多说了。 -
Android实现弹幕效果
2017-05-18 15:44:11我可以看到,弹幕效果是在屏幕上方飘过,即从屏幕外一端出现,一直在屏幕中飘动至屏幕另一端,我们将其实现逻辑进行分解如下: 1.一个viewGroup中包含多个textView 2.textView包含平移动画 经过分解发现相信大家看视频的时候都会有弹幕效果,这似乎已经成为视频软件的标配,接下来让我们来看看如何实现这个弹幕效果。
一.弹幕效果分析
我可以看到,弹幕效果是在屏幕上方飘过,即从屏幕外一端出现,一直在屏幕中飘动至屏幕另一端,我们将其实现逻辑进行分解如下:1.一个viewGroup中包含多个textView2.textView包含平移动画经过分解发现其逻辑一点也不复杂,分析完二.实现
上面我们分析了自定义弹幕效果View的逻辑:这里我们让我们自定义的的BarrageView继承RelateLayout,并实现onLayout方法。@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); int childCount = this.getChildCount(); for(int i = 0; i < childCount; i++){ final View view = getChildAt(i); if (view != null){ RelativeLayout.LayoutParams lp = (LayoutParams) view.getLayoutParams(); if (lp.leftMargin <= 0){ if(mDirection == FROM_RIGNG_TO_LEFT){ view.layout(mScreenWidth,lp.topMargin,mScreenWidth+view.getMeasuredWidth(), lp.topMargin+view.getMeasuredHeight()); }else if(mDirection == FROM_LEFT_TO_RIGHT){ view.layout(-view.getMeasuredWidth(),lp.topMargin,0, lp.topMargin+view.getMeasuredHeight()); } } } } }
这里很多人可能会问为什么是实现onLayout方法,我们的BarrageView继承了RelateLayout,即继承了一个viewGroup,而飘屏的弹幕均属于这个viewGroup的子View,我们的弹幕效果要实现必须在我们需要的时候往这个BarrageView中添加一个子view,添加子view我们就必须要为其指定在父控件中的位置。而每当我们像BarrageView中加入一个子view,就会执行其onLayout方法,所以我们最终就在onLayout中确定子view的初始位置从代码中我们可以看到这边我支持了两个方向,从左到右(FROM_LEFT_TO_RIGHT)和从右到左(FROM_RIGNG_TO_LEFT),这里我们分析从右到左,毕竟目前较多的软件都是从右到左飘屏
这段代码写的很清晰,既然是从右到左飘屏,那我们添加的子view最开始的位置当然就是在屏幕外右端,如下图view.layout(mScreenWidth,lp.topMargin,mScreenWidth+view.getMeasuredWidth(), lp.topMargin+view.getMeasuredHeight());
接下来我们要添加子view,并为其设置相应的动画效果,使其达到飘屏的效果private void createBarrageItemView(Context context,String text,int textSize,int textColor){ final TextView textView = new TextView(context); textView.setBackgroundResource(R.drawable.barrage_item_bg); if (textColor != 0){ textView.setTextColor(getResources().getColor(textColor)); } if (textSize != 0){ textView.setTextSize(textSize); } textView.setText(text); int w = View.MeasureSpec.makeMeasureSpec(0,View.MeasureSpec.UNSPECIFIED); int h = View.MeasureSpec.makeMeasureSpec(0,View.MeasureSpec.UNSPECIFIED); textView.measure(w, h); int height =textView.getMeasuredHeight(); int width = textView.getMeasuredWidth(); int row = new Random().nextInt(100) % mRowNum; while (needResetRow(row)){ row = new Random().nextInt(100) % mRowNum; } mRowPosList.add(row); RelativeLayout.LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT); lp.topMargin = row * (height + ToolUtils.dip2px(context,10)); // lastRow = row; textView.setLayoutParams(lp); textView.setPadding(ToolUtils.dip2px(context,15), ToolUtils.dip2px(context,2), ToolUtils.dip2px(context,15), ToolUtils.dip2px(context,2)); textView.getBackground().setAlpha(mAlpha); this.addView(textView); ViewPropertyAnimator animator = null; if(mDirection == FROM_RIGNG_TO_LEFT){ animator = textView.animate() .translationXBy(-(mScreenWidth + width + 80)); }else if(mDirection == FROM_LEFT_TO_RIGHT){ animator = textView.animate() .translationXBy(mScreenWidth + width + 80); } animator.setDuration(mDuration); animator.setInterpolator(new LinearInterpolator()); animator.start(); animator.setListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { // mChildView.remove(textView); BarrageView.this.removeView(textView); } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); mChildView.add(textView); }
添加子view需要注意一下几点:利用随机数,生成子view出现在第几行,
然后在判断子view是否需要重新设置所在行(需要设置是为了不让后面的子view与前面的子view重叠),其实现如下:int row = new Random().nextInt(100) % mRowNum; while (needResetRow(row)){ row = new Random().nextInt(100) % mRowNum; } mRowPosList.add(row);
该逻辑思想为,从所有子view中遍历,找到与当前需要添加的子view同一行的最后一个添加的子view,(我们用sameRowView表示),判断sameRowView是否显示完全,如果未显示完全就继续生成随机行数。(图画的丑 ,能看懂就行....)public boolean needResetRow(int row){ int size = mRowPosList.size(); int sameRowPos =-1; for(int i = size; i > 0; i--){ if (row == mRowPosList.get(i-1)){ sameRowPos = i-1; break; } } if (sameRowPos != -1){ TextView tv = mChildView.get(sameRowPos); if (mScreenWidth -tv.getX() < tv.getWidth()){ return true; } } return false; }
这里指的一提的是,以上逻辑是存在问题的,如果每个子view都很长的话,当全部子view占满屏幕且都没显示完全时,我们将会陷入一个死循环,这样我们的应用就会卡死(为了制作gif图好看,采用这种方式),通过上网查阅,给出的方案是,只记录最后一个子view所在的位置,判断当前新家的子view是否与上一个子view同行,若是,则重选行数。
很遗憾的是,上述的方法虽然不会卡死(不会卡死就很重要了,毕竟能卡死应用已经很严重,所以实际使用推荐使用这种方式,或者有其他更好的方式欢迎交流) 但是没有解决子view间相互覆盖的问题。如下图,我们1先加入BarrageView中,然后我们2加入并且我们2不会与1在同一行,如果同行就重选选行,同理3不会与2同行,但是3可以与1同行,因为1不是最后一个加入BarrageView的子view,但是确实存在了覆盖问题int row = new Random().nextInt(100) % mRowNum; while (mLastRow == row){ row = new Random().nextInt(100) % mRowNum; } mLastRow = row;
至此我们的自定义弹幕BarrageView就分析完了,下面贴上调用代码与自定义BarrageView代码MainActivity.java
BarrageView.javapublic class MainActivity extends AppCompatActivity { private BarrageView barrageView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); barrageView = (BarrageView) findViewById(R.id.barrageView); barrageView.setBackgroundResource(0); new Thread(new Runnable() { @Override public void run() { for(int i = 0; i < 10000; i++){ final int finalI = i; runOnUiThread(new Runnable() { @Override public void run() { if (finalI%2 == 0){ barrageView.addBarrageItemView(MainActivity.this,"第" + finalI +"条弹幕"); }else{ barrageView.addBarrageItemView(MainActivity.this,"第" + finalI +"条弹幕"+"第" + finalI +"条弹幕"); } } }); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } }
public class BarrageView extends RelativeLayout{ private int mRowNum = 6; private long mDuration = 5000;//弹幕在屏幕显示的时间 默认5s private int mAlpha = 180;//背景的透明度0-255 private int mDirection = FROM_RIGNG_TO_LEFT;//当前弹幕活动方向 默认从右到左 public static final int FROM_LEFT_TO_RIGHT = 1;//从左到右 public static final int FROM_RIGNG_TO_LEFT = 2;//从右到左 private int mScreenWidth; private List<TextView> mChildView; private LinkedList mRowPosList; public BarrageView(Context context) { super(context); init(context); } public BarrageView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public BarrageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } private void init(Context context){ mScreenWidth = ToolUtils.getScreenWidth(context); mChildView = new ArrayList<TextView>(); mRowPosList = new LinkedList(); } /** * 设置弹幕飘动方向 * @param direction 弹幕飘动方向 默认从右到左 FROM_RIGNG_TO_LEFT */ public void setDirection(int direction){ mDirection = direction; } /** * 设置弹幕飘屏时间 * @param duration 弹幕飘屏时间 默认5s */ public void setDuration(long duration){ mDuration = duration; } /** * 设置飘屏行数 * @param rowNum 飘屏行数 默认6条 */ public void setRowNum(int rowNum){ mRowNum = rowNum; } /** * 设置item的背景透明度 范围:0~255 * @param alpha 取值0~255 0为全透明 */ public void setBackgroundAlpha(int alpha){ mAlpha = alpha; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); int childCount = this.getChildCount(); for(int i = 0; i < childCount; i++){ final View view = getChildAt(i); if (view != null){ RelativeLayout.LayoutParams lp = (LayoutParams) view.getLayoutParams(); if (lp.leftMargin <= 0){ if(mDirection == FROM_RIGNG_TO_LEFT){ view.layout(mScreenWidth,lp.topMargin,mScreenWidth+view.getMeasuredWidth(), lp.topMargin+view.getMeasuredHeight()); }else if(mDirection == FROM_LEFT_TO_RIGHT){ view.layout(-view.getMeasuredWidth(),lp.topMargin,0, lp.topMargin+view.getMeasuredHeight()); } } } } } public void addBarrageItemView(Context context,String text,int textSize,int textColor){ createBarrageItemView(context,text,textSize,textColor); } public void addBarrageItemView(Context context,String text){ createBarrageItemView(context,text,0,0); } private void createBarrageItemView(Context context,String text,int textSize,int textColor){ final TextView textView = new TextView(context); textView.setBackgroundResource(R.drawable.barrage_item_bg); if (textColor != 0){ textView.setTextColor(getResources().getColor(textColor)); } if (textSize != 0){ textView.setTextSize(textSize); } textView.setText(text); int w = View.MeasureSpec.makeMeasureSpec(0,View.MeasureSpec.UNSPECIFIED); int h = View.MeasureSpec.makeMeasureSpec(0,View.MeasureSpec.UNSPECIFIED); textView.measure(w, h); int height =textView.getMeasuredHeight(); int width = textView.getMeasuredWidth(); int row = new Random().nextInt(100) % mRowNum; while (needResetRow(row)){ row = new Random().nextInt(100) % mRowNum; } mRowPosList.add(row); RelativeLayout.LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT); lp.topMargin = row * (height + ToolUtils.dip2px(context,10)); // lastRow = row; textView.setLayoutParams(lp); textView.setPadding(ToolUtils.dip2px(context,15), ToolUtils.dip2px(context,2), ToolUtils.dip2px(context,15), ToolUtils.dip2px(context,2)); textView.getBackground().setAlpha(mAlpha); this.addView(textView); ViewPropertyAnimator animator = null; if(mDirection == FROM_RIGNG_TO_LEFT){ animator = textView.animate() .translationXBy(-(mScreenWidth + width + 80)); }else if(mDirection == FROM_LEFT_TO_RIGHT){ animator = textView.animate() .translationXBy(mScreenWidth + width + 80); } animator.setDuration(mDuration); animator.setInterpolator(new LinearInterpolator()); animator.start(); animator.setListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { // mChildView.remove(textView); BarrageView.this.removeView(textView); } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); mChildView.add(textView); } public boolean needResetRow(int row){ int size = mRowPosList.size(); int sameRowPos =-1; for(int i = size; i > 0; i--){ if (row == mRowPosList.get(i-1)){ sameRowPos = i-1; break; } } if (sameRowPos != -1){ TextView tv = mChildView.get(sameRowPos); if (mScreenWidth -tv.getX() < tv.getWidth()){ return true; } } return false; } }
-
前端实现直播弹幕
2019-07-21 17:49:132,建立所需要的dom节点并添加到dom池里面,dom节点总数=轨道数X每条轨道可放置的最大弹幕数,并把建好的dom节点加入到可视区容器里面通过添加class使其定位到可视区外部,并绑定相应的事件进行逻辑处理(注意作用域... -
十块钱帮我做个视频网站吧,就带弹幕那种?基于websocket协议与netty实时视频弹幕交互实现
2021-01-14 21:28:162 实现思路2.1 服务架构2.2 传输流程3 实现效果3.1 视频展示4 代码实现4.1 项目结构4.2 Java服务端4.2.1 先做一个netty nio的服务端:4.2.2 服务端的具体处理逻辑4.3 网页客户端实现5 小结 基于websocket协议与netty... -
17 -Flask构建弹幕微电影网站- 电影播放及评论弹幕收藏实现
2018-03-05 21:10:00上映预告 模型: Preview 表单: 无 请求方法: GET 访问控制: 无 views中进行业务逻辑的实现 @home.route("/animation/") def animation(): """ 首页轮播动画 """ data = Preview.query.... -
使用模块化和观察者模式实现简单的弹幕效果
2020-12-14 22:12:24使用观察者模式来实现弹幕时,被观察者可以有n个,所以将每一个弹幕都作为一个被观察者,根据它不同的状态来进行不同的操作 add,remove,update状态 观察keyCode为13时并且输入为非空时,我们进行add添加并且开启定时器... -
荧屏弹幕_新增h5requestAnimationFrame实现
2019-10-03 20:35:14所有的页面逻辑也是比较简单,用原生js实现,封装也是比较简单!要让页面效果更为炫酷,则可去引入相应的css,背景图片自己去img/下下载引入喔! HTML页面 <!doctype html> <html> <head> ... -
Canvas + WebSocket + Redis 实现一个视频弹幕
2018-10-12 00:14:17首先,我们需要实现页面布局,在根目录创建 index.html 布局中我们需要有一个 video 多媒体标签引入我们的本地视频,添加输入弹幕的输入框、确认发送的按钮、颜色选择器、字体大小滑动条,创建一个 style.css 来调整... -
自动刷帖机器人 python_虎牙自动发弹幕机器人 python+airtest实现
2020-12-09 11:53:28无聊上虎牙看直播的时候就想带节奏/手动滑稽 哈哈哈哈写了一个python脚本实现,感觉效果还不错,分享给大家基本逻辑:1。自动登录。需配置好账号密码。2。可以从一个主播房间列表中随机选取一个房间进入。3。在这个... -
Python实时爬取斗鱼弹幕
2019-01-22 09:54:33输入斗鱼房间号实时获取弹幕信息,实现效果如下: douyu.gif 逻辑梳理 首先说明下斗鱼是开放了弹幕API的,可以直接去他们开发者论坛查看文档,按照文档中要求一步一步的来就好了,我这边就简单梳理下: 建立两... -
让弹幕飞一会儿——腾讯视频弹幕(39W+)爬取实战
2019-04-07 21:19:00愚人节立下本周要给出代码的flag,放假跪着也得实现...本文以腾讯视频(都挺好)为例,解析弹幕爬取的细节和难点,对思路感兴趣的旁友们可以跟着文章逻辑走一遍,对于想直接上... -
滑动UILabel 炫酷动画效果类似弹幕滚动
2017-06-14 16:45:14滑动UILabel 实现类似弹幕效果,动画炫酷,实现简单.逻辑清楚可以自己尝试实现. -
14 -Flask构建弹幕微电影网站-后台逻辑(六)
2018-03-03 21:23:00本章内容: 日志管理实现 已上线演示地址: http://movie.mtianyan.cn 项目源码地址:https://github.com/mtianyan/movie_project 模型: Oplog Userlog Adminlog 表单: 无 请求方法: GET 访问控制: @admin_... -
Javascript实现的一个简单的弹幕效果-入门版
2017-09-12 23:46:22思路如下: 依据web的三层架构搭建好页面 处理页面逻辑 一.基本结构的搭建 我需要的样式如上图:所以我的基本结构也就明确下来了 1.html结构骨架 <p class=" -
C++实现简易(多人弹幕控制主播游戏人物类型,CMD_迷宫小游戏)(一)
2021-01-14 17:29:54就像线上有什么好玩的,突然想到外国主播,让水友发弹幕来控制游戏人物行动这件事情。感觉还挺好玩的。 就想是怎么做出来的。可以做一个这种类型的小游戏,如果公司年会线上的话,可以让不同团队来控制这个小人物,... -
C++实现简易(多人弹幕控制主播游戏人物类型,CMD_迷宫小游戏)(二)
2021-01-21 18:27:37逻辑实现: 1.服务器第一个接到客户端链接时,生成地图,初始坐标等信息。 2.有客户端链接时,给客户端发送地图,当前人物坐标等数据 3.客户端不生成地图,只接收服务器地图数据,每次成功行动后,发送坐标给... -
猫耳简单过滤弹幕脚本JS插件(过滤器插件)V1.1免费最新版
2019-08-05 18:35:28脚本是批处理文件的延伸,是一种纯文本保存的程序,一般来说的计算机脚本程序是确定的一系列控制计算机进行运算操作动作的组合,在其中可以实现一定的逻辑分支等。 js脚本各浏览器安装教程 各浏览器对本方法的支持... -
关于 js自制多屏同步弹幕 原理的分析
2017-07-30 16:58:31待项目成熟应进行弹幕框架封装; ... ... 前端的坑还会继续一个个地去踩; 在路上, 会一个一个把坑尽量看清楚些, 看透彻了, 为了下一次快掉进去之前, 不抽自己嘴巴子, 而是昂首跨过去, “来哇,互相伤害哇”! -
映客都是互刷礼物吗_仿映客刷礼物效果—基本逻辑实现
2020-12-24 14:30:07(关于弹幕的实现,大家可以参考我前面写的一篇文章IOS 自定义弹幕实现在开始我的实现方案之前,大家可以先参看下这篇文章iOS 基于 IM 实现仿映客刷礼物连击效果,写得很好,Demo中关于礼物连乘的动画效果,就是引用... -
知识点总结
2016-08-30 20:38:54/// 语法及概念 Objective-C与JavaScript交互的那些事 内购 ios中的事件处理、响应者链条以及第一响应者 iOS-集成支付宝支付、微信支付 集成支付宝支付 本地 远程推送/// 实用小技能get ...弹幕实现逻辑 -
直播间页面逻辑
2021-02-22 11:05:12视频会有实时评论 首先判断弹幕是否为0,没有就隐藏,有就在这里面进行遍历评论 会有关闭直播操作 点击会有一个提示信息,询问是否关闭,然后调用接口传参验证,返回信息 在头部会显示谁的直播间 需要通过get进行... -
smox再更新,增加react-hooks支持,顺便实现一个hook版本的redux
2018-11-05 04:53:15前几个月一直忙于c站(clicli弹幕网)的业务逻辑,所以几乎消失了 然后,直到前几天,react-hooks 出来后,才更新了 smox react-hooks 的使用我就不多说啦,有其他文章 其实如果算到 smox 上,也就是状态管理,有...
-
nasm: error: more than one input file specified 原因
-
MySQL 管理利器 mysql-utilities
-
C++学习(二一八)GUID
-
敏捷个人:内容框架之执行力
-
用户体验之网页板块设计
-
TypeError: Class advice impossible in Python3. Use the @Implementer class decorator instead.
-
碳中和目标下A股先锋调研:钢铁行业加速碳达峰 多行业影响已现
-
jquery如何判断滚动条是否到底部
-
洛谷P5734 【深基6.例6】文字处理软件经典解法
-
pdf是图片还是文档
-
Qt调用libVLC实现播放器
-
A股开户热情:1月新增投资者再破200万 市场能否走出当下阴霾?
-
用微服务spring cloud架构打造物联网云平台
-
昆仑万维业绩快报:2020年净利润47.75亿元 同比增269%
-
python-递归-Product_sum-“乘积的和“
-
宝塔安装sqlsrv扩展,PHP连接 Sqlserver
-
五金机械工具箱电商淘宝详情页设计模板.zip
-
【硬核】一线Python程序员实战经验分享(1)
-
一起看看 Activity启动模式
-
C++代码规范和Doxygen根据注释自动生成手册