最近项目中需要添加弹幕功能,就用了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;
可以看成 屏幕宽度-时间*速度,结果就是距离左边的距离。
因此整个弹幕可以说就是根据一个计时器更新时间,并根据时间计算弹幕位置,实现弹幕的滑动效果。
更多问题使用过程中再继续研究。。。