Android开源弹幕引擎DanmakuFlameMaster源码解析

公孙高轩
2023-12-01

最近项目中需要添加弹幕功能,就用了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;

可以看成 屏幕宽度-时间*速度,结果就是距离左边的距离。

因此整个弹幕可以说就是根据一个计时器更新时间,并根据时间计算弹幕位置,实现弹幕的滑动效果。

更多问题使用过程中再继续研究。。。

 类似资料: