Android彈幕DanmakuFlameMaster源碼解析

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

可以看成 屏幕寬度-時間*速度,結果就是距離左邊的距離。

因此整個彈幕可以說就是根據一個計時器更新時間,並根據時間計算彈幕位置,實現彈幕的滑動效果。

更多問題使用過程中再繼續研究。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章