最近項目中需要添加彈幕功能,就用了B站的開源框架DanmakuFlameMaster。用法比較簡單,創建一個Parser添加數據源,prepare然後start就可以了。然而會用並不夠,由於比較好奇彈幕是怎麼動起來的,就着重看了下這一部分的代碼。至於緩存以及其他的源碼暫時並沒有研究。
先從prepare開始看
@Override
public void prepare(BaseDanmakuParser parser, DanmakuContext config) {
prepare();
handler.setConfig(config);
handler.setParser(parser);
handler.setCallback(mCallback);
handler.prepare();
}
先調用自己的prepare
private void prepare() {
if (handler == null)
handler = new DrawHandler(getLooper(mDrawingThreadType), this,mDanmakuVisible);
}
創建了一個DrawHandler,這個Handler獲取新創建的HandlerThread的Looper,用來執行handlerMessage在子線程,所以不能再prepare的回調裏更新UI。
而handler的prepare會走DanmakuView的回調
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) {
}
@Override
public void prepared() {
//執行在子線程裏
mDanmakuView.start();
}
});
然後在prepared回調方法裏調用start後彈幕就開始了。而start最終會在DrawHandler的handlerMessage執行
case START:
Long startTime = (Long) msg.obj;
if (startTime != null) {
pausedPosition = startTime;
} else {
pausedPosition = 0;
}
case SEEK_POS:
if (what == SEEK_POS) {
quitFlag = true;
quitUpdateThread();
Long position = (Long) msg.obj;
long deltaMs = position - timer.currMillisecond;
mTimeBase -= deltaMs;
timer.update(position);
mContext.mGlobalFlagValues.updateMeasureFlag();
if (drawTask != null)
drawTask.seek(position);
pausedPosition = position;
}
case RESUME:
removeMessages(DrawHandler.PAUSE);
quitFlag = false;
if (mReady) {
mRenderingState.reset();
mDrawTimes.clear();
mTimeBase = SystemClock.uptimeMillis() - pausedPosition;
timer.update(pausedPosition);
removeMessages(RESUME);
sendEmptyMessage(UPDATE);
drawTask.start();
notifyRendering();
mInSeekingAction = false;
if (drawTask != null) {
drawTask.onPlayStateChanged(IDrawTask.PLAY_STATE_PLAYING);
}
} else {
sendEmptyMessageDelayed(RESUME, 100);
}
break;
可以看到START和SEEK_TO均沒有break,因此最後執行到了RESUME裏,到這裏爲止初始化了DrawTask用來處理彈幕繪製,併發送了UPDATE的消息
case UPDATE:
if (mUpdateInNewThread) {
updateInNewThread();
} else {
updateInCurrentThread();
}
這裏根據當前系統線程數決定是否新建線程處理彈幕繪製,這裏就看一下創建新線程的邏輯
while (!isQuited() && !quitFlag) {
long startMS = SystemClock.uptimeMillis();
dTime = SystemClock.uptimeMillis() - lastTime;
long diffTime = mFrameUpdateRate - dTime;
if (diffTime > 1) {
SystemClock.sleep(1);
continue;
}
lastTime = startMS;
long d = syncTimer(startMS);
if (d < 0) {
SystemClock.sleep(60 - d);
continue;
}
d = mDanmakuView.drawDanmakus();
if (d > mCordonTime2) { // this situation may be cuased by ui-thread waiting of DanmakuView, so we sync-timer at once
timer.add(d);
mDrawTimes.clear();
}
if (!mDanmakusVisible) {
waitRendering(INDEFINITE_TIME);
} else if (mRenderingState.nothingRendered && mIdleSleep) {
dTime = mRenderingState.endTime - timer.currMillisecond;
if (dTime > 500) {
notifyRendering();
waitRendering(dTime - 10);
}
}
}
可以看到進入了一個循環,到這裏彈幕的繪製就開始了,可以看到這一行:
d = mDanmakuView.drawDanmakus();
這一行上面代碼是用來同步時間並且更新計時,並控制最多16ms一幀,彈幕的滑動就是靠時間來計算位置並更新。
看drawDanmakus()可以看到最後執行postInvalidate方法,使View重繪。所以直接看onDraw方法。
@Override
protected void onDraw(Canvas canvas) {
if ((!mDanmakuVisible) && (!mRequestRender)) {
super.onDraw(canvas);
return;
}
if (mClearFlag) {
DrawHelper.clearCanvas(canvas);
mClearFlag = false;
} else {
if (handler != null) {
RenderingState rs = handler.draw(canvas);
if (mShowFps) {
if (mDrawTimes == null)
mDrawTimes = new LinkedList<Long>();
String fps = String.format(Locale.getDefault(),
"fps %.2f,time:%d s,cache:%d,miss:%d", fps(), getCurrentTime() / 1000,
rs.cacheHitCount, rs.cacheMissCount);
DrawHelper.drawFPS(canvas, fps);
}
}
}
mRequestRender = false;
unlockCanvasAndPost();
}
- 可以看到調用了handler.draw(canvas),繼續跟蹤到了DrawTask的drawDanmakus方法,只看關鍵代碼
screenDanmakus = danmakuList.sub(beginMills, endMills);
mRenderer.draw(mDisp, screenDanmakus, mStartRenderTime, renderingState);
上面一行截取要顯示的彈幕,下面一行開始繪製,繼續跟蹤,到了關鍵的方法
// layout
mDanmakusRetainer.fix(drawItem, disp, mVerifier);
繼續跟蹤
drawItem.layout(disp, drawItem.getLeft(), topPos);
因爲我們是從右往左的彈幕,就看R2LDanmaku的layout方法:
@Override
public void layout(IDisplayer displayer, float x, float y) {
if (mTimer != null) {
long currMS = mTimer.currMillisecond;
long deltaDuration = currMS - getActualTime();
if (deltaDuration > 0 && deltaDuration < duration.value) {
this.x = getAccurateLeft(displayer, currMS);
if (!this.isShown()) {
this.y = y;
this.setVisibility(true);
}
mLastTime = currMS;
return;
}
mLastTime = currMS;
}
this.setVisibility(false);
}
先看獲取x的方法,也就是彈幕可以動起來的核心所在
protected float getAccurateLeft(IDisplayer displayer, long currTime) {
long elapsedTime = currTime - getActualTime();
if (elapsedTime >= duration.value) {
return -paintWidth;
}
return displayer.getWidth() - elapsedTime * mStepX;
}
getActualTime返回的是彈幕應該顯示的時間,現在的時間減去彈幕的時間就是已經經過的時間,如果已經經過的時間超過彈幕的持續時間,說明彈幕已經顯示完畢。否則返回
displayer.getWidth() - elapsedTime * mStepX;
可以看成 屏幕寬度-時間*速度,結果就是距離左邊的距離。
因此整個彈幕可以說就是根據一個計時器更新時間,並根據時間計算彈幕位置,實現彈幕的滑動效果。