本文已受權微信公衆號:鴻洋(hongyangAndroid)在微信公衆號平臺原創首發git
最近剛寫完了一個彈幕庫Muti-Barrage,它具備以下功能:github
花費了很多閒暇的時間,故打算在此總結一下。老規矩,在寫下文以前,咱們先看一下效果:緩存
咱們先看一下彈幕的產生過程: bash
總體並不難,BarrageAdapter
負責管理數據,BarrageView
負責管理視圖,數據被加入BarrageAdapter
後,單線程的線程池控制子View的產生速度,定時發送消息給BarrageAdapterHandler
,生成彈幕的子View以後通過一些列操做添加進BarrageView
中微信
這裏,我不會把整段代碼都貼上,而是根據彈幕產生過程逐步展開。dom
全部彈幕的數據都必須實現DataSource
接口,getType()
方法能夠幫咱們肯定視圖的佈局。ide
public interface DataSource {
// 返回當前的類型
int getType();
// 返回生成的時間
long getShowTime();
}
複製代碼
IBarrageView
接口BarrageView
須要實現的方法,讓BarrageAdapter
調用oop
public interface IBarrageView {
// 添加視圖
void addBarrageItem(View view);
// 獲取是否存在緩存
View getCacheView(int type);
// 發送View間隔
long getInterval();
// 循環的次數
int getRepeat();
}
複製代碼
爲了約束數據類型,咱們須要在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的生成。
咱們將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)
是抽象方法,主要根據不一樣的數據類型肯定不一樣的佈局文件。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添加進去。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的速度。
這個咱們利用屬性動畫完成便可:
@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
中移除。mHandler.sendEmptyMessage(0)
,在mHandler
中,若是緩存View過多的時候就會清理緩存,這裏的細節不會過多描述,具體的能夠看代碼。到這兒,咱們BarrageView
對子View的處理就結束了~
Handler
的狀況,這裏都是採用建立靜態內部類和弱引用,以及在destroy
方法中對Handler
進行處理的方法。BarrageView
僅僅是顯示,不但願影響到BarrageView
重疊區域下方控件觸摸事件的下發,須要在onInterceptTouchEvent
阻止事件的下發,須要用戶對isInterceptTouchEvent
字段進行手動設置。其餘的一些細節能夠查看具體的代碼。
本文涉及到了Android中屬性動畫、內存泄漏以及Java中範型和線程池等知識的簡單運用,算是對去年學習的知識一些實戰吧。固然了,本人對知識的理解不免有偏差,若有錯誤,歡迎指出,若是以爲個人庫寫的不錯,能夠給個Star呦~
Muti-Barrage:github.com/mCyp/Muti-B…