教你寫一個彈幕庫,確定不瞭解一下?
最近剛寫完了一個彈幕庫 Muti-Barrage ,它具有如下功能:
- 自定義多檢視(彈幕必備)
- 碰撞檢測
- 檢測觸控事件
- 設定傳送間隔
- 設定速度生成區間
- 迴圈播放
花費了不少閒暇的時間,故打算在此總結一下。老規矩,在寫下文之前,我們先看一下效果:
單檢視彈幕應用

多檢視彈幕例子

目錄

一、會遇到的坑
- 多檢視如何處理
- 如何防碰撞
- 觸控事件如何檢測
二、總體一覽
我們先看一下彈幕的產生過程:

整體並不難, BarrageAdapter
負責管理資料, BarrageView
負責管理檢視,資料被加入 BarrageAdapter
後,單執行緒的執行緒池控制子View的產生速度,定時傳送訊息給 BarrageAdapterHandler
,生成彈幕的子View之後經過一些列操作新增進 BarrageView
中
三、程式碼細節
這裡,我不會把整段程式碼都貼上,而是根據彈幕產生過程逐步展開。
1.資料定義
所有彈幕的資料都必須實現 DataSource
介面, getType()
方法可以幫我們確定檢視的佈局。
public interface DataSource { // 返回當前的型別 int getType(); // 返回生成的時間 long getShowTime(); } 複製程式碼
2.定義 IBarrageView
介面
BarrageView
需要實現的方法,讓 BarrageAdapter
呼叫
public interface IBarrageView { // 新增檢視 void addBarrageItem(View view); // 獲取是否存在快取 View getCacheView(int type); // 傳送View間隔 long getInterval(); // 迴圈的次數 int getRepeat(); } 複製程式碼
3.資料新增
為了約束資料型別,我們需要在 BarrageAdapter
使用範型,也就是
public abstract class BarrageAdapter<T extends DataSource> implements View.OnClickListener { } 複製程式碼
下面我們從資料的新增入口講起:
/** * 新增一組資料 * * @param dataList 一組資料 */ public void addList(List<T> dataList) { if (dataList == null || dataList.size() == 0) return; int len = dataList.size(); mDataList.addAll(dataList); mService.submit(new DelayRunnable(len)); } 複製程式碼
mDataList
是我們存放資料的 List
,資料新增好之後,執行緒池會執行我們的任務 DelayRunnable
, DelayRunnable
是什麼呢?看程式碼:
/** * 延遲的Runnable */ public class DelayRunnable implements Runnable { private int len; DelayRunnable(int len) { this.len = len; } @Override public void run() { if (repeat != -1 && repeat > 0) { for (int j = 0; j < repeat; j++) { sendMsg(len); } } else if (repeat == -1) { while (!isDestroy.get()) { sendMsg(len); } } } } private void sendMsg(int len) { for (int i = 0; i < len; i++) { Message msg = new Message(); msg.what = MSG_CREATE_VIEW; msg.obj = i; mHandler.sendMessage(msg); try { Thread.sleep(interval * 20); } catch (InterruptedException e) { e.printStackTrace(); } } } 複製程式碼
可以看到, DelayRunnable
實現了 Runnable
介面, run()
方法主要控制彈幕的迴圈次數, sendMsg(int len)
中不斷髮送訊息給 mHandler
,其中迴圈次數 repeat
和傳送訊息的間隔 interval
都是 IBarrageView
提供的,而 mHandler
就是生產過程中有的 BarrageAdapterHandler
,主要負責子View的生成。
4.子View的生成
我們將 BarrageAdapterHandler
設定成靜態類。從資料變成 BarrageView
子View的過程直接在下面的程式碼體現了出來:
public static class BarrageAdapterHandler<T extends DataSource> extends Handler { private WeakReference<BarrageAdapter> adapterReference; BarrageAdapterHandler(Looper looper, BarrageAdapter adapter) { super(looper); adapterReference = new WeakReference<>(adapter); } @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what) { case MSG_CREATE_VIEW: { int pos = (int) msg.obj; T data = (T) adapterReference.get().mDataList.get(pos); if (data == null) break; if (adapterReference.get().barrageView == null) throw new RuntimeException("please set barrageView,barrageView can't be null"); // get from cache View cacheView = adapterReference.get().barrageView.getCacheView(data.getType()); adapterReference.get().createItemView(data, cacheView); } } } } 複製程式碼
先獲取 msg.obj
中的序號,從而從 mDataList
中取出具體資料,接著從 IBarrageView
中的 getCacheView(data.getType())
獲取快取檢視,我們先拋開 BarrageAdapter
,從 BarrageView
中繼續挖掘,在 BarrageView
中,我們利用 SparseArray<LinkedList<View>>
進行快取彈幕子View的管理,根據不同的 DataSource
中的 type
,將快取彈幕子View存進不同的 LinkedList<View>
中,我們需要快取彈幕子View的時候直接從 SparseArray<LinkedList<View>>
裡面取出一個子View。現在可以回到 BarrageAdapter
了,我們來看 createItemView(data, cacheView)
方法,這裡就很像我們平時對 RecyclerView
中 RecyclerAdapter
的封裝了:
/** * 建立子檢視的過程 * * @param cacheView 快取檢視 */ public void createItemView(T data, View cacheView) { // 1.獲取子佈局 // 2. 建立ViewHolder // 3. 繫結ViewHolder // 4. 返回檢視 int layoutType = getItemLayout(data); BarrageViewHolder<T> holder = null; if (cacheView != null) { holder = (BarrageViewHolder<T>) cacheView.getTag(R.id.barrage_view_holder); } if (null == holder) { holder = createViewHolder(mContext, layoutType); mTypeList.add(data.getType()); } bindViewHolder(holder, data); if (barrageView != null) barrageView.addBarrageItem(holder.getItemView()); } /** * 建立ViewHolder * * @param type 佈局型別 * @return ViewHolder */ private BarrageViewHolder<T> createViewHolder(Context context, int type) { View root = LayoutInflater.from(context).inflate(type, null); BarrageViewHolder<T> holder = onCreateViewHolder(root, type); // 設定點選事件 root.setTag(R.id.barrage_view_holder, holder); root.setOnClickListener(this); return holder; } public abstract static class BarrageViewHolder<T> { public T mData; private View itemView; public BarrageViewHolder(View itemView) { this.itemView = itemView; } public View getItemView() { return itemView; } void bind(T data) { mData = data; onBind(data); } protected abstract void onBind(T data); } 複製程式碼
在子View的生成過程中:
- 先獲取子佈局檔案,
getItemLayout(T t)
是抽象方法,主要根據不同的資料型別確定不同的佈局檔案。 - 接著判斷快取View
cacheView
是否為空,不為空則利用getTag(R.id.barrage_view_holder)
方法獲取快取View中繫結的BarrageViewHolder
。 -
holder
即BarrageViewHolder
為空的情況下就重新建立彈幕的子View,這裡我們可以從createViewHolder(mContext, layoutType)
中得處結論,子View就是在這裡根據不同的佈局檔案建立的,Tag
和彈幕的觸控事件的設定也是在這裡設定的,這也就解決了上面的兩個問題, 如何設定多檢視 和 觸控事件的檢測 。 -
bindViewHolder(holder, data);
將holder
和具體的資料進行繫結。最終呼叫BarrageViewHolder
中的抽象onBind(T data)
方法,從而進行UI的設定。 - 最後,
IBarrageView
將子彈幕子View新增進去。
5. BarrageView
對子View的處理
子View新增來之後, BarrageView
會對子View進行高度和寬度的測量,測量完之後進行最佳彈幕航道的選擇和速度的設定,最後進行屬性動畫的建立,我們逐個分析。
寬度和高度的設定
@Override public void addBarrageItem(final View view) { // 獲取高度和寬度 int w = View.MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); int h = View.MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); view.measure(w, h); final int itemWidth = view.getMeasuredWidth(); final int itemHeight = view.getMeasuredHeight(); if (singleLineHeight == -1) { // 如果沒有設定高度 啟用新增的第一個Item作為行數 // 建議使用最小的Item的高度 singleLineHeight = itemHeight; initBarrageListAndSpeedArray(); } // 先省略後面程式碼 } /** * 初始化一個空的彈幕列表和速度列表 */ private void initBarrageListAndSpeedArray() { barrageDistance = DeviceUtils.dp2px(getContext(), 12); barrageLines = height / (singleLineHeight + barrageDistance); for (int i = 0; i < barrageLines; i++) { barrageList.add(i, null); } speedArray = new int[barrageLines]; for (int i = 0; i < barrageLines; i++) { speedArray[i] = 0; } } 複製程式碼
在上面程式碼中,我們獲取了子View的高度和寬度,如果是第一次新增子View,同時使用者也沒有對彈幕的高度進行設定,這個時候只能由 BarrageView
自身進行 barrageList
和 speedArray
進行初始化, barrageList
是 List<View>
,用來管理每個彈幕航道最新彈幕的子View, speedArray
是 int[]
,則用於管理最新彈幕子View的速度,他們可以用來幹嘛,這裡先賣個關子。
獲取最佳彈幕航道
獲取最佳航道的程式碼比較多,這裡就不寫了,首先會根據彈幕的佈局(可以將彈幕放在頂部、中間、底部和全屏)進行行數的過濾,接著從 barrageList
獲取每一行的子View從而獲取 getX()
,最終得出哪一行剩餘的空間大,你可能會有疑問,當前航道沒有子View呢?這種情況就簡單了,直接返回該航道啊。
獲取速度
/** * 獲取速度 * * @param line 最佳彈道 * @param itemWidth 子View的寬度 * @return 速度 */ private int getSpeed(int line, int itemWidth) { if (model == MODEL_RANDOM) { return speed - speedWaveValue + random.nextInt(2 * speedWaveValue); } else { int lastSpeed = speedArray[line]; View view = barrageList.get(line); int curSpeed; if (view == null) { curSpeed = speed - speedWaveValue + random.nextInt(2 * speedWaveValue); Log.e(TAG, "View:null" + ",line:" + line + ",speed:" + curSpeed); // 如果當前為空 隨機生成一個滑動時間 return curSpeed; } int slideLength = (int) (width - view.getX()); if (view.getWidth() > slideLength) { // 資料密集的時候跟上面的時間間隔相同 Log.e(TAG, "View:------" + ",line:" + line + ",speed:" + lastSpeed); return lastSpeed; } // 得到上個View剩下的滑動時間 int lastLeavedSlidingTime = (int) ((view.getX() + view.getWidth() ) / (float) lastSpeed)+1; //Log.e(TAG,"lastLeavedSlidingTime:"+lastLeavedSlidingTime+",lastLeavedSlidingTime:"+); int fastestSpeed = (width) / lastLeavedSlidingTime; fastestSpeed = Math.min(fastestSpeed, speed + speedWaveValue); if (fastestSpeed <= speed - speedWaveValue) { curSpeed = speed - speedWaveValue; } else curSpeed = speed - speedWaveValue + random.nextInt(fastestSpeed - (speed - speedWaveValue)); Log.e(TAG, "view:" + view.getX() + ",lastLeavedSlidingTime:" + lastLeavedSlidingTime + ",line:" + line + ",speed:" + curSpeed); return curSpeed; } } 複製程式碼
speed
和 speedWaveValue
分別是速度初始值和速度波動值, [speed-speedWaveValue,speed+speedWaveValue]
代表彈幕的速度區間。這裡 BarrageView
會先判斷當前彈幕的模式,如果是 MODEL_RANDOM
模式,我們直接隨機生成彈幕速度就好了,不過需要在速度區間中生成;如果是防碰撞模式,我們需要:
- 通過我們上面提到的
barrageList
和speedArray
分別獲取之前該航道前一個子View和其速度。 - 如果前子View為空,跟隨機模式生成速度的規則一樣。
- 如果前子View不為空,我們需要獲取前子View已經滑動的距離,並且根據它的速度計算剩下滑動的時間,用剩下滑動時間下我們計算當前子View在不碰撞的前提下能夠設定最快的速度,計算好之後再在使用者設定的速度區間和不超過最快速度的前提下隨機生成一個速度。
當然,這並不是絕對的,如果彈幕生成間隔設定不理想的情況下,較短的時間內會產生大量的子View,肯定會發生碰撞的,這個時候我們就直接設定前一個子View的速度。
彈幕滑動
這個我們利用屬性動畫完成即可:
@Override public void addBarrageItem(final View view) { // 省略前面程式碼 // 生成動畫 final ValueAnimator valueAnimator = ValueAnimator.ofInt(width, -itemWidth); // 獲取最佳的行數 final int line = getBestLine(itemHeight); int curSpeed = getSpeed(line, itemWidth); long duration = (int)((float)(width+itemWidth)/(float)curSpeed+1) * 1000; Log.i(TAG,"duration:"+duration); valueAnimator.setDuration(duration); valueAnimator.setInterpolator(new LinearInterpolator()); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { int value = (int) animation.getAnimatedValue(); //Log.e(TAG, "value:" + value); if(cancel){ valueAnimator.cancel(); BarrageView.this.removeView(view); } view.layout(value, line * (singleLineHeight + barrageDistance) + barrageDistance / 2, value + itemWidth, line * (singleLineHeight + barrageDistance) + barrageDistance / 2 + itemHeight); } }); valueAnimator.addListener(new SimpleAnimationListener() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); BarrageView.this.removeView(view); BarrageAdapter.BarrageViewHolder holder = (BarrageAdapter.BarrageViewHolder) view.getTag(R.id.barrage_view_holder); DataSource d = (DataSource) holder.mData; int type = d.getType(); addViewToCaches(type, view); // 通知記憶體新增快取 mHandler.sendEmptyMessage(0); } }); addView(view); speedArray[line] = curSpeed; // 因為使用快取View,必須重置位置 view.layout(width, line * (singleLineHeight + barrageDistance) + barrageDistance / 2, width + itemWidth, line * (singleLineHeight + barrageDistance) + barrageDistance / 2 + itemHeight); barrageList.set(line, view); valueAnimator.start(); } 複製程式碼
這裡就比較簡單了,當前速度獲取以後,直接利用當前螢幕寬度加子View寬度除以當前速度計算彈幕子View執行屬性動畫的時間。這裡需要注意的是:
- 動畫執行結束或者
BarrageView
銷燬的時候,需要將當前子View從BarrageView
中移除。 - 動畫執行結束的時候,當前View被移除後會被新增到快取,之後執行
mHandler.sendEmptyMessage(0)
,在mHandler
中,如果快取View過多的時候就會清理快取,這裡的細節不會過多描述,具體的可以看程式碼。
到這兒,我們 BarrageView
對子View的處理就結束了~