精华内容
下载资源
问答
  • Android 弹幕可用开源框架

    千次阅读 2017-06-23 10:51:31
    1 黑暗火焰https://github.com/Bilibili/DanmakuFlameMaster2 开源组件https://github.com/linsea/OpenDanmaku
    展开全文
  • 主要介绍了Android弹幕框架黑暗火焰使基本使用方法,需要的朋友可以参考下。今天我将分享由BiliBili开源的Android弹幕框架(DanmakuFlameMaster)的学习经验,感兴趣的朋友一起看看吧
  • 弹幕框架DanmakuFlameMaster简单分析

    千次阅读 2018-06-29 18:35:06
    随着B站逐渐崛起,其开源弹幕项目DanmakuFlameMaster应用场景也越来越多。我也是在一次偶然机会下发现了这个项目,被其惊艳的效果震撼。以前我就对弹幕技术很感兴趣,可能是因为B站动漫看多,几乎每一部番都是漫天的...

           随着B站逐渐崛起,其开源弹幕项目DanmakuFlameMaster应用场景也越来越多。我也是在一次偶然机会下发现了这个项目,被其惊艳的效果震撼。以前我就对弹幕技术很感兴趣,可能是因为B站动漫看多,几乎每一部番都是漫天的弹幕乱飞,如果哪部剧没有弹幕反而觉得不适应;久而久之就愈发倾向钻研其原理。

           看到效果后,我猜想绘制原理应该是创建一个定时器作为全部弹幕的时间参考,然后每条弹幕出现的位置都以这个定时器去计算x、y值,然后定时任务定期postInvalidate,弹幕画布重新绘制onDraw;弹幕如此之多,应该有缓存机制,也许建立了一个弹幕池让出现过的弹幕缓存起来,新弹幕可以复用旧弹幕item。
           先这么假设吧,然后验证我们的猜想,看看有哪些坑。


    基本使用

           首先是添加控件,项目里提供了三个控件:DanmakuSurfaceView、DanmakuTextureView和DanmakuView,使用其中三个任意一个都可以。我们选个DanmakuView方便分析。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
       
             省略一些布局...
            
        <master.flame.danmaku.ui.widget.DanmakuView
            android:id="@+id/sv_danmaku"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
           
        省略一些布局... 
       
    </FrameLayout>

           然后是代码配置,先看一下初始化相关:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34

      @Override
      protectedvoidonCreate(Bundle savedInstanceState){
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_main);
          findViews();
      }
     
      privatevoidfindViews(){

    省略一些代码...

          // DanmakuView
    mDanmakuView = (IDanmakuView) findViewById(R.id.sv_danmaku);
          // 设置最大显示行数
          HashMap<Integer, Integer> maxLinesPair = new HashMap<Integer, Integer>();
          maxLinesPair.put(BaseDanmaku.TYPE_SCROLL_RL, 5); // 滚动弹幕最大显示5
          // 设置是否禁止重叠
          HashMap<Integer, Boolean> overlappingEnablePair = new HashMap<Integer, Boolean>();
          overlappingEnablePair.put(BaseDanmaku.TYPE_SCROLL_RL, true);
          overlappingEnablePair.put(BaseDanmaku.TYPE_FIX_TOP, true);
       //创建弹幕控件上下文,类似Context,里面可以进行一系列配置
          mContext = DanmakuContext.create();
    mContext.setDanmakuStyle(IDisplayer.DANMAKU_STYLE_STROKEN, 3)//设置描边样式
    .setDuplicateMergingEnabled(false) //设置不合并相同内容弹幕
    .setScrollSpeedFactor(1.2f) //设置弹幕滚动速度缩放比例,越大速度越慢
    .setScaleTextSize(1.2f) //设置字体缩放比例
       .setCacheStuffer(new SpannedCacheStuffer(), mCacheStufferAdapter) // 图文混排使用SpannedCacheStuffer 
    //.setCacheStuffer(new BackgroundCacheStuffer())  // 绘制背景使用BackgroundCacheStuffer
          .setMaximumLines(maxLinesPair) //设置最大行数策略
          .preventOverlapping(overlappingEnablePair); //设置禁止重叠策略
                     
    省略一些代码...

      }

           DanmakuContext设置setCacheStuffer(CacheStuffer, Proxy)时,如果不设置此方法,则CacheStuffer默认为SimpleTextCacheStuffer,proxy默认为null;第一个参数,项目例子中提供了BackgroundCacheStuffer和SpannedCacheStuffer,其实也可以自己扩展,第二个参数例子中也写了一个mCacheStufferAdapter,同理也可以自己扩展。这个sample中注释也写得比较明确,我们往下分析原理时会解释。
           然后设置数据源:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35

                              //替换为A站弹幕数据源,因为A站弹幕数据是jsonB站是xml,为了方便分析因此替换为A站源
                //mParser = createParser(this.getResources().openRawResource(R.raw.comments));
                try {
                    mParser = createParser(this.getAssets().open("comment.json"));
                } catch (IOException e) {
                    e.printStackTrace();
                }

        private BaseDanmakuParser createParser(InputStream stream){

            if (stream == null) {
                returnnew BaseDanmakuParser() {

                    @Override
                    protected Danmakus parse(){
                        returnnew Danmakus();
                    }
                };
            }

    //        ILoader loader = DanmakuLoaderFactory.create(DanmakuLoaderFactory.TAG_BILI);
            ILoader loader = DanmakuLoaderFactory.create(DanmakuLoaderFactory.TAG_ACFUN);

            try {
                loader.load(stream);
            } catch (IllegalDataException e) {
                e.printStackTrace();
            }
    //        BaseDanmakuParser parser = new BiliDanmukuParser();
            BaseDanmakuParser parser = new AcFunDanmakuParser();
            IDataSource<?> dataSource = loader.getDataSource();
            parser.load(dataSource);
            return parser;

        }

           最后启动弹幕:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

    //设置弹幕view相关回调
    mDanmakuView.setCallback(new DrawHandler.Callback() {
            @Override
            publicvoidupdateTimer(DanmakuTimer timer){
            }

            @Override
            publicvoiddrawingFinished(){

            }

            @Override
            publicvoiddanmakuShown(BaseDanmaku danmaku){
             //Log.d("DFM", "danmakuShown(): text=" + danmaku.text);
            }

            @Override
            publicvoidprepared(){
                Log.d("DFM", "MainActivity inline callback's method prepared");
                mDanmakuView.start();
            }
        });
        mDanmakuView.prepare(mParser, mContext);
        mDanmakuView.showFPS(true);
        mDanmakuView.enableDanmakuDrawingCache(true);

           基本使用在项目的例子中都写的很清楚,这些应该难度不大。接下来应该是分析流程了。

    流程分析

          DanmakuFlameMaster流程确实十分复杂,因为变量实在太多了,所以分析时推荐先整体看个大概,然后一步一步打断点确认细节。

    初始配置

           上面写基本使用法,第一步是初始配置,我们看看到底初始化了哪些参数。对比上面的调用顺序,首先进入DanmakuContext看看:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27

                     //相关配置如下,主要初始化一下变量
                     mContext.setDanmakuStyle(IDisplayer.DANMAKU_STYLE_STROKEN, 3)//设置描边样式
                     .setDuplicateMergingEnabled(false) //设置不合并相同内容弹幕
                     .setScrollSpeedFactor(1.2f) //设置弹幕滚动速度缩放比例,越大速度越慢
                     .setScaleTextSize(1.2f) //设置字体缩放比例
                 .setCacheStuffer(new SpannedCacheStuffer(), mCacheStufferAdapter) // 图文混排使用SpannedCacheStuffer 
                     //.setCacheStuffer(new BackgroundCacheStuffer())  // 绘制背景使用BackgroundCacheStuffer
            .setMaximumLines(maxLinesPair) //设置最大行数策略
            .preventOverlapping(overlappingEnablePair); //设置禁止重叠策略

                     //DanmakuContext 类重要方法
                     /*------------DanmakuContext STAET-----------*/
    privatefinal AbsDisplayer mDisplayer = new AndroidDisplayer();//创建DanmakuContext 对象时直接new了个mDisplayer 全局变量
              /**
         * 设置缓存绘制填充器,默认使用SimpleTextCacheStuffer只支持纯文字显示, 如果需要图文混排请设置SpannedCacheStuffer
         * 如果需要定制其他样式请扩展SimpleTextCacheStuffer或者SpannedCacheStuffer
         */
        public DanmakuContext setCacheStuffer(BaseCacheStuffer cacheStuffer, BaseCacheStuffer.Proxy cacheStufferAdapter){
            this.mCacheStuffer = cacheStuffer;
            if (this.mCacheStuffer != null) {
                this.mCacheStuffer.setProxy(cacheStufferAdapter);
                mDisplayer.setCacheStuffer(this.mCacheStuffer);
            }
            returnthis;
        }

                     /*------------DanmakuContext END-----------*/

           以上配置主要配置一些常规参数,记不住也没关系,我们可以打断点一一查看。

    加载资源

           然后就是加载弹幕源:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

      private BaseDanmakuParser createParser(InputStream stream){
    ......

    //创建A站弹幕加载器
          ILoader loader = DanmakuLoaderFactory.create(DanmakuLoaderFactory.TAG_ACFUN);
          try {
           //将数据流载入加载器里
              loader.load(stream);
          } catch (IllegalDataException e) {
              e.printStackTrace();
          }
          //创建弹幕解析器
          BaseDanmakuParser parser = new AcFunDanmakuParser();
          //取出数据源
          IDataSource<?> dataSource = loader.getDataSource();
          //解析器放入数据源
          parser.load(dataSource);
          return parser;

      }

           我们一步一步来,先创建A站弹幕加载器:

    1
    2
    3
    4
    5
    6
    7
    8

    //根据不同标签创建不同加载器,可以根据不同业务自己扩展定制
    publicstatic ILoader create(String tag){
           if (TAG_BILI.equalsIgnoreCase(tag)) {
               return BiliDanmakuLoader.instance();
           } elseif(TAG_ACFUN.equalsIgnoreCase(tag))//我们到了这里
              return AcFunDanmakuLoader.instance();
           returnnull;
       }

           载入数据流:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26

    publicvoidload(InputStream in)throws IllegalDataException {
             try {
                     dataSource = new JSONSource(in);//这里创建了一个JSONSource
             } catch (Exception e) {
                     thrownew IllegalDataException(e);
             }
    }
    //JSONSource构造方法
    publicJSONSource(InputStream in)throws JSONException{
             init(in);
    }
    privatevoidinit(InputStream in)throws JSONException {
             ......
             mInput = in;
             String json = IOUtils.getString(mInput);//将流转成字符串
             init(json);
    }
    privatevoidinit(String json)throws JSONException {
             if(!TextUtils.isEmpty(json)){
                     mJSONArray = new JSONArray(json);//json字符串保存到一个JSONArray全局变量
             }
    }
    //取出JSONSource
    public JSONSource getDataSource(){
             return dataSource;
    }

           载入数据流就是读取弹幕数据文件流,然后转成字符串,最后保存到一个JSONArray变量里存起来。
           继续往下分析创建弹幕解析器、取出数据源、解析器放入数据源:

    1
    2
    3
    4
    5

    //AcFunDanmakuParserload方法,将上一步得到的JSONSource放入到AcFunDanmakuParser
    public BaseDanmakuParser load(IDataSource<?> source){
          mDataSource = source;
          returnthis;
      }

           到这里数据就载入到解析器里了,parser里有弹幕源数据了。

    启动弹幕

           启动弹幕重要的就是这一句:

    1

    mDanmakuView.prepare(mParser, mContext);

           此时mParser和mContext都已经初始化完成。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    publicvoidprepare(BaseDanmakuParser parser, DanmakuContext config){
         prepare();///创建一个 DrawHandler
         handler.setConfig(config);
         handler.setParser(parser);
         handler.setCallback(mCallback);
         handler.prepare();//然后调用DrawHandlerprepare方法
     }
    //创建一个 DrawHandler
    privatevoidprepare(){
         if (handler == null)
             handler = new DrawHandler(getLooper(mDrawingThreadType), this, mDanmakuVisible);//mDanmakuVisibletrue
     }

           设置一些全局变量后,会调用DrawHandler的prepare方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65

    publicvoidprepare(){
        sendEmptyMessage(DrawHandler.PREPARE);
    }
    publicvoidhandleMessage(Message msg){
        int what = msg.what;
        switch (what) {
            case PREPARE:
                mTimeBase = SystemClock.uptimeMillis();
                if (mParser == null || !mDanmakuView.isViewReady()) {// false || false
                    sendEmptyMessageDelayed(PREPARE, 100);
                } else {
                    prepare(new Runnable() {//会继续调用prepare重载方法
                        @Override
                        publicvoidrun(){
                            pausedPosition = 0;
                            mReady = true;
                            if (mCallback != null) {
                                mCallback.prepared();
                            }
                        }
                    });
                }
                break;
                ......
        }       
    }
    private DanmakuTimer timer = new DanmakuTimer();//已经初始化timer
    privatevoidprepare(final Runnable runnable){
        if (drawTask == null) {//会继续调用createDrawTask方法
            drawTask = createDrawTask(mDanmakuView.isDanmakuDrawingCacheEnabled(), timer,
                    mDanmakuView.getContext(), mDanmakuView.getWidth(), mDanmakuView.getHeight(),
                    mDanmakuView.isHardwareAccelerated(), new IDrawTask.TaskListener() {
                        @Override
                        publicvoidready(){
                            initRenderingConfigs();
                            runnable.run();
                        }
                     ......
                    });
        } else {
            runnable.run();
        }
    }
    //继续调用createDrawTask(true, timer, context, width, height, true, listener)方法
    private IDrawTask createDrawTask(boolean useDrwaingCache, DanmakuTimer timer,
                                     Context context,
                                     int width, int height,
                                     boolean isHardwareAccelerated,
                                     IDrawTask.TaskListener taskListener) {
        mDisp = mContext.getDisplayer();//AndroidDisplayer赋给它,顾名思义,Displayer就是显示器
        mDisp.setSize(width, height);//设置弹幕视图宽高
        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
        mDisp.setDensities(displayMetrics.density, displayMetrics.densityDpi,
                displayMetrics.scaledDensity);//设置密度先关
        mDisp.resetSlopPixel(mContext.scaleTextSize);//设置字体缩放比例,之前设过了1.2
        mDisp.setHardwareAccelerated(isHardwareAccelerated);//硬件加速,true
        //useDrwaingCache true
        IDrawTask task = useDrwaingCache ?
                new CacheManagingDrawTask(timer, mContext, taskListener, 1024 * 1024 * AndroidUtils.getMemoryClass(context) / 3)
                : new DrawTask(timer, mContext, taskListener);
        task.setParser(mParser);//把存放数据源的mParser放入CacheManagingDrawTask
        task.prepare();//这个才是重点,调用CacheManagingDrawTaskprepare方法
        obtainMessage(NOTIFY_DISP_SIZE_CHANGED, false).sendToTarget();
        return task;
    }

           上述过程最后一个调用了createDrawTask方法,这里先初始化了一下AndroidDisplayer配置,就把他当做显示器吧,我猜ctiao当初设计时也是这么比喻的吧。
           
    设置好弹幕显示相关的参数,然后就是创建绘制任务IDrawTask了。这里有两个选择,如果使用缓存就创建CacheManagingDrawTask,不使用就创建DrawTask。不过CacheManagingDrawTask比DrawTask复杂很多。

    CacheManagingDrawTask绘制任务

           我们的useDrwaingCache为true(其实把它改为false也没关系,并且这样就用不上那些so库了),则创建CacheManagingDrawTask绘制任务,然后调用prepare方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27

      publicCacheManagingDrawTask(DanmakuTimer timer, DanmakuContext config, TaskListener taskListener, int maxCacheSize){//传入定时器timerconfiglistener,还有三分一应用分配内存大小的maxCacheSize
          super(timer, config, taskListener);//会调用父类DrawTask的构造方法
          NativeBitmapFactory.loadLibs();//加载so库,用于创建bitmap,同时测试时候加载成功
          mMaxCacheSize = maxCacheSize;
          if (NativeBitmapFactory.isInNativeAlloc()) {//true,将最大内存扩大到2
              mMaxCacheSize = maxCacheSize * 2;
          }
          mCacheManager = new CacheManager(maxCacheSize, MAX_CACHE_SCREEN_SIZE);
          mRenderer.setCacheManager(mCacheManager);
      }
      //看看父类的构造方法
      publicDrawTask(DanmakuTimer timer, DanmakuContext context,
              TaskListener taskListener) {
    ......
          mContext = context;
          mDisp = context.getDisplayer();
          mTaskListener = taskListener;
          mRenderer = new DanmakuRenderer(context);
    ......
          initTimer(timer);//初始化相关定时器
    ......
      }
      protectedvoidinitTimer(DanmakuTimer timer){
          mTimer = timer;
          mCacheTimer = new DanmakuTimer();
          mCacheTimer.update(timer.currMillisecond);
      }

           CacheManagingDrawTask的构造方法设置了一些变量。其中NativeBitmapFactory.loadLibs()加载了用于创建bitmap的so文件,就是用skia图形处理库直接创建bitmap,Android对2D图形处理采用的就是skia,3D图形处理用的是OpenGLES。这样通过native层创建bitmap直接跳过Dalvik,毕竟java层内存用多了很容易oom。因为以前我就对native层比较感兴趣,所以我要任性的跟一遍源码 ^O.O^。为了怕跟完后自己晕了,找不到现在分析的地方了,所以在这里打个标签,mark一下。如不感兴趣,可以跳过= 。=

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31

    //NativeBitmapFactory
    publicstaticvoidloadLibs(){
                     ......
            System.loadLibrary("ndkbitmap");//载入so
                     ......
            //测试功能
            if (nativeLibLoaded) {
                boolean libInit = init();//这是一个native方法
                if (!libInit) {
                    release();
                    notLoadAgain = true;
                    nativeLibLoaded = false;
                } else {//初始化成功后
                    initField();//反射Bitmap.ConfignativeInt字段
                    boolean confirm = testLib();//测试例子
     
                }
            }
            Log.e("NativeBitmapFactory", "loaded" + nativeLibLoaded);
        }
        //反射Bitmap.ConfignativeInt字段
        staticvoidinitField(){
            try {
                nativeIntField = Bitmap.Config.class.getDeclaredField("nativeInt");
                nativeIntField.setAccessible(true);
            } catch (NoSuchFieldException e) {
                nativeIntField = null;
                e.printStackTrace();
            }
        }
    privatestaticnativebooleaninit();

           这里会调用测试方法testLib

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51

     privatestaticbooleantestLib(){
         if (nativeIntField == null) {
             returnfalse;
         }
         Bitmap bitmap = null;
         Canvas canvas = null;
         ......
          //native方法创建一个bitmap
             bitmap = createNativeBitmap(2, 2, Bitmap.Config.ARGB_8888, true);
             boolean result = (bitmap != null && bitmap.getWidth() == 2 && bitmap.getHeight() == 2);
    ......
                 canvas = new Canvas(bitmap);
                 Paint paint = new Paint();
                 paint.setColor(Color.RED);
                 paint.setTextSize(20f);
                 canvas.drawRect(0f, 0f, (float) bitmap.getWidth(), (float) bitmap.getHeight(),
                         paint);
                 canvas.drawText("TestLib", 0, 0, paint);

       ......
             return result;

     }
     privatestatic Bitmap createNativeBitmap(int width, int height, Config config, boolean hasAlpha){
         int nativeConfig = getNativeConfig(config);//反射设置Bitmap.Config.ARGB_8888
         return android.os.Build.VERSION.SDK_INT == 19 ? createBitmap19(width, height,
                 nativeConfig, hasAlpha) : createBitmap(width, height, nativeConfig, hasAlpha);
     }
     publicstaticintgetNativeConfig(Bitmap.Config config){
         try {
             if (nativeIntField == null) {
                 return0;
             }
             return nativeIntField.getInt(config);
         } catch (IllegalArgumentException e) {
             e.printStackTrace();
         } catch (IllegalAccessException e) {
             e.printStackTrace();
         }
         return0;
     }

     privatestaticnativebooleaninit();

     privatestaticnativebooleanrelease();

     privatestaticnative Bitmap createBitmap(int width, int height, int nativeConfig,
             boolean hasAlpha);

     privatestaticnative Bitmap createBitmap19(int width, int height, int nativeConfig,
             boolean hasAlpha);

           上述最终用native方法创建bitmap,C++文件地址为 https://github.com/Bilibili/NativeBitmapFactory ,接着继续查看native方法具体实现NativeBitmapFactory.cpp。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31

    //先看javainit方法对应的本地方法
    jboolean Java_tv_cjump_jni_NativeBitmapFactory_init(JNIEnv *env)
    {
             ......
             //继续看Start方法
        int r = Start();
        return r == SUCCESS;
    }
    staticintStart()
    {
             //创建一个类型为ndkbitmap_obj 的结构体指针
        ndkbitmap_obj = (ndkbitmap_object_t *)malloc(sizeof(*ndkbitmap_obj));
        int r = Open(ndkbitmap_obj);
             ......
        return SUCCESS;
    }
    staticintOpen(ndkbitmap_object_t *obj)
    {
             //创建一个类型为skbitmap_sys_t 的结构体指针
        skbitmap_sys_t *sys = (skbitmap_sys_t *)malloc(sizeof (*sys));
             ......
             //打开libskia.so动态链接库,初始化一些参数并返回动态链接库的句柄
        sys->libskia = InitLibrary(sys);
             ......
             //打开libandroid_runtime.so动态链接库,初始化一些参数并返回动态链接库的句柄
        sys->libjnigraphics = InitLibrary2(sys);
             ......
             //将初始化过后的结构指针sys赋给结构体objsys成员
        obj->sys = sys;
        return SUCCESS;
    }

           init方法主要是打开和skia相关的动态链接库,并初始化一些配置。(InitLibrary和InitLibrary2方法的细节我没有贴,里面实现需要一些专业知识,有兴趣的可以找资料钻研)然后就是createBitmap:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74

    jobject Java_tv_cjump_jni_NativeBitmapFactory_createBitmap(JNIEnv *env , jobject  obj, jint w, jint h, jint config, jboolean hasAlpha)
    {
        return createBitmap(env, obj, w, h, config, hasAlpha, true, 0);
    }

    jobject Java_tv_cjump_jni_NativeBitmapFactory_createBitmap19(JNIEnv *env , jobject  obj, jint w, jint h, jint config, jboolean hasAlpha)
    {
        return createBitmap(env, obj, w, h, config, hasAlpha, 0x3, 19);
    }

    jobject createBitmap(JNIEnv *env , jobject  obj, jint w, jint h, jint config, jboolean hasAlpha, int isMuttable, int api)
    {
        void *bm = createSkBitmap(ndkbitmap_obj, config, w, h);//调用重载方法创建bitmap指针
        if (bm == NULL)
        {
            return NULL;
        }
        jobject result = NULL;
        skbitmap_sys_t *p_sys = ndkbitmap_obj->sys;
        if(p_sys->libjnigraphics)
        {
            if(p_sys->gjni_createBitmap)
            {//SDK版本小于19
                     //通过这个函数指针把JNIbitmap的转换对象returnjava
                result = p_sys->gjni_createBitmap(env, bm, isMuttable, NULL, -1);
            } elseif(p_sys->gjni_createBitmap_19later) {//SDK版本19以后返回值
                result = p_sys->gjni_createBitmap_19later(env, bm, NULL, isMuttable, NULL, NULL, -1);
            }

        }
       
        return result;
    }
    //创建bitmap指针,并通过相关指针函数设置bitmap参数
    inline void *createSkBitmap(ndkbitmap_object_t *obj, int config, int w, int h)
    {
        skbitmap_sys_t *p_sys = obj->sys;
        if (p_sys == NULL || p_sys->libskia == NULL)
        {
            return NULL;
        }
        //申请内存,创建skBitmap 指针
        void *skBitmap = malloc(SIZE_OF_SKBITMAP);
        if (!skBitmap)
        {
            return NULL;
        }
        *((uint32_t *) ((uint32_t)skBitmap + SIZE_OF_SKBITMAP - 4)) = 0xbaadbaad;
        //ctor  
        p_sys->sk_ctor(skBitmap);
        if (p_sys->sk_setConfig)
        {
            p_sys->sk_setConfig(skBitmap, config, w, h, 0);
        }
        elseif (p_sys->sk_setConfig_19later)
        {
            p_sys->sk_setConfig_19later(skBitmap, config, w, h, 0, (uint8_t)kPremul_SkAlphaType);
        } elseif (p_sys->sk_setInfo)
        {
            int imageInfo[4] = {w, h, SkBitmapConfigToColorType(config), kPremul_SkAlphaType};
            p_sys->sk_setInfo(skBitmap, imageInfo, 0);
        }
        p_sys->sk_allocPixels(skBitmap, NULL, NULL);
        p_sys->sk_eraseARGB(skBitmap, 0, 0, 0, 0);


        if (!(*((uint32_t *) ((uint32_t)skBitmap + SIZE_OF_SKBITMAP - 4)) == 0xbaadbaad) )
        {
            free(skBitmap);
            return NULL;
        }

        return skBitmap;
    }

           通过skia图形库创建bitmap流程大概就是这些,其实skia的东西也是巨多无比,如果是从事这一方面工作应该都轻车熟路,我是完全的小白,能力有限,只能先到这儿。

           好了,继续回到上次打标签的地方。接着该调用CacheManagingDrawTask的prepare方法:

    1
    2
    3
    4
    5

    publicvoidprepare(){
        assert (mParser != null);
        loadDanmakus(mParser);
        mCacheManager.begin();
    }

           先调用loadDanmakus方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    protected IDanmakus danmakuList;
        protectedvoidloadDanmakus(BaseDanmakuParser parser){
            danmakuList = parser.setConfig(mContext)
                                .setDisplayer(mDisp)
                                .setTimer(mTimer)
                                .getDanmakus();//parser中取出弹幕数据,做出相关处理

            ......

            if(danmakuList != null) {
                mLastDanmaku = danmakuList.last();
            }
        }

           parser设置完DanmakuContext,AndroidDisplayer,DanmakuTimer之后,再调用getDanmakus取出弹幕信息:

    1
    2
    3
    4
    5
    6
    7
    8
    9

    public IDanmakus getDanmakus(){
        if (mDanmakus != null)
            return mDanmakus;
        mContext.mDanmakuFactory.resetDurationsData();//重庆内置一些变量为null
        mDanmakus = parse();//解析弹幕
        releaseDataSource();//关闭JSONSource
        mContext.mDanmakuFactory.updateMaxDanmakuDuration();//修正弹幕最大时长
        return mDanmakus;
    }

           进入AcFunDanmakuParser的parse方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88

        public Danmakus parse(){
            if (mDataSource != null && mDataSource instanceof JSONSource) {
                JSONSource jsonSource = (JSONSource) mDataSource;
                return doParse(jsonSource.data());//go on
            }
            returnnew Danmakus();
        }
        private Danmakus doParse(JSONArray danmakuListData){
            Danmakus danmakus = new Danmakus();
            if (danmakuListData == null || danmakuListData.length() == 0) {
                return danmakus;
            }
            for (int i = 0; i < danmakuListData.length(); i++) {
                try {
                    JSONObject danmakuArray = danmakuListData.getJSONObject(i);
                    if (danmakuArray != null) {
                        danmakus = _parse(danmakuArray, danmakus);//解析每一条弹幕
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
            return danmakus;
        }
         /**
         * {"c":"19.408,16777215,1,25,178252,1376325904","m":"金刚如来!"}
         // 0:时间(弹幕出现时间)
         // 1:颜色
         // 2:类型(1从右往左滚动弹幕|6从右至左滚动弹幕|5顶端固定弹幕|4底端固定弹幕|7高级弹幕|8脚本弹幕)
         // 3:字号
         // 4:用户id ?
         // 5:时间戳 ?
         */
        private Danmakus _parse(JSONObject jsonObject, Danmakus danmakus){
            if (danmakus == null) {
                danmakus = new Danmakus();
            }
            if (jsonObject == null || jsonObject.length() == 0) {
                return danmakus;
            }
            for (int i = 0; i < jsonObject.length(); i++) {
                try {
                    JSONObject obj = jsonObject;
                    String c = obj.getString("c");//弹幕配置信息
                    String[] values = c.split(",");
                    if (values.length > 0) {
                        int type = Integer.parseInt(values[2]); // 弹幕类型
                        if (type == 7)
                            // FIXME : hard code
                            // TODO : parse advance danmaku json
                            continue;
                        long time = (long) (Float.parseFloat(values[0]) * 1000); // 出现时间
                        int color = Integer.parseInt(values[1]) | 0xFF000000; // 颜色
                        float textSize = Float.parseFloat(values[3]); // 字体大小
                        //使用弹幕工厂创建一条弹幕
                        BaseDanmaku item = mContext.mDanmakuFactory.createDanmaku(type, mContext);
                        if (item != null) {
                            item.time = time;
                            item.textSize = textSize * (mDispDensity - 0.6f);
                            item.textColor = color;
                            item.textShadowColor = color <= Color.BLACK ? Color.WHITE : Color.BLACK;
                            //弹幕文字内容,如果多行文本会拆分内容
                            DanmakuUtils.fillText(item, obj.optString("m", "...."));
                            item.index = i;
                            item.setTimer(mTimer);//将定时器设置给每一条弹幕
                            danmakus.addItem(item);
                        }
                    }
                } catch (JSONException e) {
                } catch (NumberFormatException e) {
                }
            }
            return danmakus;
        }
        //DanmakuUtilsdefillText方法,多行文本会拆分
        publicstaticvoidfillText(BaseDanmaku danmaku, CharSequence text){
            danmaku.text = text;
            //如果文本没有换行符则不用拆分
            if (TextUtils.isEmpty(text) || !text.toString().contains(BaseDanmaku.DANMAKU_BR_CHAR)) {
                return;
            }
                     //如果有换行符则拆分,然后将拆分的数组付给lines 属性
            String[] lines = String.valueOf(danmaku.text).split(BaseDanmaku.DANMAKU_BR_CHAR, -1);
            if (lines.length > 1) {
                danmaku.lines = lines;
            }
        }   
    }

           从JSONSource里解析每一条弹幕,接着我们看看弹幕工厂DanmakuFactory创建弹幕的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89

      public BaseDanmaku createDanmaku(int type, DanmakuContext context){
          if (context == null)
              returnnull;
          sLastConfig = context;
          sLastDisp = context.getDisplayer();
          return createDanmaku(type, sLastDisp.getWidth(), sLastDisp.getHeight(), CURRENT_DISP_SIZE_FACTOR, context.scrollSpeedFactor);// go on overload method
      }
      public BaseDanmaku createDanmaku(int type, int viewportWidth, int viewportHeight,
              float viewportScale, float scrollSpeedFactor) {
          return createDanmaku(type, (float) viewportWidth, (float) viewportHeight, viewportScale, scrollSpeedFactor);
      }
      public BaseDanmaku createDanmaku(int type, float viewportWidth, float viewportHeight,
              float viewportSizeFactor, float scrollSpeedFactor) {
          int oldDispWidth = CURRENT_DISP_WIDTH; // 默认是0
          int oldDispHeight = CURRENT_DISP_HEIGHT; // 默认是0
          //修正试图宽高,缩放比,弹幕时长
          boolean sizeChanged = updateViewportState(viewportWidth, viewportHeight, viewportSizeFactor);
          //滚动弹幕的Duration赋值
          if (MAX_Duration_Scroll_Danmaku == null) {
              MAX_Duration_Scroll_Danmaku = new Duration(REAL_DANMAKU_DURATION);
              MAX_Duration_Scroll_Danmaku.setFactor(scrollSpeedFactor);
          } elseif (sizeChanged) {
              MAX_Duration_Scroll_Danmaku.setValue(REAL_DANMAKU_DURATION);
          }
    //固定位置弹幕的Duration赋值
          if (MAX_Duration_Fix_Danmaku == null) {
              MAX_Duration_Fix_Danmaku = new Duration(COMMON_DANMAKU_DURATION);
          }
          if (sizeChanged && viewportWidth > 0) {// true && true
              updateMaxDanmakuDuration();// 修正弹幕最长时长
             ......
          }

          BaseDanmaku instance = null;
          switch (type) {
              case1: // 从右往左滚动
                  instance = new R2LDanmaku(MAX_Duration_Scroll_Danmaku);
                  break;
              case4: // 底端固定
                  instance = new FBDanmaku(MAX_Duration_Fix_Danmaku);
                  break;
              case5: // 顶端固定
                  instance = new FTDanmaku(MAX_Duration_Fix_Danmaku);
                  break;
              case6: // 从左往右滚动
                  instance = new L2RDanmaku(MAX_Duration_Scroll_Danmaku);
                  break;
              case7: // 特殊弹幕
                  instance = new SpecialDanmaku();
                  sSpecialDanmakus.addItem(instance);
                  break;
          }
          return instance;
      }
      //修正试图宽高,缩放比,弹幕时长
      publicbooleanupdateViewportState(float viewportWidth, float viewportHeight,
              float viewportSizeFactor) {
          boolean sizeChanged = false;
          if (CURRENT_DISP_WIDTH != (int) viewportWidth
                  || CURRENT_DISP_HEIGHT != (int) viewportHeight
                  || CURRENT_DISP_SIZE_FACTOR != viewportSizeFactor) {
              sizeChanged = true;
              //弹幕时长 t = 3800 * (1.2 * 视图宽 / 682)
              REAL_DANMAKU_DURATION = (long) (COMMON_DANMAKU_DURATION * (viewportSizeFactor
                      * viewportWidth / BILI_PLAYER_WIDTH));
              // t = min(t, 9000)
              REAL_DANMAKU_DURATION = Math.min(MAX_DANMAKU_DURATION_HIGH_DENSITY,
                      REAL_DANMAKU_DURATION);
              // t = max(t, 4000)       
              REAL_DANMAKU_DURATION = Math.max(MIN_DANMAKU_DURATION, REAL_DANMAKU_DURATION);
              
              CURRENT_DISP_WIDTH = (int) viewportWidth;
              CURRENT_DISP_HEIGHT = (int) viewportHeight;
              CURRENT_DISP_SIZE_FACTOR = viewportSizeFactor;
          }
          return sizeChanged;
      }
      //修正弹幕最长时长
      publicvoidupdateMaxDanmakuDuration(){
          long maxScrollDuration = (MAX_Duration_Scroll_Danmaku == null ? 0: MAX_Duration_Scroll_Danmaku.value),
                maxFixDuration = (MAX_Duration_Fix_Danmaku == null ? 0 : MAX_Duration_Fix_Danmaku.value),
                maxSpecialDuration = (MAX_Duration_Special_Danmaku == null ? 0: MAX_Duration_Special_Danmaku.value);

          MAX_DANMAKU_DURATION = Math.max(maxScrollDuration, maxFixDuration);
          MAX_DANMAKU_DURATION = Math.max(MAX_DANMAKU_DURATION, maxSpecialDuration);

          MAX_DANMAKU_DURATION = Math.max(COMMON_DANMAKU_DURATION, MAX_DANMAKU_DURATION);
          MAX_DANMAKU_DURATION = Math.max(REAL_DANMAKU_DURATION, MAX_DANMAKU_DURATION);
      }

          DanmakuFactory创建弹幕主要是计算了弹幕时长,然后根据不同类型创建不同的弹幕。

           到此CacheManagingDrawTask的loadDanmakus方法走完了。loadDanmakus方法主要从 mParser里的JSONSource解析弹幕数据源,根据不同类型的type用DanmakuFactory创建不同的Danmaku,分别计算Duration,最后存放到一个Danmakus对象里。

            继续回到刚才的prepare方法,往下继续执行:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

     @Override
     publicvoidprepare(){
         assert (mParser != null);
         loadDanmakus(mParser);//走完了
         mCacheManager.begin();//走这个
     }
      //CacheManager的方法
         publicvoidbegin(){
             mEndFlag = false;
             //创建一个HandlerThread用于在工作线程处理事务
             if (mThread == null) {
                 mThread = new HandlerThread("DFM Cache-Building Thread");
                 mThread.start();
             }
             //创建一个HandlerHandlerThread搭配用
             if (mHandler == null)
                 mHandler = new CacheHandler(mThread.getLooper());
             mHandler.begin();// 走到这里
         }
    //HandlerThreadbegin方法
             publicvoidbegin(){
                 sendEmptyMessage(PREPARE);
             ......
             }

           我们可以看到创建了一个HandlerThread,然后创建了一个CacheHandler,所以CacheHandler发送消息后,处理消息内容都是在子线程。
           然后发送了PREPARE消息,然后就是回调handleMessage方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34

         DrawingCachePoolManager mCachePoolManager = new DrawingCachePoolManager();
         //创建一个缓存个数上限为800FinitePool
            Pool<DrawingCache> mCachePool = Pools.finitePool(mCachePoolManager, 800);
            //PoolsfinitePool方法
            publicstatic <T extends Poolable<T>> Pool<T> finitePool(PoolableManager<T> manager, int limit){
            returnnew FinitePool<T>(manager, limit);
         }
         //CacheHandlerhandleMessage方法
              publicvoidhandleMessage(Message msg){
                   int what = msg.what;
                   switch (what) {
                       case PREPARE:
                           evictAllNotInScreen();//清除所有不在屏幕内的缓存,此时还没有缓存
                           for (int i = 0; i < 300; i++) {//在池里放300个预留缓存,以链式存储方式存放
                               mCachePool.release(new DrawingCache());
                           }

                              ......
                              }
             }
    //FinitePoolrelease方法:回收缓存对象,并且用头插法,以链式存储(类似链表)   
       publicvoidrelease(T element){
           if (!element.isPooled()) {
               if (mInfinite || mPoolCount < mLimit) {
                   mPoolCount++;
                   element.setNextPoolable(mRoot);
                   element.setPooled(true);
                   mRoot = element;
               }
               mManager.onReleased(element);
           } else {
               System.out.print("[FinitePool] Element is already in pool: " + element);
           }
       }

           处理完PREPARE消息后,会继续进入DISPATCH_ACTIONS逻辑处理中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34

             ......
              case DISPATCH_ACTIONS:
                  long delayed = dispatchAction();//走到这里
                  if (delayed <= 0) {// true
                   //会没隔半条弹幕时间发送一次DISPATCH_ACTIONS消息
                      delayed = mContext.mDanmakuFactory.MAX_DANMAKU_DURATION / 2;
                  }
               sendEmptyMessageDelayed(DISPATCH_ACTIONS, delayed);
                  break;
              ......
        /*----------dispatchAction方法START----------*/       
        privatelongdispatchAction(){
       
    ...省略一些第一次不会执行的逻辑...
               
                removeMessages(BUILD_CACHES);
                sendEmptyMessage(BUILD_CACHES);//发送BUILD_CACHES消息
                return0;
        }
        /*----------dispatchAction方法END----------*/
              ......
                    case BUILD_CACHES:
                        removeMessages(BUILD_CACHES);
                        boolean repositioned = ((mTaskListener != null
                        && mReadyState == false) || mSeekedFlag);// true
                        prepareCaches(repositioned);//调用prepareCaches方法
                        if (repositioned)
                            mSeekedFlag = false;
                        if (mTaskListener != null && mReadyState == false) {
                            mTaskListener.ready();//然后回到mTaskListener监听ready方法
                            mReadyState = true;
                        }
                        break;
                    ......

           我们发现,处理接着处理DISPATCH_ACTIONS消息时,会每隔半条弹幕时间发送一次DISPATCH_ACTIONS消息。
           处理DISPATCH_ACTIONS消息内会执行dispatchAction方法,这个方法内逻辑情况比较多,我们先挖个坑,先把刚开始时会走的逻辑执行了,其他逻辑以后用时会填上。(挖坑 ^O_O^)

           
    首次调用dispatchAction方法内发送了BUILD_CACHES消息消息,会先调用prepareCaches(true)方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51

    privatelongprepareCaches(boolean repositioned){
                    long curr = mCacheTimer.currMillisecond;// 0
                    //3条弹幕时间
                    long end = curr + mContext.mDanmakuFactory.MAX_DANMAKU_DURATION * mScreenSize;
                    if (end < mTimer.currMillisecond) {
                        return0;
                    }
                    long startTime = SystemClock.uptimeMillis();
                    IDanmakus danmakus = null;
                    int tryCount = 0;
                    boolean hasException = false;
                    do {
                        try {
                                 //截取三条弹幕时间中所有的弹幕
                            danmakus = danmakuList.subnew(curr, end);
                        } catch (Exception e) {
                            hasException = true;
                            SystemClock.sleep(10);
                        }
                    } while (++tryCount < 3 && danmakus == null && hasException);//截取成功后跳出循环
                    if (danmakus == null) {
                        mCacheTimer.update(end);
                        return0;
                    }
                                      ......
                    IDanmakuIterator itr = danmakus.iterator();
                    BaseDanmaku item = null;
                    int sizeInScreen = danmakus.size();
                    while (!mPause && !mCancelFlag) {//
                        boolean hasNext = itr.hasNext();
                        if (!hasNext) {
                            break;
                        }
                        item = itr.next();

                                                 ......

                        // build cache ,省略了一些障眼法,这才是重点,建立缓存
                        if (buildCache(item, false) == RESULT_FAILED) {
                            break;
                        }
                                                ......
                    }
                    ......
                    if (item != null) {//截取的最后一条弹幕,更新缓存定时器时间
                        mCacheTimer.update(item.time);
                    } else {
                        mCacheTimer.update(end);
                    }
                    return consumingTime;
                }

           为截取的每一条弹幕建立缓存会调用buildCache(item, false)方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71

           privatebytebuildCache(BaseDanmaku item, boolean forceInsert){

               // measure ,先测量每一条弹幕的宽高
               if (!item.isMeasured()) {
                   item.measure(mDisp, true);
               }

               DrawingCache cache = null;
               try {
                   // try to find reuseable cache, mCaches缓存的20条内查找和目标弹幕样式完全一样的弹幕(文字、大小、边框、下划线、颜色完全相同)
                   BaseDanmaku danmaku = findReuseableCache(item, true, 20);
                   if (danmaku != null) {//如果查找出了这样的弹幕
                       cache = (DrawingCache) danmaku.cache;
                   }
                   if (cache != null) {//如果找到的弹幕有缓存
                       cache.increaseReference();//则将引用计数 +1
                       item.cache = cache;//将目标弹幕缓存的引用指向查找出来的弹幕缓存,即多个引用指向同一个对象
                       //将这个目标弹幕的引用放入缓存Danmakus(mCaches),同时更新已使用大小mRealSize
                       mCacheManager.push(item, 0, forceInsert);
                       return RESULT_SUCCESS;
                   }

                   // try to find reuseable cache from timeout || no-refrerence caches
                   //如果上述的查找样式完全相同的弹幕没有找到,则在前50条缓存中查找对比当前时间已经过时的
                   //,没有被重复引用的(只有上面那种情况才会增加引用计数,其他情况都不会)
                   //,而且宽高和目标弹幕差值在规定范围内的弹幕
                   danmaku = findReuseableCache(item, false, 50);
                   if (danmaku != null) {// 如果找到了这样的弹幕
                       cache = (DrawingCache) danmaku.cache;
                   }
                   if (cache != null) {//如果找到的弹幕有缓存
                       danmaku.cache = null;//先清除过时弹幕的缓存
                       //再根据目标弹幕样式,重新设置缓存(为每条弹幕创建一个bitmapcanvas,然后画出边框、下划线、文字等等)
                       cache = DanmakuUtils.buildDanmakuDrawingCache(item, mDisp, cache);  //redraw
                       item.cache = cache;//将缓存应用赋给目标弹幕
                       mCacheManager.push(item, 0, forceInsert);//将这个目标弹幕的引用放入缓存Danmakus(mCaches),同时更新已使用大小mRealSize
                       return RESULT_SUCCESS;
                   }

    //如果上述两次查找缓存都没找到,则进入下面逻辑
                   // guess cache size
                   if (!forceInsert) {//如果forceInsertfalse,则表示不检测内存超出
                    //计算此弹幕bitmap的大小,width * height * 4
                    //(因为用native创建的BitmapConfigARGB_8888,所以一个像素占4个字节)
                       int cacheSize = DanmakuUtils.getCacheSize((int) item.paintWidth,
                               (int) item.paintHeight);
                       //如果当前已经使用大小 + 此弹幕缓存大小 > 设置的最大内存(2/3 应用内存)       
                       if (mRealSize + cacheSize > mMaxSize) {//没有超
                           return RESULT_FAILED;
                       }
                   }
    //FinitePool中的300DrawingCache对象中取出来一个
                   cache = mCachePool.acquire();
                   //如果从上面的FinitePool取完了,则会直接new一个DrawingCache,配置DrawingCache
                   cache = DanmakuUtils.buildDanmakuDrawingCache(item, mDisp, cache);
                   item.cache = cache;
                   //item存入mCaches缓存,同时更新已使用大小mRealSize
                   boolean pushed = mCacheManager.push(item, sizeOf(item), forceInsert);
                   if (!pushed) {//如果item存放失败(使用内存超出规定大小)
                       releaseDanmakuCache(item, cache);//释放DrawingCache
                   }
                   return pushed ? RESULT_SUCCESS : RESULT_FAILED;

               } catch (OutOfMemoryError e) {
                   releaseDanmakuCache(item, cache);
                   return RESULT_FAILED;
               } catch (Exception e) {
                   releaseDanmakuCache(item, cache);
                   return RESULT_FAILED;
               }
           }

          buildCache(item, false)为每一条弹幕建立缓存,其中有几处:

    • 先测量弹幕的宽高
    • 在mCaches缓存的20条内查找和目标弹幕样式完全一样的弹幕(文字、大小、边框、下划线、颜色完全相同)
    • 如果上述的查找样式完全相同的弹幕没有找到,则在前50条缓存中查找对比当前时间已经过时的 ,没有被重复引用的(只有上面那种情况才会增加引用计数,其他情况都不会),而且宽高和目标弹幕差值在规定范围内的弹幕,再根据目标弹幕样式,重新设置缓存(为每条弹幕创建一个bitmap和canvas,然后画出边框、下划线、文字等等)
    • 如果上述两次查找缓存都没找到,则从FinitePool中取出一个,没有就new一个,然后同上配置DrawingCache

           1)我们一个一个来,先测量:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    //弹幕的基类都是BaseDanmaku,只有子类R2LDanmaku重写了measure方法
        @Override//R2LDanmakumeasure方法
        publicvoidmeasure(IDisplayer displayer, boolean fromWorkerThread){
            super.measure(displayer, fromWorkerThread);//调用了父类的方法
            mDistance = (int) (displayer.getWidth() + paintWidth);//滚动弹幕的距离都是视图宽度+弹幕宽度,很好理解
            mStepX = mDistance / (float) duration.value; //每秒步长就是总滚动距离除以弹幕时长
        }
        //父类BaseDanmakumeasure方法
        publicvoidmeasure(IDisplayer displayer, boolean fromWorkerThread){
            displayer.measure(this, fromWorkerThread);//AndroidDisplayermeasure方法
            this.measureResetFlag = flags.MEASURE_RESET_FLAG;//设置已经测量过了的标签
        }

           接着会调用AndroidDisplayer的measure方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

       @Override
       publicvoidmeasure(BaseDanmaku danmaku, boolean fromWorkerThread){
             ...设置画笔style,color,alpha,省略...
           calcPaintWH(danmaku, paint, fromWorkerThread);//计算宽高
             ...设置画笔style,color,alpha,省略...
       }
    private BaseCacheStuffer sStuffer = new SimpleTextCacheStuffer();//默认是SimpleTextCacheStuffer
       privatevoidcalcPaintWH(BaseDanmaku danmaku, TextPaint paint, boolean fromWorkerThread){
           sStuffer.measure(danmaku, paint, fromWorkerThread);//sStuffer就是我们在MainActivity里配置DanmakuContext时设置的,默认是SimpleTextCacheStuffer
          
           ...加上描边,padding等额外值,省略...
       }

           还记得在MainActivity里配置DanmakuContext吗?当时是这么写的:
    .setCacheStuffer(new SpannedCacheStuffer(), mCacheStufferAdapter) // 图文混排使用SpannedCacheStuffer
    // .setCacheStuffer(new BackgroundCacheStuffer()) // 绘制背景使用BackgroundCacheStuffer

           比如SpannedCacheStuffer的measure方法是这样的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    @Override//SpannedCacheStuffermeasure方法
      publicvoidmeasure(BaseDanmaku danmaku, TextPaint paint, boolean fromWorkerThread){
          if (danmaku.text instanceof Spanned) {
              CharSequence text = danmaku.text;
              if (text != null) {
               //可看到将弹幕的宽高,文字等信息包在了一个StaticLayout对象中,然后付给danmakuobj对象
                  StaticLayout staticLayout = new StaticLayout(text, paint, (int) Math.ceil(StaticLayout.getDesiredWidth(danmaku.text, paint)), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true);
                  danmaku.paintWidth = staticLayout.getWidth();
                  danmaku.paintHeight = staticLayout.getHeight();
                  danmaku.obj = new SoftReference<>(staticLayout);
                  return;
              }
          }
          super.measure(danmaku, paint, fromWorkerThread);//如果不是图文混排类型,则调用父类SimpleTextCacheStuffer的方法
      }

           可以看到measure方法创建了一个StaticLayout对象,并将它的软引用赋给了danmaku的obj属性;如果是图文混排类型弹幕,则danmaku.obj不为空;如果是普通弹幕则danmaku.obj为空
           BackgroundCacheStuffer
    也差不多,都是对弹幕样式的一些改造。

           然后我们看SimpleTextCacheStuffer的measure方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44

    //SimpleTextCacheStuffermeasure方法  
       publicvoidmeasure(BaseDanmaku danmaku, TextPaint paint, boolean fromWorkerThread){
           if (mProxy != null) {//这个mProxy BaseCacheStuffer.Proxy类型的对象,也是初始化DanmakuContext调用setCacheStuffer(cacheStuffer, proxy)时设置的
               mProxy.prepareDrawing(danmaku, fromWorkerThread);//根据你的条件检查是否需要需要更新弹幕
           }
           float w = 0;
           Float textHeight = 0f;
           if (danmaku.lines == null) {//不是多行文本
               if (danmaku.text == null) {
                   w = 0;
               } else {
                   w = paint.measureText(danmaku.text.toString());//测量出文字宽度
                   textHeight = getCacheHeight(danmaku, paint);//计算出文字高度
               }
               danmaku.paintWidth = w;
               danmaku.paintHeight = textHeight;
           } else {//如果是多行文本
               textHeight = getCacheHeight(danmaku, paint);//计算出单行文字高度
               for (String tempStr : danmaku.lines) {//计算出多行文本总宽高
                   if (tempStr.length() > 0) {
                       float tr = paint.measureText(tempStr);
                       w = Math.max(tr, w);
                   }
               }
               danmaku.paintWidth = w;
               danmaku.paintHeight = danmaku.lines.length * textHeight;
           }
       }
       privatefinalstatic Map<Float, Float> sTextHeightCache = new HashMap<Float, Float>();//key是字号大小,value是字体高度
       protected Float getCacheHeight(BaseDanmaku danmaku, Paint paint){
           Float textSize = paint.getTextSize();
           Float textHeight = sTextHeightCache.get(textSize);
           if (textHeight == null) {
               Paint.FontMetrics fontMetrics = paint.getFontMetrics();
               //Android对文字绘制有些特殊,基准点是baseline,也就是例如canvas.drawText(text, baseX, baseY, textPaint)中写入的baseY大小
               //Ascentbaseline之上字符最高处的y值;
                     //Descentbaseline之下字符最低处的y值;
                     //Leading其实是上一行字符的descent到下一行的ascent之间的距离。
                     //所以文本高度就是descent - ascent + leading
               textHeight = fontMetrics.descent - fontMetrics.ascent + fontMetrics.leading;
               sTextHeightCache.put(textSize, textHeight);
           }
           return textHeight;
       }

           这样就计算完了每一条弹幕的宽高,完成了测量。

           2) 在mCaches缓存的20条内查找和目标弹幕样式完全一样的弹幕(文字、大小、边框、下划线、颜色完全相同):
           先回到buildCache方法中这个位置。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72

       privatebytebuildCache(BaseDanmaku item, boolean forceInsert){//item, false

             ...测量已经完成...
                 DrawingCache cache = null;
                 try {
                     // try to find reuseable cache, mCaches缓存的20条内查找和目标弹幕样式完全一样的弹幕(文字、大小、边框、下划线、颜色完全相同)
                     BaseDanmaku danmaku = findReuseableCache(item, true, 20);
                     if (danmaku != null) {//如果查找出了这样的弹幕
                         cache = (DrawingCache) danmaku.cache;
                     }
                     if (cache != null) {//如果找到的弹幕有缓存
                         cache.increaseReference();//则将引用计数 +1
                         item.cache = cache;//将目标弹幕缓存的引用指向查找出来的弹幕缓存,即多个引用指向同一个对象
                         //将这个目标弹幕的引用放入缓存Danmakus(mCaches),同时更新已使用大小mRealSize
                         mCacheManager.push(item, 0, forceInsert);
                         return RESULT_SUCCESS;
                     }
                 ......   
       }
         //mCaches缓存的20条内查找和目标弹幕样式完全一样的弹幕(文字、大小、边框、下划线、颜色完全相同)            
         private BaseDanmaku findReuseableCache(BaseDanmaku refDanmaku,
                                                boolean strictMode,
                                                int maximumTimes) {//item true 20
             IDanmakuIterator it = mCaches.iterator();
    ......
             int count = 0;
             while (it.hasNext() && count++ < maximumTimes) {  // limit maximum times 20
                 BaseDanmaku danmaku = it.next();
                 IDrawingCache<?> cache = danmaku.getDrawingCache();
                 if (cache == null || cache.get() == null) {
                     continue;
                 }
                 //对比mCaches中的弹幕和目标的内幕文字、大小、边框、下划线、颜色是否完全相同
                 if (danmaku.paintWidth == refDanmaku.paintWidth
                         && danmaku.paintHeight == refDanmaku.paintHeight
                         && danmaku.underlineColor == refDanmaku.underlineColor
                         && danmaku.borderColor == refDanmaku.borderColor
                         && danmaku.textColor == refDanmaku.textColor
                         && danmaku.text.equals(refDanmaku.text)) {
                     return danmaku;
                 }
                 if (strictMode) {//true
                     continue;
                 }
             ......
             }
             returnnull;
         }
         //CacheManagingDrawTask.CacheManagerpush方法
         //将这个目标弹幕的引用放入缓存Danmakus(mCaches),同时更新已使用大小mRealSize
         privatebooleanpush(BaseDanmaku item, int itemSize, boolean forcePush){//item0false
             int size = itemSize; //0
    ......
    //这里注意mCachesDanmakus类型,addItem方法里面实现其实是类型为TreeSet的集合去添加,如果是同一个对象,则不会添加
             this.mCaches.addItem(item);
             mRealSize += size;//因为已经存在相同的缓存,因此已经使用缓存总大小不再增加
             returntrue;
         }
     //DanmakusaddItem方法
     publicbooleanaddItem(BaseDanmaku item){
         if (items != null) {//items 类型为TreeSet
             try {
                 if (items.add(item)) {//如果是相同对象,则返回falsemSize个数不会增加
                     mSize++;
                     returntrue;
                 }
             } catch (Exception e) {
                 e.printStackTrace();
             }
         }
         returnfalse;
     }

           上述情况仅仅在相同样式,大小,颜色等都相同的弹幕第二次和以后的才会进入这段逻辑。对于不同的弹幕不会进入这个逻辑。(而且即使是相同弹幕,mCaches也只会存一个对象的,因为内部TreeSet的特性)
           所以我们继续看下一种逻辑。

           3)在前50条缓存中查找对比当前时间已经过时的 ,没有被重复引用的(只有上面那种情况才会增加引用计数,其他情况都不会),而且宽高和目标弹幕差值在规定范围内的弹幕,再根据目标弹幕样式,重新设置缓存(为每条弹幕创建一个bitmap和canvas,然后画出边框、下划线、文字等等):
           继续回到buildCache方法这个位置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64

    privatebytebuildCache(BaseDanmaku item, boolean forceInsert){//item, false
                     ...测量过了...
                     ...第一策略已经pass...
                              // try to find reuseable cache from timeout || no-refrerence caches
                      //如果上述的查找样式完全相同的弹幕没有找到,则在前50条缓存中查找对比当前时间已经过时的
                      //,没有被重复引用的(只有上面那种情况才会增加引用计数,其他情况都不会)
                      //,而且宽高和目标弹幕差值在规定范围内的弹幕
                      danmaku = findReuseableCache(item, false, 50);
                      if (danmaku != null) {// 如果找到了这样的弹幕
                          cache = (DrawingCache) danmaku.cache;
                      }
                      if (cache != null) {//如果找到的弹幕有缓存
                          danmaku.cache = null;//先清除过时弹幕的缓存
                          //再根据目标弹幕样式,重新设置缓存(为每条弹幕创建一个bitmapcanvas,然后画出边框、下划线、文字等等)
                          cache = DanmakuUtils.buildDanmakuDrawingCache(item, mDisp, cache);  //redraw
                          item.cache = cache;//将缓存应用赋给目标弹幕
                          mCacheManager.push(item, 0, forceInsert);//将这个目标弹幕的引用放入缓存Danmakus(mCaches),同时更新已使用大小mRealSize
                          return RESULT_SUCCESS;
                      }
                  ......   
    }
          private BaseDanmaku findReuseableCache(BaseDanmaku refDanmaku,
                                                 boolean strictMode,
                                                 int maximumTimes) {//item,false,50
              IDanmakuIterator it = mCaches.iterator();
              int slopPixel = 0;
              if (!strictMode) {//进入逻辑,非严苛模式
                  slopPixel = mDisp.getSlopPixel() * 2;//允许目标弹幕与mCaches中找到的弹幕宽高偏差
              }
              int count = 0;
              while (it.hasNext() && count++ < maximumTimes) {  // limit maximum times 20
                  BaseDanmaku danmaku = it.next();
                  IDrawingCache<?> cache = danmaku.getDrawingCache();
                  if (cache == null || cache.get() == null) {
                      continue;
                  }
                  //在这种第二策略中这段逻辑根本不会执行,因为以已经被上面的第一策略拦截了
                  if (danmaku.paintWidth == refDanmaku.paintWidth
                          && danmaku.paintHeight == refDanmaku.paintHeight
                          && danmaku.underlineColor == refDanmaku.underlineColor
                          && danmaku.borderColor == refDanmaku.borderColor
                          && danmaku.textColor == refDanmaku.textColor
                          && danmaku.text.equals(refDanmaku.text)) {
                      return danmaku;
                  }
                  if (strictMode) {//false
                      continue;
                  }
                  if (!danmaku.isTimeOut()) {//还必须在mCaches中过时的弹幕中查找
                      break;
                  }
                  if (cache.hasReferences()) {//如果是相同弹幕被重新引用的,第二策略没有这样的
                      continue;
                  }
                  //所以会走到这里,比较mCaches中过时的弹幕和目标弹幕宽高在不在允许的偏差内,如果在就返回查找出的这个弹幕
                  float widthGap = cache.width() - refDanmaku.paintWidth;
                  float heightGap = cache.height() - refDanmaku.paintHeight;
                  if (widthGap >= 0 && widthGap <= slopPixel &&
                          heightGap >= 0 && heightGap <= slopPixel) {
                      return danmaku;
                  }
              }
              returnnull;
          }

           如果在上述第二策略中,在过时的缓存中找到了和目标弹幕宽高差不多的缓存项,则根据目标弹幕样式,重新设置缓存(为每条弹幕创建一个bitmap和canvas,然后画出边框、下划线、文字等等),调用DanmakuUtils.buildDanmakuDrawingCache(item,mDisp, cache)方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

      publicstatic DrawingCache buildDanmakuDrawingCache(BaseDanmaku danmaku, IDisplayer disp,
              DrawingCache cache) {
          if (cache == null)
              cache = new DrawingCache();
    //组建弹幕缓存(bitmap,canvas)
          cache.build((int) Math.ceil(danmaku.paintWidth), (int) Math.ceil(danmaku.paintHeight), disp.getDensityDpi(), false);
          DrawingCacheHolder holder = cache.get();
          if (holder != null) {
           //绘制弹幕内容
              ((AbsDisplayer) disp).drawDanmaku(danmaku, holder.canvas, 0, 0, true);
              if(disp.isHardwareAccelerated()) {//如果有硬件加速
               //超过一屏的弹幕要切割
                  holder.splitWith(disp.getWidth(), disp.getHeight(), disp.getMaximumCacheWidth(),
                          disp.getMaximumCacheHeight());
              }
          }
          return cache;
      }

           重新设置缓存分三步:1.组建弹幕缓存,2.绘制弹幕内容,3.切割超过一屏的弹幕。

           No.1 组建弹幕缓存:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33

    //DrawingCachebuild方法
       publicvoidbuild(int w, int h, int density, boolean checkSizeEquals){//checkSizeEqualsfalse
           final DrawingCacheHolder holder = mHolder;
           //每个DrawingCache都有一个DrawingCacheHolder
           holder.buildCache(w, h, density, checkSizeEquals);//DrawingCacheHolderbuildCache方法
           mSize = mHolder.bitmap.getRowBytes() * mHolder.bitmap.getHeight();//返回创建的bitmap的大小
       }
       //DrawingCacheHolderbuildCache方法
       publicvoidbuildCache(int w, int h, int density, boolean checkSizeEquals){
           boolean reuse = checkSizeEquals ? (w == width && h == height) : (w <= width && h <= height);//检测大小宽高相等小于已经缓存的bitmap宽高
           if (reuse && bitmap != null) {//如果能够复用bitmap
               bitmap.eraseColor(Color.TRANSPARENT);//擦出之前的颜色
               canvas.setBitmap(bitmap);//Canvas重新预设bitmap
               recycleBitmapArray();//回收超过一屏弹幕切割后的bitmap数组,这个接下来会讲
               return;
           }
           if (bitmap != null) {//如果不能复用,则回收旧的缓存bitmap
               recycle();
           }
           width = w;
           height = h;
           bitmap = NativeBitmapFactory.createBitmap(w, h, Bitmap.Config.ARGB_8888);//native方法创建一个bitmap
           if (density > 0) {//设置density
               mDensity = density;
               bitmap.setDensity(density);
           }
           //设置canvas
           if (canvas == null){
               canvas = new Canvas(bitmap);
               canvas.setDensity(density);
           }else
               canvas.setBitmap(bitmap);
       }

           组建弹幕缓存就是为个DrawingCache根据目标弹幕大小创建bitmap和canvas。

           No.2 绘制弹幕内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94

      publicsynchronizedvoiddrawDanmaku(BaseDanmaku danmaku, Canvas canvas,
      float left, float top, boolean fromWorkerThread) {//danmaku, holder.canvas, 0, 0, true
          float _left = left;
          float _top = top;
         
    ...一些杂项,忽略...

          TextPaint paint = getPaint(danmaku, fromWorkerThread);//获取画笔
          //绘制背景,sStuffer可以自己设置,默认是SimpleTextCacheStuffer,默认drawBackground为空
          //这个可以自己扩展,上面讲过
          sStuffer.drawBackground(danmaku, canvas, _left, _top);
          if (danmaku.lines != null) {//如果是多行文本
              String[] lines = danmaku.lines;
              if (lines.length == 1) {//多行文本行数为1
                  if (hasStroke(danmaku)) {//如果有描边,则绘制描边
                   //重设画笔(绘制描边)
                      applyPaintConfig(danmaku, paint, true);
                      float strokeLeft = left;
                      float strokeTop = top - paint.ascent();
                      ......
                      //绘制描边
                      sStuffer.drawStroke(danmaku, lines[0], canvas, strokeLeft, strokeTop, paint);
                  }
                  //再次重设画笔(绘制文字)
                  applyPaintConfig(danmaku, paint, false);
                  //绘制文字
                  sStuffer.drawText(danmaku, lines[0], canvas, left, top - paint.ascent(), paint, fromWorkerThread);
              } else {//多行文本行数大于1
               //先计算每行文本的高度
                  float textHeight = (danmaku.paintHeight - 2 * danmaku.padding) / lines.length;
                  //循环绘制每一行文本
                  for (int t = 0; t < lines.length; t++) {
                      ......
                      if (hasStroke(danmaku)) {//如果有描边,则绘制描边
                       //重设画笔(绘制描边)
                          applyPaintConfig(danmaku, paint, true);
                          float strokeLeft = left;
                          float strokeTop = t * textHeight + top - paint.ascent();
                                       ......
                          //绘制描边
                          sStuffer.drawStroke(danmaku, lines[t], canvas, strokeLeft, strokeTop, paint);
                      }
                      //再次重设画笔(绘制文字)
                      applyPaintConfig(danmaku, paint, false);
                      //绘制文字
                      sStuffer.drawText(danmaku, lines[t], canvas, left, t * textHeight + top - paint.ascent(), paint, fromWorkerThread);
                  }
              }
          } else {//如果是单行文本
              if (hasStroke(danmaku)) {//如果有描边,则绘制描边
               //重设画笔(绘制描边)
                  applyPaintConfig(danmaku, paint, true);
                  float strokeLeft = left;
                  float strokeTop = top - paint.ascent();
                     ......
                  //绘制描边
                  sStuffer.drawStroke(danmaku, null, canvas, strokeLeft, strokeTop, paint);
              }
             //再次重设画笔(绘制文字)
              applyPaintConfig(danmaku, paint, false);
              //绘制文字
              sStuffer.drawText(danmaku, null, canvas, left, top - paint.ascent(), paint, fromWorkerThread);
          }

          // draw underline
          if (danmaku.underlineColor != 0) {//绘制下划线(if
              Paint linePaint = getUnderlinePaint(danmaku);
              float bottom = _top + danmaku.paintHeight - UNDERLINE_HEIGHT;
              canvas.drawLine(_left, bottom, _left + danmaku.paintWidth, bottom, linePaint);
          }

          //draw border
          if (danmaku.borderColor != 0) {//绘制外框
              Paint borderPaint = getBorderPaint(danmaku);
              canvas.drawRect(_left, _top, _left + danmaku.paintWidth, _top + danmaku.paintHeight,
                      borderPaint);
          }

      }
      //设置画笔
      privatevoidapplyPaintConfig(BaseDanmaku danmaku, Paint paint, boolean stroke){

             ......
              if (stroke) {
                  paint.setStyle(HAS_PROJECTION ? Style.FILL : Style.STROKE);
                  paint.setColor(danmaku.textShadowColor & 0x00FFFFFF);
                  int alpha = HAS_PROJECTION ? sProjectionAlpha : AlphaValue.MAX;
                  paint.setAlpha(alpha);
              } else {
                  paint.setStyle(Style.FILL);
                  paint.setColor(danmaku.textColor & 0x00FFFFFF);
                  paint.setAlpha(AlphaValue.MAX);
              }
      }

           上述就是绘制弹幕内容过程,主要就是sStuffer的drawStroke,drawText方法。如果你在DanmakuContext中没有设置CacheStuffer,则上述drawDanmaku方法中的sStuffer为默认的SimpleTextCacheStuffer。
           drawStroke方法及其扩展都一样:

    1
    2
    3
    4
    5
    6
    7
    8

    //SimpleTextCacheStufferdrawStroke方法
       publicvoiddrawStroke(BaseDanmaku danmaku, String lineText, Canvas canvas, float left, float top, Paint paint){
           if (lineText != null) {
               canvas.drawText(lineText, left, top, paint);
           } else {
               canvas.drawText(danmaku.text.toString(), left, top, paint);
           }
       }

           我们设了SpannedCacheStuffer, drawText方法有些区别:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57

    //SimpleTextCacheStufferdrawText方法
       publicvoiddrawText(BaseDanmaku danmaku, String lineText, Canvas canvas, float left, float top, TextPaint paint, boolean fromWorkerThread){
           if (lineText != null) {
               canvas.drawText(lineText, left, top, paint);
           } else {
               canvas.drawText(danmaku.text.toString(), left, top, paint);
           }
       }
       //SpannedCacheStufferdrawText方法
       publicvoiddrawText(BaseDanmaku danmaku, String lineText, Canvas canvas, float left, float top, TextPaint paint, boolean fromWorkerThread){
           if (danmaku.obj == null) {//普通弹幕
               super.drawText(danmaku, lineText, canvas, left, top, paint, fromWorkerThread);
               return;
           }
           //如果是图文混排弹幕
           SoftReference<StaticLayout> reference = (SoftReference<StaticLayout>) danmaku.obj;
           StaticLayout staticLayout = reference.get();
           //按位与,判断标志位是否有效。这里判断是否请求重新测量
           boolean requestRemeasure = 0 != (danmaku.requestFlags & BaseDanmaku.FLAG_REQUEST_REMEASURE);
           //判断是否请求重绘
           boolean requestInvalidate = 0 != (danmaku.requestFlags & BaseDanmaku.FLAG_REQUEST_INVALIDATE);

           if (requestInvalidate || staticLayout == null) {//如果请求重绘或者staticLayout 软引用被回收了
               if (requestInvalidate) {
                //与非操作,清除标志位。清除请求重绘标志位
                   danmaku.requestFlags &= ~BaseDanmaku.FLAG_REQUEST_INVALIDATE;
               } elseif (mProxy != null) {//这个在设置DanmakuContext时设置,上面讲过,可以自己扩展
                   mProxy.prepareDrawing(danmaku, fromWorkerThread);
               }
               CharSequence text = danmaku.text;
               if (text != null) {
                   if (requestRemeasure) {//重新测量
                       staticLayout = new StaticLayout(text, paint, (int) Math.ceil(StaticLayout.getDesiredWidth(danmaku.text, paint)), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true);
                       danmaku.paintWidth = staticLayout.getWidth();
                       danmaku.paintHeight = staticLayout.getHeight();
                       danmaku.requestFlags &= ~BaseDanmaku.FLAG_REQUEST_REMEASURE;//清除标志位
                   } else {//不用重新测量
                       staticLayout = new StaticLayout(text, paint, (int) danmaku.paintWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true);
                   }
                   danmaku.obj = new SoftReference<>(staticLayout);
               } else {
                   return;
               }
           }
           //staticLayout可以继续用
           boolean needRestore = false;
           if (left != 0 && top != 0) {
               canvas.save();
               canvas.translate(left, top + paint.ascent());
               needRestore = true;
           }
           //绘制弹幕内容
           staticLayout.draw(canvas);
           if (needRestore) {
               canvas.restore();
           }
       }

           绘制弹幕内容就完了,主要是绘制描边,绘制文字,绘制下划线,边框等等。

           No.3 切割超过一屏的弹幕:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49

    //DrawingCacheHoldersplitWith方法
       publicvoidsplitWith(int dispWidth, int dispHeight, int maximumCacheWidth, int maximumCacheHeight){
           recycleBitmapArray();//回收已存的bitmapArray数组
           if (width <= 0 || height <= 0 || bitmap == null) {
               return;
           }
           //如果弹幕的宽高都没有超过屏幕宽高,则不切割bitmap
           if (width <= maximumCacheWidth && height <= maximumCacheHeight) {
               return;
           }
           //切割超过一屏的弹幕
           maximumCacheWidth = Math.min(maximumCacheWidth, dispWidth);
           maximumCacheHeight = Math.min(maximumCacheHeight, dispHeight);
           //计算弹幕宽高是屏幕宽高的倍数,然后决定切割成多少块
           int xCount = width / maximumCacheWidth + (width % maximumCacheWidth == 0 ? 0 : 1);
           int yCount = height / maximumCacheHeight + (height % maximumCacheHeight == 0 ? 0 : 1);
           //然后求切割后弹幕每一块宽和高的平均值
           int averageWidth = width / xCount;
           int averageHeight = height / yCount;
           //建立二位bitmap数组,用于存放切割碎片
           final Bitmap[][] bmpArray = new Bitmap[yCount][xCount];
           if (canvas == null){
               canvas = new Canvas();
               if (mDensity > 0) {
                   canvas.setDensity(mDensity);
               }
           }
           Rect rectSrc = new Rect();
           Rect rectDst = new Rect();
           //切割bitmapbitmapArray
           for (int yIndex = 0; yIndex < yCount; yIndex++) {
               for (int xIndex = 0; xIndex < xCount; xIndex++) {
                //创建每一块小块bitmap
                   Bitmap bmp = bmpArray[yIndex][xIndex] = NativeBitmapFactory.createBitmap(
                           averageWidth, averageHeight, Bitmap.Config.ARGB_8888);
                   if (mDensity > 0) {
                       bmp.setDensity(mDensity);
                   }
                   //将弹幕的大bitmap绘制进每个小块bitmap
                   canvas.setBitmap(bmp);
                   int left = xIndex * averageWidth, top = yIndex * averageHeight;
                   rectSrc.set(left, top, left + averageWidth, top + averageHeight);
                   rectDst.set(0, 0, bmp.getWidth(), bmp.getHeight());
                   canvas.drawBitmap(bitmap, rectSrc, rectDst, null);
               }
           }
           canvas.setBitmap(bitmap);
           bitmapArray = bmpArray;
       }

           切割超过一屏的弹幕,就像玩切田字格游戏一样,完成后保存了一个bitmapArray数组。

           到这里我们buildCache(item, false)的策略二中的重新设置缓存DanmakuUtils.buildDanmakuDrawingCache(item, mDisp, cache)就走完了。然后将这个目标弹幕的引用放入缓存Danmakus中(mCaches),同时更新已使用大小mRealSize。同时注意mCaches内部成员items是TreeSet类型,不能添加相同的对象。

           策略二设计的挺复杂的,我们可以看到这个策略应该是弹幕已经播放时不断执行的,对过时弹幕缓存的重复利用。不过我们刚开始,这一策略还未起作用,所以跳过,进入下一阶段:

           4)如果上述两次查找缓存都没找到,则从FinitePool中取出一个,没有就new一个,然后同上配置DrawingCache:
           继续回到buildCache方法这个位置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47

             privatebytebuildCache(BaseDanmaku item, boolean forceInsert){//item, false
                     ...测量过了...
                     ...第一策略已经pass...
                     ...第二策略已经pass...
                      //如果上述两次查找缓存都没找到,则进入下面逻辑
                      // guess cache size
                      if (!forceInsert) {//如果forceInsertfalse,则表示不检测内存超出
                          //计算此弹幕bitmap的大小,width * height * 4
                          //(因为用native创建的BitmapConfigARGB_8888,所以一个像素占4个字节)
                          int cacheSize = DanmakuUtils.getCacheSize((int) item.paintWidth,
                                  (int) item.paintHeight);
                          //如果当前已经使用大小 + 此弹幕缓存大小 > 设置的最大内存(2/3 应用内存)       
                          if (mRealSize + cacheSize > mMaxSize) {//没有超
                              return RESULT_FAILED;
                          }
                      }
                      //FinitePool中的300DrawingCache对象中取出来一个
                      cache = mCachePool.acquire();
                      //如果从上面的FinitePool取完了,则会直接new一个DrawingCache,配置DrawingCache
                      cache = DanmakuUtils.buildDanmakuDrawingCache(item, mDisp, cache);
                      item.cache = cache;
                      //item存入mCaches缓存,同时更新已使用大小mRealSize
                      boolean pushed = mCacheManager.push(item, sizeOf(item), forceInsert);
                      if (!pushed) {//如果item存放失败(使用内存超出规定大小)
                          releaseDanmakuCache(item, cache);//释放DrawingCache
                      }
                      return pushed ? RESULT_SUCCESS : RESULT_FAILED;
                     ......
    }
       //FinitePoolacquire方法,从缓存链表头取出一个对象
       public T acquire(){
            T element;
                     //mRoot 就是缓存链表表头指向的对象
            if (mRoot != null) {
                element = mRoot;
                mRoot = element.getNextPoolable();
                mPoolCount--;
            } else {
                element = mManager.newInstance();
            }
                     if (element != null) {
                element.setNextPoolable(null);
                element.setPooled(false);
                mManager.onAcquired(element);
            }
            return element;
        }

           上述策略三是直接新建一个缓存DrawingCache,然后根据目标弹幕样式等配置它然后将它付给目标弹幕,再将目标弹幕放入缓存mCaches中。
           刚开始时会执行策略三,因为刚开始时还没有缓存供我们使用,所以只能新建。

           到此buildCache方法就走完了。我们可以看到buildCache主要截取了从当前时间开始的3倍弹幕时间内所有弹幕,然后为每一条弹幕建立缓存(创建DrawingCache对象,然后测量弹幕大小,再绘制弹幕内容,最后将信息保存到DrawingCache中,然后将它赋给目标弹幕的cache属性),并将这些弹幕保存到缓存mCaches中。

           再次回顾一下上面的逻辑:

    • 子线程从发送PREPARE消息开始,然后接着发送了DISPATCH_ACTIONS消息;
    • DISPATCH_ACTIONS消息处理逻辑内部又会发送DISPATCH_ACTIONS消息,时间间隔为半条弹幕时间就这样不断循环发送;
    • DISPATCH_ACTIONS消息处理会调用dispatchAction方法,dispatchAction方法会发送BUILD_CACHES消息;
    • BUILD_CACHES消息处理会调用prepareCaches方法,prepareCaches方法内部会调用buildCache方法为从当前时间开始的3倍弹幕时间内所有的弹幕做缓存。

           buildCache走完后,赶紧回到它之前调用方法的地方,不要把自己搞晕了= 。=
           回到CacheManagingDrawTask的prepareCaches方法中,最后更新一下缓存定时器的时间,到缓存的最后一条弹幕的出现时间

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    privatelongprepareCaches(boolean repositioned){

             ...截取三倍弹幕时间内所有弹幕,并为他们一一建立缓存...
            
             if (item != null) {//截取的最后一条弹幕,更新缓存定时器时间到它的出现时间
            mCacheTimer.update(item.time);
           } else {
            mCacheTimer.update(end);
           }
    }

           prepareCaches方法走完后,回到处理原先处理BUILD_CACHES消息的逻辑中,继续执行剩余部分:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

       //CacheHandlerhandleMessage方法
    publicvoidhandleMessage(Message msg){
     ......
     case BUILD_CACHES:
            removeMessages(BUILD_CACHES);
            boolean repositioned = ((mTaskListener != null
             && mReadyState == false) || mSeekedFlag);// true
            prepareCaches(repositioned);//首次建立缓存已经完毕
            if (repositioned)
             mSeekedFlag = false;
            if (mTaskListener != null && mReadyState == false) {
             mTaskListener.ready();//然后回到mTaskListener监听ready方法
             mReadyState = true;//mReadyState标志位置为true,下次BUILD_CACHES不会进入这段逻辑了
            }
           break;
           ......
    }

           执行mTaskListener.ready()方法,得回到上层逻辑DrawHandler的prepare(runnable)方法中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

    //DrawHandlerprepare方法
       privatevoidprepare(final Runnable runnable){
           if (drawTask == null) {
               drawTask = createDrawTask(mDanmakuView.isDanmakuDrawingCacheEnabled(), timer,
                       mDanmakuView.getContext(), mDanmakuView.getWidth(), mDanmakuView.getHeight(),
                       mDanmakuView.isHardwareAccelerated(), new IDrawTask.TaskListener() {
                           @Override
                           publicvoidready(){
                               initRenderingConfigs();//初始化一些渲染参数
                               runnable.run();//执行runnablerun方法,继续追踪
                           }
                                       ......
                       });
           } else {
               runnable.run();
           }
       }
       //DrawHandlerinitRenderingConfigs方法
       privatevoidinitRenderingConfigs(){
           long averageFrameConsumingTime = 16;//平均每帧渲染间隔
           mCordonTime = Math.max(33, (long) (averageFrameConsumingTime * 2.5f));//40,警戒值1
           mCordonTime2 = (long) (mCordonTime * 2.5f);//100,警戒值2
           mFrameUpdateRate = Math.max(16, averageFrameConsumingTime / 15 * 15);//16,每帧渲染间隔
           mThresholdTime = mFrameUpdateRate + 3;//19,渲染间隔阀值
       }

           初始化一些渲染参数,主要就是计算一下警戒时间和渲染频率。然后继续追踪runnable.run()方法,这个得回到DrawHandler的handleMessage方法中处理DrawHandler.PREPARE逻辑处:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    //DrawHandlerhandleMessage方法
       publicvoidhandleMessage(Message msg){
           int what = msg.what;
           switch (what) {
               case PREPARE:
                     ......
                       prepare(new Runnable() {
                           @Override
                           publicvoidrun(){//会回调到这里
                               pausedPosition = 0;
                               mReady = true;//mReady 标志位置为true
                               if (mCallback != null) {
                                   mCallback.prepared();//回调callback监听
                               }
                           }
                       });
                   ......
                   break;
       }

           继续追踪mCallback.prepared(),会回到MainActivity当中我们设置DanmakuView的地方:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    //MainActivity中设置mDanmakuView
       mDanmakuView.setCallback(new master.flame.danmaku.controller.DrawHandler.Callback() {
             ......
        @Override
        publicvoidprepared(){                
        mDanmakuView.start();
        }
       });
       //继续产看DanmaKuViewstart方法
       publicvoidstart(){
           start(0);
       }

       publicvoidstart(long postion){
             ......
           handler.obtainMessage(DrawHandler.START, postion).sendToTarget();//DrawHandler发送START消息
       }

           然后就是DrawHandler发送START消息:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35

    //DrawHandlerhandleMessage方法
    publicvoidhandleMessage(Message msg){
                     ......
            case START:
                   Long startTime = (Long) msg.obj;//0
                   if (startTime != null) {
                       pausedPosition = startTime;//0
                   } else {
                       pausedPosition = 0;
                   }
               case SEEK_POS:
                              ......
               case RESUME:
                   quitFlag = false;
                   if (mReady) {//true
                                       ......
                       mTimeBase = SystemClock.uptimeMillis() - pausedPosition;//将时间基线设为当前时间
                       timer.update(pausedPosition);//更新主定时器时间到初始位置,为0
                       removeMessages(RESUME);
                       sendEmptyMessage(UPDATE);//发送UPDATE消息
                       drawTask.start();//CacheManagingDrawTaskstart方法
                                       ......
                   } else {
                      ......
                   }
                   break;
               case UPDATE:
                   if (mUpdateInNewThread) {//DrawHandler构造方法里赋值的变量,只有当可用CPU个数大于3时才为true
                       updateInNewThread();//四核,八核的请进
                   } else {
                       updateInCurrentThread();//单核,双核的请进
                   }
                   break;
               ......   
    }

           上述逻辑最后会进入RESUME消息处理中,先调用CacheManagingDrawTask的start方法,然后处理UPDATE消息。我们先看看CacheManagingDrawTask的start方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    //CacheManagingDrawTaskstart方法
       publicvoidstart(){
             ......
           mCacheManager.resume();//CacheManagerresume方法
       }
       //继续跟CacheManagerresume方法
          publicvoidresume(){
               ......
               mHandler.resume();//CacheManagingDrawTaskresume方法
                     ......
           }
        //继续跟CacheManagingDrawTaskresume方法  
        publicvoidresume(){
               mCancelFlag = false;
               mPause = false;
               removeMessages(DISPATCH_ACTIONS);
               sendEmptyMessage(DISPATCH_ACTIONS);//发送DISPATCH_ACTIONS消息,我们上面分析过,就是建立缓存
               sendEmptyMessageDelayed(CLEAR_TIMEOUT_CACHES, mContext.mDanmakuFactory.MAX_DANMAKU_DURATION);//延时发送CLEAR_TIMEOUT_CACHES消息
               }

           我们可以看到CacheManagingDrawTask的start方法最终做了两件事,一件是发送DISPATCH_ACTIONS再次建立缓存,这个流程我们上面分析过;第二件是延时发送CLEAR_TIMEOUT_CACHES消息。

           所以我们看看CLEAR_TIMEOUT_CACHES消息处理逻辑:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26

    //CacheHandlerhandleMessage方法
    publicvoidhandleMessage(Message msg){
              .......
              case CLEAR_TIMEOUT_CACHES:
                clearTimeOutCaches();//继续跟这个
                   break;
               ......   
    }
    //调用 clearTimeOutCaches方法
          privatevoidclearTimeOutCaches(){
              clearTimeOutCaches(mTimer.currMillisecond);//调用重载方法,参数为主定时器当前时间
          }
          //调用重载方法,参数为主定时器当前时间
          privatevoidclearTimeOutCaches(long time){
              IDanmakuIterator it = mCaches.iterator();//从之前buildCache中建立的缓存中一一遍历
              while (it.hasNext() && !mEndFlag) {//mEndFlag = false
                  BaseDanmaku val = it.next();
                  if (val.isTimeOut()) {//如果缓存的弹幕已经超时
                              ......
                      entryRemoved(false, val, null);//销毁缓存
                      it.remove();//从缓存mCaches中移除此引用
                  } else {
                      break;
                  }
              }
          }

           顺着逻辑看看entryRemoved(false, val, null)方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38

    protectedvoidentryRemoved(boolean evicted, BaseDanmaku oldValue, BaseDanmaku newValue){//1个和第3个参数没用到
        IDrawingCache<?> cache = oldValue.getDrawingCache();
        if (cache != null) {
            long releasedSize = clearCache(oldValue);//调用了clearCache方法
            if (oldValue.isTimeOut()) {
             //这个方法最终会调用我们最初设置DanmakuContext.setCacheStuffer(new SpannedCacheStuffer(), mCacheStufferAdapter)
             //中第二个参数类型为BaseCacheStuffer.ProxyreleaseResource方法,
             //方法注释是这么写的 TODO 重要:清理含有ImageSpantext中的一些占用内存的资源例如drawable
                mContext.getDisplayer().getCacheStuffer().releaseResource(oldValue);
            }
            if (releasedSize <= 0) return;
            mRealSize -= releasedSize;//真正缓存大小减去需要释放的缓存大小
            mCachePool.release((DrawingCache) cache);//Drawingcache放回到FinitePool中,已供下次取出
        }
    }
    //往下看,看看clearCache方法
    privatelongclearCache(BaseDanmaku oldValue){
        IDrawingCache<?> cache = oldValue.cache;
        if (cache == null) {
            return0;
        }
        if (cache.hasReferences()) {//如果DrawingCache缓存还被重复引用
            cache.decreaseReference();//则将引用计数-1
            oldValue.cache = null;
            return0;//不销毁缓存(bitmap,canvas),只有等到引用计数为0时才会销毁
        }
        long size = sizeOf(oldValue);//计算缓存的bitmap大小
        cache.destroy();//同时销毁bitmap
        oldValue.cache = null;
        return size;
    }
    //缓存的bitmap的大小
    protectedintsizeOf(BaseDanmaku value){
        if (value.cache != null && !value.cache.hasReferences()) {
            return value.cache.size();//返回的是Drawingbitmap对象的大小,上面讲过的
        }
        return0;
    }

          CLEAR_TIMEOUT_CACHES消息处理就分析完了,就是移除缓存弹幕mCache中过时的弹幕,并且销毁他们持有的DrawingCache,同时销毁内部的bitmap、canvas等。

    缓存机制

           现在重点来了!还记得我们之前挖的一个大坑么?就是妹子图那个地方。那是CacheHandler给工作线程发送DISPATCH_ACTIONS消息时调用的dispatchAction方法。因为CacheHandler每个半条弹幕时间就会发DISPATCH_ACTIONS消息,所以我们得仔细分析一下dispatchAction方法的各种情况:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72

            privatelongdispatchAction(){
             //如果上一次buildCache完成后得到的缓存弹幕末尾项的时间(上面分析过,这个值存在mCacheTimer.currMillisecond中)
             //和主定时器当前时间之间的时间差值已经大于一条弹幕时间,
             //则会清除所有不在屏幕内的缓存,然后重新buildCache建立缓存
                if (mCacheTimer.currMillisecond <= mTimer.currMillisecond - mContext.mDanmakuFactory.MAX_DANMAKU_DURATION) {
                    evictAllNotInScreen();//则会清除所有不在屏幕内的缓存
                    mCacheTimer.update(mTimer.currMillisecond);
                    sendEmptyMessage(BUILD_CACHES);//重新建立缓存
                    return0;
                }
                float level = getPoolPercent();//获得缓存实际大小占设置最大内存的百分比
                BaseDanmaku firstCache = mCaches.first();
                //TODO 如果firstcache大于当前时间超过半屏并且水位在0.5f以下,就要往里蓄水
                long gapTime = firstCache != null ? firstCache.time - mTimer.currMillisecond : 0;
                long doubleScreenDuration = mContext.mDanmakuFactory.MAX_DANMAKU_DURATION * 2;
                if (level < 0.6f && gapTime > mContext.mDanmakuFactory.MAX_DANMAKU_DURATION) {
                    mCacheTimer.update(mTimer.currMillisecond);
                    removeMessages(BUILD_CACHES);
                    sendEmptyMessage(BUILD_CACHES);//重新建立缓存
                    return0;
                } elseif (level > 0.4f && gapTime < -doubleScreenDuration) {//如果水位在0.5以上,并且上一次蓄水距离现在已经超过两条弹幕时间了,就要开闸放水
                    // clear timeout caches
                    removeMessages(CLEAR_TIMEOUT_CACHES);
                    sendEmptyMessage(CLEAR_TIMEOUT_CACHES);//CLEAR_TIMEOUT_CACHES消息刚分析过了,清除过时缓存
                    return0;
                }

                if (level >= 0.9f) {//水位快满了,等待下次放水
                    return0;
                }
                // check cache time
                long deltaTime = mCacheTimer.currMillisecond - mTimer.currMillisecond;
                //缓存的第一条弹幕已经过时了,并且缓存弹幕末尾时间和现在时间差值已经超过一条弹幕时间了
                if (firstCache != null && firstCache.isTimeOut() && deltaTime < -mContext.mDanmakuFactory.MAX_DANMAKU_DURATION) {
                    mCacheTimer.update(mTimer.currMillisecond);
                    sendEmptyMessage(CLEAR_OUTSIDE_CACHES);//先清除过时缓存
                    sendEmptyMessage(BUILD_CACHES);//再重组缓存
                    return0;
                } elseif (deltaTime > doubleScreenDuration) {//如果缓存的最后一条弹幕时间距离现在还有双倍弹幕时间多,则啥都不做
                    return0;
                }
    //剩余情况组建缓存
                removeMessages(BUILD_CACHES);
                sendEmptyMessage(BUILD_CACHES);
                return0;
            }
        //则会清除所有不在屏幕内的缓存   
        privatevoidevictAllNotInScreen(){
            evictAllNotInScreen(false);
        }
        privatevoidevictAllNotInScreen(boolean removeAllReferences){
            if (mCaches != null) {
                IDanmakuIterator it = mCaches.iterator();
                while (it.hasNext()) {
                    BaseDanmaku danmaku = it.next();
             ......
                    if (danmaku.isOutside()) {//如果弹幕已经走完了,超过屏幕
                        entryRemoved(true, danmaku, null);//回收缓存
                        it.remove();
                    }
                }
     
            }
            mRealSize = 0;
        }
        //获得缓存实际大小占设置最大内存的百分比
        publicfloatgetPoolPercent(){
            if (mMaxSize == 0) {
                return0;
            }
            return mRealSize / (float) mMaxSize;
        }

          dispatchAction方法主要分为以下几种规则:

    • 如果上一次buildCache完成后得到的缓存弹幕末尾项的时间(上面分析过,这个值存在mCacheTimer.currMillisecond中)和主定时器当前时间之间的时间差值已经大于一条弹幕时间, 则会清除所有不在屏幕内的缓存,然后重新buildCache建立缓存;
    • 如果缓存弹幕的第一项出现时间大于当前时间超过半屏,并且总缓存大小在规定最大值一半以下, 就要重新建立缓存;
    • 如果总缓存大小在规定最大值一半以上,并且上一次建立缓存距离现在已经超过两条弹幕时间了,就要清除超时缓存;
    • 如果总缓存大小快达到规定最大值,就等待下一次清除超时缓存;
    • 缓存的第一条弹幕已经过时了,并且缓存弹幕末尾时间和现在时间差值已经超过一条弹幕时间了,先清除过时缓存,再重组缓存;
    • 如果缓存的最后一条弹幕时间距离现在还有双倍弹幕时间多,则啥都不做;
    • 剩余情况就是重组缓存。

           因为DISPATCH_ACTIONS消息是每隔半条弹幕时间发送一次,所以会不断执行dispatchAction方法。然后根据上述出现的情况不断BUILD_CACHES和CLEAR_TIMEOUT_CACHES,这样工作线程就形成了一套缓存机制。

    绘制弹幕界面

           到此CacheManagingDrawTask的start方法就分析完了,继续回到DrawHandler的handleMessage方法,接着处理UPDATE消息:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    //DrawHandlerhandleMessage方法
    publicvoidhandleMessage(Message msg){
               case UPDATE:
                   if (mUpdateInNewThread) {//DrawHandler构造方法里赋值的变量,只有当可用CPU个数大于3时才为true
                       updateInNewThread();//四核,八核的请进
                   } else {
                       updateInCurrentThread();//单核,双核的请进
                   }
                   break;
               ......
    }

           到这里,我们应该能猜到接下要进行应该就是绘制工作了。其实updateInNewThread和updateInCurrentThread做的事情是一样的,只不过其中一个新开了子线程去做这些事情。两者的工作原理都是更新定时器,然后postInvalidate,使DanmakuView重绘,然后再发UPDATE消息,重复上述过程。

           鉴于目前四核手机已经烂大街了,我们也就挑个多核的方法进去看看:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34

    privatevoidupdateInNewThread(){
        if (mThread != null) {
            return;
        }
        mThread = new UpdateThread("DFM Update") {
            @Override
            publicvoidrun(){
                long lastTime = SystemClock.uptimeMillis();
                long dTime = 0;
                while (!isQuited() && !quitFlag) {
                    long startMS = SystemClock.uptimeMillis();
                    dTime = SystemClock.uptimeMillis() - lastTime;
                    long diffTime = mFrameUpdateRate - dTime;//mFrameUpdateRate 16,之前计算过
                    if (diffTime > 1) {//如果间隔时间太短,则会延时,一定要等够16毫秒,达到绘制时间间隔
                        SystemClock.sleep(1);
                        continue;
                    }d
                    //上面逻辑是为了延时,稳定帧率
                    lastTime = startMS;
                    long d = syncTimer(startMS);//同步主定时器时间
           ......
          
                    d = mDanmakuView.drawDanmakus();//开始postInvalidate,绘制弹幕,同时返回绘制时间
                    //这种情况出现在绘制时间内,绘制时子线程在wait,等待绘制结束,然后返回差值必定大于警戒值100
                    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();
                    }
             ......
                }
            }
        };
        mThread.start();
    }

          updateInNewThread主要做了两件事:延时然后同步主定时器时间,然后通知DanmakuView重绘。

           我们先看同步主定时器时间:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33

      privatefinallongsyncTimer(long startMS){

          ......
          long d = 0;
          long time = startMS - mTimeBase;//当前时间到初始时间的时间差
           ......
              long gapTime = time - timer.currMillisecond;//总时间差减去上一次绘制完成时间,得到绘制间隙时间
              long averageTime = Math.max(mFrameUpdateRate, getAverageRenderingTime());//计算绘制间隙平均时间,大于等于16getAverageRenderingTime方法是计算加入mDrawTimes队列的已经绘制过的时间总和除以帧数,得到平均时间,这个下面会讲到)
              //若果距离上次间隙时间过长||上次渲染时间大于第一警戒时间(40 ms||上一步计算的绘制间隙平均时间大于第一警戒时间
              if (gapTime > 2000 || mRenderingState.consumingTime > mCordonTime || averageTime > mCordonTime) {
                  d = gapTime;
                  gapTime = 0;
              } else {//如果是普通情况
                  d = averageTime + gapTime / mFrameUpdateRate;//将绘制间隙平均时间赋给d,后面的项值不大,可以忽略
                  d = Math.max(mFrameUpdateRate, d);//大于等于固定绘制间隔16
                  d = Math.min(mCordonTime, d);//小于第一警戒时间40
          
                     ......
              }
              ......
              timer.add(d);//更新主定时器时间,加上计算的时间间隔

    ......
          return d;
      }
      //计算平均绘制间隔时间
      privatesynchronizedlonggetAverageRenderingTime(){
          int frames = mDrawTimes.size();
          if(frames <= 0)
              return0;
          long dtime = mDrawTimes.getLast() - mDrawTimes.getFirst();
          return dtime / frames;
      }

           syncTimer主要是计算了一下绘制间隔时间,然后同步一下主定时器。

           然后我们看看通知DanmakuView重绘部分:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34

    //DanmakuViewdrawDanmakus方法
       publiclongdrawDanmakus(){
           long stime = SystemClock.uptimeMillis();
           lockCanvas();//再看看lockCanvas
           return SystemClock.uptimeMillis() - stime;//返回等待时间差
       }
       //DanmakuViewlockCanvas方法
       privatevoidlockCanvas(){
             ......
           postInvalidateCompat();//通知view重绘
           synchronized (mDrawMonitor) {
               while ((!mDrawFinished) && (handler != null)) {//mDrawFinished标志位为false,所以会进入循环。只有onDraw方法的绘制走完了才会将他置为true,才会跳出循环
                   try {
                       mDrawMonitor.wait(200);//onDraw没走完就会一直循环等待
                   } catch (InterruptedException e) {
                       if (mDanmakuVisible == false || handler == null || handler.isStop()) {
                           break;
                       } else {
                           Thread.currentThread().interrupt();
                       }
                   }
               }
               mDrawFinished = false;//绘制结束后,将标志位置为false,一边下次进入方法后再次进入上述等待逻辑
           }
       }
       privatevoidpostInvalidateCompat(){
           mRequestRender = true;//mRequestRender 标志位置为true,一遍onDraw方法逻辑执行
           //通知view重绘
           if(Build.VERSION.SDK_INT >= 16) {
               this.postInvalidateOnAnimation();
           } else {
               this.postInvalidate();
           }
       }

           这样就能保证保证每隔一定时间(这个时间通过syncTimer计算),更新主定时器(就是从0开始,往后每次加上(间隔时间 + 绘制时间)),然后执行postInvalidate通知DanmakuView重绘。

          postInvalidate后,View重绘,会重走onDraw方法,所以我们进入DanmakuView的onDraw方法看看:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    //DanmakuViewonDraw方法
       protectedvoidonDraw(Canvas canvas){
           if ((!mDanmakuVisible) && (!mRequestRender)) {//如果没有请求重绘则mRequestRenderfalse,不会绘制弹幕
               super.onDraw(canvas);
               return;
           }
             ......
               if (handler != null) {
                   RenderingState rs = handler.draw(canvas);//DrawHandlerdraw方法
                              ......
               }
          ......
           //绘制结束后将mRequestRender 标志位重新设为false
           //以便下一次发绘制消息时进入等待逻辑等候绘制结束,这个上面DanmakuViewdrawDanmakus方法提到过
           mRequestRender = false;
           unlockCanvasAndPost();//通知UpdateThread绘制完成
       }
       privatevoidunlockCanvasAndPost(){
           synchronized (mDrawMonitor) {
               mDrawFinished = true;//mDrawFinished 置为true,以便DanmakuViewlockCanvas方法跳出循环,这个上面也提到过
               mDrawMonitor.notifyAll();
           }
       }

           DanmakuView的onDraw回调逻辑会执行DrawHandler的draw方法,我们继续跟进去:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

      public RenderingState draw(Canvas canvas){
    ......
          mDisp.setExtraData(canvas);//canvas一些信息设置给AndroidDisplayer
          mRenderingState.set(drawTask.draw(mDisp));//绘制部分是drawTask.draw(mDisp)
          recordRenderingTime();//记录绘制结束时间
          return mRenderingState;
      }
      //还记得上面的DrawHandlersyncTimer方法吗?里面调用了getAverageRenderingTime计算绘制平均间隔时间,
      //其中用到的mDrawTimes变量就是在这里添加元素的
      privatesynchronizedvoidrecordRenderingTime(){
          long lastTime = SystemClock.uptimeMillis();
          mDrawTimes.addLast(lastTime);//将绘制结束时间加入到类型为LinkedListmDrawTimes集合中
          int frames = mDrawTimes.size();
          if (frames > MAX_RECORD_SIZE) {//最大容量为500个绘制时间,超出了则移除第一个
              mDrawTimes.removeFirst();
          }
      }

           上述逻辑中,我的注释部分先分析了记录绘制结束时间部分,填了上边syncTimer时的坑。
           然后应该进入主要绘制部分了drawTask.draw(mDisp),也就是CacheManagingDrawTask的draw方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36

    //CacheManagingDrawTaskdraw方法
       public RenderingState draw(AbsDisplayer displayer){
           RenderingState result = super.draw(displayer);//会调用父类的draw方法
             ......
           return result;
       }
       //DrawTaskdraw方法
       publicsynchronized RenderingState draw(AbsDisplayer displayer){
           return drawDanmakus(displayer,mTimer);//又调用了drawDanmakus方法
       }
       //DrawTaskdrawDanmakus方法
       protected RenderingState drawDanmakus(AbsDisplayer disp, DanmakuTimer timer){
             ......
           if (danmakuList != null) {
               Canvas canvas = (Canvas) disp.getExtraData();//取出DanmakuViewcanvas
                     //当前时间 - 1屏弹幕时间 -100 (多减100是为了下次重新截取弹幕组时让绘制边界做到无缝衔接)
               long beginMills = timer.currMillisecond - mContext.mDanmakuFactory.MAX_DANMAKU_DURATION - 100;
               //当前时间 + 1屏弹幕时间
               long endMills = timer.currMillisecond + mContext.mDanmakuFactory.MAX_DANMAKU_DURATION;
               //每过了一屏的弹幕时间,就会进入如下if逻辑,截取以当前时间为基准的前后两屏弹幕;
               //如果距离上次截取时间不到一屏弹幕时间,则不会进入if的逻辑
               if(mLastBeginMills > beginMills || timer.currMillisecond > mLastEndMills) {
                   IDanmakus subDanmakus = danmakuList.sub(beginMills, endMills);
                   if(subDanmakus != null) {
                       danmakus = subDanmakus;
                   }
                   mLastBeginMills = beginMills;
                   mLastEndMills = endMills;
               } else {//距离上次截取时间不到一屏时间
                     ......
               }
               if (danmakus != null && !danmakus.isEmpty()) {//开始绘制弹幕
                   RenderingState renderingState = mRenderingState = mRenderer.draw(mDisp, danmakus, mStartRenderTime);
                     ......
                     }
       }

           我们可以看到第一次进入会截取以当前时间为基准的前后两屏弹幕。以后每过一屏弹幕时间,会重新截取当时时间为基准的前后两屏弹幕,如果不到一屏时间则不截取,还是以前的弹幕数据。

           截取完弹幕数据后,就是绘制了,继续执行下面逻辑(mRenderer.draw(mDisp,danmakus, mStartRenderTime)),开始绘制工作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48

    //DanmakuRendererdraw方法
       public RenderingState draw(IDisplayer disp, IDanmakus danmakus, long startRenderTime){
           ......      
           IDanmakuIterator itr = danmakus.iterator();
           ......     

           BaseDanmaku drawItem = null;
           while (itr.hasNext()) {

               drawItem = itr.next();

                     ......
                     //如果弹幕还没有到出现时间,则检查它有没有缓存,如果没有则为它建立缓存
               if (drawItem.isLate()) {
                   IDrawingCache<?> cache = drawItem.getDrawingCache();
                   if (mCacheManager != null && (cache == null || cache.get() == null)) {
                       mCacheManager.addDanmaku(drawItem);
                   }
                   break;
               }
              
                     ......

               // measure 测量,我们之前prepareCache已经为他们在buildCache是测量过了
               if (!drawItem.isMeasured()) {
                   drawItem.measure(disp, false);
               }

               // layout 布局,计算弹幕在屏幕上应该显示的位置
               mDanmakusRetainer.fix(drawItem, disp, mVerifier);

               // draw //绘制弹幕
               if (!drawItem.isOutside() && drawItem.isShown()) {
                   if (drawItem.lines == null && drawItem.getBottom() > disp.getHeight()) {
                       continue;    // skip bottom outside danmaku ,忽略超过视图底部的弹幕
                   }
                   //开始绘制
                   int renderingType = drawItem.draw(disp);
                   if(renderingType == IRenderer.CACHE_RENDERING) {//如果是使用缓存bitmap绘制的
                       ......
                   } elseif(renderingType == IRenderer.TEXT_RENDERING) {//如果使用缓存绘制失败,则会使用原声方法Canvasdraw
                       ......
                       if (mCacheManager != null) {
                           mCacheManager.addDanmaku(drawItem);//再次为词条弹幕构建缓存,以便下次使用缓存bitmap绘制
                       }
                   }
                              ......
           }

           从截取的弹幕中遍历每一个,然后一一绘制。绘制步骤有如下几步:

    • 如果弹幕还没有到出现时间,则检查它有没有缓存,如果没有则为它建立缓存;
    • measure 测量,我们之前prepareCache已经为他们在buildCache时测量过了;
    • layout 布局,计算弹幕在屏幕上应该显示的位置;
    • draw 绘制弹幕。

           我们一步一步分析:
           1
    )弹幕未到出现时间,检查是否建立缓存:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31

    //调用CacheManagingDrawTaskaddDanmaku方法
          publicvoidaddDanmaku(BaseDanmaku danmaku){
              if (mHandler != null) {
                              ......
                              //CacheHandler
                      mHandler.obtainMessage(CacheHandler.ADD_DANMAKKU, danmaku).sendToTarget();
                ......
              }
          }
          //CacheHandler
          publicvoidhandleMessage(Message msg){
              case ADD_DANMAKKU:
                  BaseDanmaku item = (BaseDanmaku) msg.obj;
                  addDanmakuAndBuildCache(item);//调用了addDanmakuAndBuildCache方法
                  break;       
          }
           //调用了addDanmakuAndBuildCache方法
              privatefinalvoidaddDanmakuAndBuildCache(BaseDanmaku danmaku){
               //过时了 || 并且弹幕时间不在3屏弹幕时间内(因为mCaches只缓存了3屏时间内的所有弹幕,上面说过的),并且它不是直播弹幕。则不建立缓存
                  if (danmaku.isTimeOut() || (danmaku.time > mCacheTimer.currMillisecond + mContext.mDanmakuFactory.MAX_DANMAKU_DURATION && !danmaku.isLive)) {
                      return;
                  }
                  //优先级为0或者在过滤规则内,不建立缓存
                  if (danmaku.priority == 0 && danmaku.isFiltered()) {
                      return;
                  }
                  IDrawingCache<?> cache = danmaku.getDrawingCache();
                  if (cache == null || cache.get() == null) {//如果弹幕没有缓存
                      buildCache(danmaku, true);//建立缓存(buildCache方法我们上面分析过,就是用来建立缓存的)
                  }
              }

           2)测量,这个我们上面再buildCache时分析过了,不再赘述;

           3)布局:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    //调用DanmakusRetainerfix方法
       publicvoidfix(BaseDanmaku danmaku, IDisplayer disp, Verifier verifier){
           int type = danmaku.getType();
           switch (type) {
               case BaseDanmaku.TYPE_SCROLL_RL:
                   rldrInstance.fix(danmaku, disp, verifier);
                   break;
               case BaseDanmaku.TYPE_SCROLL_LR:
                   lrdrInstance.fix(danmaku, disp, verifier);
                   break;
               case BaseDanmaku.TYPE_FIX_TOP:
                   ftdrInstance.fix(danmaku, disp, verifier);
                   break;
               case BaseDanmaku.TYPE_FIX_BOTTOM:
                   fbdrInstance.fix(danmaku, disp, verifier);
                   break;
               case BaseDanmaku.TYPE_SPECIAL:
                   danmaku.layout(disp, 0, 0);
                   break;
           }
       }

           类型太多了,我们只分析TYPE_SCROLL_RL类型弹幕其他的就不分析,有兴趣的可以自己分析一下其他的。接着会调用AlignTopRetainer的fix方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133

    //保存需要显示的弹幕容器类(保存的一行只有一条弹幕,下面会说明的),内部持有一个以弹幕的y坐标排序的TreeSet集合,这个需要注意
    protected Danmakus mVisibleDanmakus = new Danmakus(Danmakus.ST_BY_YPOS);
         
    //AlignTopRetainerfix方法
          publicvoidfix(BaseDanmaku drawItem, IDisplayer disp, Verifier verifier){
              if (drawItem.isOutside())//如果弹幕已经滚动到视图边界外,则不会为它布局
                  return;
              float topPos = 0;//弹幕的y坐标
              int lines = 0;//弹幕在第几行显示
              boolean shown = drawItem.isShown();//弹幕是否已经显示
              boolean willHit = !shown && !mVisibleDanmakus.isEmpty();//是否会和其他弹幕碰撞
              boolean isOutOfVertialEdge = false;//弹幕y值是否超过试图高度
              BaseDanmaku removeItem = null;//需要移除的弹幕
              //为即将显示的弹幕确认位置
              if (!shown) {
                  mCancelFixingFlag = false;
                  // 确定弹幕位置开始
                  IDanmakuIterator it = mVisibleDanmakus.iterator();
                  //这四个变量分别为:
                  //insertItem ---- 确认目标弹幕插入到哪一行的同行参考弹幕
                  //firstItem ---- 已经布局过的弹幕保存容器中的第一项
                  //lastItem ---- 已经布局过的弹幕保存容器中最后一项
                  //minRightRow ---- 已经布局过弹幕中x值最小的弹幕,即最左边的弹幕
                  BaseDanmaku insertItem = null, firstItem = null, lastItem = null, minRightRow = null;
                  boolean overwriteInsert = false;//是否超出插入范围
                  //遍历已经绘制过的弹幕,因为mVisibleDanmakus 内弹幕以y值排序的,所以按y值从小到大遍历
                  while (!mCancelFixingFlag && it.hasNext()) {
                      lines++;//每次循环都会将行号+1
                      BaseDanmaku item = it.next();
                     
                      if(item == drawItem){//如果已经布局过了,说明已经存在自己位置了
                          insertItem = item;//将布局过的弹幕复制给参考弹幕insertItem
                          lastItem = null;//置空 lastItem
                          shown = true;//shown 置为true,以便末尾不再执行加入mVisibleDanmakus逻辑
                          willHit = false;//本身已经存在自己位置了,当然没有碰壁一说
                          break;//怕被下面干扰晕的可以跳出去继续看
                      }
                             
                      if (firstItem == null)//找到已经布局过的弹幕第一项
                          firstItem = item;
                              //如果插入目标弹幕后,y值超过了视图高度
                      if (drawItem.paintHeight + item.getTop() > disp.getHeight()) {
                          overwriteInsert = true;//则将超出插入范围标签置为true
                          break;//怕晕的跳出循环
                      }
                              //找出最左边的弹幕
                      if (minRightRow == null) {
                          minRightRow = item;
                      } else {
                          if (minRightRow.getRight() >= item.getRight()) {
                              minRightRow = item;
                          }
                      }

                      // 检查如果插入目标弹幕是否会和正在遍历的已经布局过的参考弹幕碰撞
                      willHit = DanmakuUtils.willHitInDuration(disp, item, drawItem,
                              drawItem.getDuration(), drawItem.getTimer().currMillisecond);
                      if (!willHit) {//如果没有碰撞
                          insertItem = item;//则将它复制给参考弹幕insertItem
                          break;//然后跳出循环,下去确定位置
                      }/*如果有碰撞,则继续弹幕缩小添加范围,寻找可以添加的条件,最后出while循环,下去布局*/
                     
                             
                      lastItem = item;//暂时找到已经布局过的弹幕最后一项,然后继续循环
                  }
                  boolean checkEdge = true;
                  if (insertItem != null) {//已经布局过了||目标弹幕不会碰壁可以插入
                      if (lastItem != null)//目标弹幕插入,y值即为上一次遍历的弹幕的底部
                          topPos = lastItem.getBottom();
                      else//已经布局过了,则y的位置不变
                          topPos = insertItem.getTop();
                         
                      if (insertItem != drawItem){//如果目标弹幕可以插入
                       //这里需要注意,因为一行可以放n多条弹幕,只要前后不碰撞就行;
                       //所以下次我们在同一行插入弹幕判断碰壁时,当然要和这行最后一条弹幕去判断;
                       //因此我们移除前一条弹幕,放入插入的目标弹幕,下次添加弹幕判断时就和目标弹幕判断,然后这么循环下去
                          removeItem = insertItem;
                         
                          shown = false;//置为false,以便mVisibleDanmakus 添加还未布局的新弹幕
                      }
                  } elseif (overwriteInsert && minRightRow != null) {//没有空行可以插入
                      topPos = minRightRow.getTop();//暂时放到最最左边的弹幕那一行(excuse me ???)
                      checkEdge = false;//不做范围检查
                      shown = false;
                  } elseif (lastItem != null) {//找不到插入的位置
                      topPos = lastItem.getBottom();//暂时放到最低位置的弹幕下面,下面检测边界时会酌情河蟹
                      willHit = false;//false碰壁标志
                  } elseif (firstItem != null) {mVisibleDanmakus只有第一条数据,截取弹幕集的第二条弹幕没有和第一条碰壁时
                      topPos = firstItem.getTop();//此时第二条弹幕和第一条在同一行
                      removeItem = firstItem;
                      shown = false;
                  } else {//mVisibleDanmakus 没有数据,截取弹幕集的第一条弹幕
                      topPos = 0;//第一条弹幕当然在最上面
                  }
                  if (checkEdge) {//如果检查范围
                   //检查是否超出布局范围
                      isOutOfVertialEdge = isOutVerticalEdge(overwriteInsert, drawItem, disp, topPos, firstItem,
                              lastItem);
                  }
                  if (isOutOfVertialEdge) {//如果超出布局范围,等待河蟹
                      topPos = 0;
                      willHit = true;
                      lines = 1;
                  } elseif (removeItem != null) {//上面可以插入目标弹幕的逻辑用上了
                      lines--;//因为参考弹幕和目标弹幕在同一行,但是每进入while循环一次就将行号+1,所有要减回去和参考弹幕保持相同行号
                  }
                  if (topPos == 0) {//方便加入容器
                      shown = false;
                  }
              }
             //这是河蟹规则,都是在设置DanmakuContext时指定的,比如最大行数限制,重复限制等等。
             //这里限于篇幅已经太长了,也实在写不动了,就不再跟下去了。内部逻辑也不难,大家有兴趣可以自己看看。
              if (verifier != null && verifier.skipLayout(drawItem, topPos, lines, willHit)) {
                  return;
              }

              if (isOutOfVertialEdge) {//mVisibleDanmakus中所有弹幕绘制出来都超出范围了
                  clear();
              }
             //这才是真正确认弹幕位置的地方
              drawItem.layout(disp, drawItem.getLeft(), topPos);

              if (!shown) {//如果还未显示,则加入即将显示的容器中。可以看到,最终会把所有截取的弹幕加入到这个容器里
                  mVisibleDanmakus.removeItem(removeItem);//移除同一行之前的参考弹幕,保持保存的一行只有一条弹幕,上面说明过
                  mVisibleDanmakus.addItem(drawItem);
              }

          }
          //清除容器,重新放入新的内容
          publicvoidclear(){
              mCancelFixingFlag = true;
              mVisibleDanmakus.clear();
          }

           这绝对是我写的注释最多的方法了ToT。。。。。。其实思路挺好理解的,通俗地讲就是这样的过程:

    • 先添往最第一行添加一条弹幕,把它存到一个容器里(这个容器会把新添加进来的弹幕按照y值从小到大排序,而且容器只保存每一行的最后一条弹幕)
    • 然后添加第二条弹幕,从第一行开始添加,先判断和第一条弹幕会不会碰壁,如果不会碰壁则添加到这一行,然后容器内移除之前第一条的弹幕,保存这一条弹幕;如果会碰壁则添加到下一行,然后容器保存这条弹幕
    • 然后添加第三条,继续从第一行开始添加,先判断和第一条……(重复第二条的逻辑)……
      。。。。。。

           就是这么个思路,但是写起来真心不是随意就能写出来的。即使先不说写,把这个思路想出来,让我去设计一套规则,估计都相当困难啊。唉,人与人之间的差距始终在思维。。。。。。

           扯远了,我们继续回归正题,上面逻辑完成了弹幕定位规则(内部那个layout接下来再讲),限于篇幅,我只挑一个检查碰撞的代码贴出来分析,其它的请有兴趣者自行跟踪。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72

      publicstaticbooleanwillHitInDuration(IDisplayer disp, BaseDanmaku d1, BaseDanmaku d2,
              long duration, long currTime) {//disp, item, drawItem, drawItem.getDuration(), drawItem.getTimer().currMillisecond
          finalint type1 = d1.getType();
          finalint type2 = d2.getType();
          // allow hit if different type 不同类型的弹幕允许碰撞
          if(type1 != type2)
              returnfalse;
         
          if(d1.isOutside()){//item已经跑出视图了,不存在碰撞问题
              returnfalse;
          }
          long dTime = d2.time - d1.time;
          if (dTime <= 0)//drawItemitem前面,已经碰撞了
              returntrue;
          //两者出现时间已经相差一条弹幕时间了 || item超时跑出去了 || drawItem超时,都不会碰撞   
          if (Math.abs(dTime) >= duration || d1.isTimeOut() || d2.isTimeOut()) {
              returnfalse;
          }
    //itemdrawItem都是顶部或者底部固定弹幕,因为在同一行,必定碰撞
          if (type1 == BaseDanmaku.TYPE_FIX_TOP || type1 == BaseDanmaku.TYPE_FIX_BOTTOM) {
              returntrue;
          }
    //调用checkHitAtTime方法
          return checkHitAtTime(disp, d1, d2, currTime)
                  || checkHitAtTime(disp, d1, d2,  d1.time + d1.getDuration());
      }
      //调用checkHitAtTime方法
      privatestaticbooleancheckHitAtTime(IDisplayer disp, BaseDanmaku d1, BaseDanmaku d2, long time){//time = currTime || time = item.time + item.duration
          finalfloat[] rectArr1 = d1.getRectAtTime(disp, time);//time获得item在视图的(ltrb
          finalfloat[] rectArr2 = d2.getRectAtTime(disp, time);//time获得drawItem在视图的(ltrb
          if (rectArr1 == null || rectArr2 == null)
              returnfalse;
          return checkHit(d1.getType(), d2.getType(), rectArr1, rectArr2);
      }
      //调用checkHit方法   
      privatestaticbooleancheckHit(int type1, int type2, float[] rectArr1,
              float[] rectArr2) {
          if(type1 != type2)
              returnfalse;
          if (type1 == BaseDanmaku.TYPE_SCROLL_RL) {//只要drawItemleft小于itemright就碰撞了
              // hit if left2 < right1
              return rectArr2[0] < rectArr1[2];
          }
         
          if (type1 == BaseDanmaku.TYPE_SCROLL_LR){
              // hit if right2 > left1
              return rectArr2[2] > rectArr1[0];
          }
         
          returnfalse;
      }
      //R2LDanmakugetRectAtTime方法
      publicfloat[] getRectAtTime(IDisplayer displayer, long time) {//time = currTime || time = item.time + item.duration
          if (!isMeasured())
              returnnull;
          float left = getAccurateLeft(displayer, time);//获得此时弹幕在视图的x坐标
          if (RECT == null) {
              RECT = newfloat[4];
          }
          RECT[0] = left;//left
          RECT[1] = y;//top
          RECT[2] = left + paintWidth;//right
          RECT[3] = y + paintHeight;//bottom
          return RECT;
      }
      //R2LDanmakugetAccurateLeft方法
      protectedfloatgetAccurateLeft(IDisplayer displayer, long currTime){//currTime = timer.currTime || currTime = item.time + item.duration
          long elapsedTime = currTime - time;//当前时间 - 弹幕出现时间
    ......
    //因此返回弹幕位于视图的x坐标,即视图宽度 - 弹幕已经显示了多少秒 * 每秒移动步长
    return displayer.getWidth() - elapsedTime * mStepX;
      }

           检查碰撞逻辑比较简单,就是先根据当前时间就算出两条弹幕的位置(l1,t1,r1,b1),看看是否前面弹幕的 r1 小于后面弹幕的 l1;再根据前面弹幕的结束时间,计算出两条弹幕的位置(l2,t2,r2,b2)再次看看是否前面弹幕的 r2小于后面弹幕的 l2。只有两条都满足才不会碰撞。

           好了检测碰撞就先到这里,然后继续回到AlignTopRetainer的fix方法,还有一个drawItem.layout(disp,drawItem.getLeft(), topPos);没讲呢,这才是真正确认弹幕位置的地方,继续查看L2RDanmaku的layout方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    publicvoidlayout(IDisplayer displayer, float x, float y){//disp, drawItem.getLeft(), topPos
        if (mTimer != null) {
            long currMS = mTimer.currMillisecond;
            long deltaDuration = currMS - time;//计算出出现时间和当前时间的时间差
            if (deltaDuration > 0 && deltaDuration < duration.value) {//如果还没有到出现时间或者超出弹幕时间
                this.x = getAccurateLeft(displayer, currMS);//计算出当前时间弹幕的x坐标,上面刚讲过
                if (!this.isShown()) {
                    this.y = y;//把上面计算好的y值赋过来
                    this.setVisibility(true);
                }
                mLastTime = currMS;
                return;
            }
            mLastTime = currMS;
        }
        this.setVisibility(false);
    }

           这样弹幕的位置也就确定了,layout步骤就走完了。下一步就是draw步骤了。

           4)绘制弹幕:
           
    赶紧回到DanmakuRenderer的draw方法,这个时候千万不要把自己搞晕了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36

    //DanmakuRendererdraw方法
        public RenderingState draw(IDisplayer disp, IDanmakus danmakus, long startRenderTime){
            ......      
            IDanmakuIterator itr = danmakus.iterator();
            ......     

            BaseDanmaku drawItem = null;
            while (itr.hasNext()) {

                drawItem = itr.next();

                              ......
                              ...检查是否建立缓存...            
                              ......

                ...是否测量...

                ...layout布局...

                // draw //绘制弹幕
                if (!drawItem.isOutside() && drawItem.isShown()) {
                    if (drawItem.lines == null && drawItem.getBottom() > disp.getHeight()) {
                        continue;    // skip bottom outside danmaku ,忽略超过视图底部的弹幕
                    }
                    //开始绘制
                    int renderingType = drawItem.draw(disp);
                    if(renderingType == IRenderer.CACHE_RENDERING) {//如果是使用缓存bitmap绘制的
                        ......
                    } elseif(renderingType == IRenderer.TEXT_RENDERING) {//如果使用缓存绘制失败,则会使用原声方法Canvasdraw
                        ......
                        if (mCacheManager != null) {
                            mCacheManager.addDanmaku(drawItem);//再次为词条弹幕构建缓存,以便下次使用缓存bitmap绘制
                        }
                    }
                                       ......   
        }

           继续跟踪int renderingType = drawItem.draw(disp) 这里:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36

    //BaseDanmakudraw方法
       publicintdraw(IDisplayer displayer){
           return displayer.draw(this);//调用AndroidDisplayerdraw方法
       }
       //调用AndroidDisplayerdraw方法
       publicintdraw(BaseDanmaku danmaku){
           float top = danmaku.getTop();//弹幕在视图的y
           float left = danmaku.getLeft();//弹幕在视图的x
           if (canvas != null) {

                     ......
                             
               // drawing cache
               boolean cacheDrawn = false;
               int result = IRenderer.CACHE_RENDERING;
               IDrawingCache<?> cache = danmaku.getDrawingCache();
               if (cache != null) {//如果弹幕有缓存
                //取出缓存
                   DrawingCacheHolder holder = (DrawingCacheHolder) cache.get();
                   if (holder != null) {
                    //DrawingCacheHolderdraw方法,我们在上面的buildCache时分析过了,将每一条弹幕的bitmap绘制到视图的canvas
                       cacheDrawn = holder.draw(canvas, left, top, alphaPaint);
                   }
               }
               if (!cacheDrawn) {//如果缓存绘制失败
                              ......
                              //则使用Android原生的canvas.drawText等方法绘制,drawDanmaku方法我们上面buildCache时也分析过
                   drawDanmaku(danmaku, canvas, left, top, false);
                   result = IRenderer.TEXT_RENDERING;
               }

               return result;
           }

           return IRenderer.NOTHING_RENDERING;
       }

           上面逻辑比较简单,先查看弹幕有没有缓存,如果有,就使用缓存绘制。在上面的buildCache时我们知道,缓存绘制的每一条弹幕都是一条bitmap,所以这里用缓存也是将bitmap绘制到视图的Canvas中。如果使用缓存绘制失败,会调用drawDanmaku方法,这个方法我们在上面的buildCache也分析过,则使用Android原生的canvas.drawText等绘制。

           这样弹幕就被绘制到视图界面上了。

           终于完了,以上就是DanmakuFlameMaster的流程分析过程了,分析的快吐学了ToT。。。。。。

    TODO

           上面刚开始奖CacheManagingDrawTask时曾经说过,也可以不用CacheManagingDrawTask,直接使用DrawTask,只要将DanmakuViewmEnableDanmakuDrwaingCache变量改为false就可以了。这样改动之后就用不上工程里那些so库了,也就不用建立那么复杂的缓存机制。

           还有一点区别就是使用CacheManagingDrawTask画出来的每一条弹幕都是bitmap,而用DrawTask的弹幕都是Canvas.drawText画出来的。

           限于篇幅,DrawTask就不分析了,逻辑比CacheManagingDrawTask简单多了,大家有兴趣的自己看看。

    结语

          DanmakuFlameMaster到此就分析完全了,简单总结一下流程就是:

    • 加载弹幕资源
    • 开启缓存机制,不断建立缓存和回收
    • 开始绘制任务,根据定时器时间确定弹幕位置,绘制弹幕

           这篇文章写的过程中也是十分蛋疼的,写的我差点over了。因为DanmakuFlameMaster源码实在太复杂了,坑非常多,所以很多细节都没有顾及。下次我绝对不会再写这么长的文章了,身体和脑力真心伤不起啊。赶紧休息一下~~~~~

     

    展开全文
  • Android弹幕框架 黑暗火焰使

    千次阅读 2016-10-20 11:25:13
    笑谈风云,一语定乾坤。大家好,我是皖江。 今天我将分享由BiliBili开源的Android弹幕框架(DanmakuFlameMaster)的学习经验。

    笑谈风云,一语定乾坤。大家好,我是皖江。

    今天我将分享由BiliBili开源的Android弹幕框架(DanmakuFlameMaster)的学习经验。

    我是将整个框架以model的形式引入项目中的,这样更方便的观察源码。也可以通过依赖的方式注入进来

    dependencies {
        compile 'com.github.ctiao:DanmakuFlameMaster:0.5.3'
    }

    先放一下我要做成的效果图:


    页面分析

    从上图来看,整个UI分成了三层。最下面是视频层,中间是弹幕层,顶层是控制层。现在市场上主流的视频直播软件大多都是这样分层的,不同的是直播类的话,可能还会再多一层交互层,显示签到信息、礼物信息什么的。

    既然是分层的话,我就直接用FrameLayout帧布局来实现了。贴一下我的布局文件:

    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <VideoView
            android:id="@+id/vv_video"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    
        <master.flame.danmaku.ui.widget.DanmakuView
            android:id="@+id/sv_danmaku"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    
        <include android:id="@+id/media_controller"
            android:layout_width="match_parent"
            android:layout_height="fill_parent"
            layout="@layout/media_controller" />
    
    </FrameLayout>
    控制层的布局:

    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    
        <LinearLayout
            android:gravity="center_vertical"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom"
            android:background="#8acc22dd" >
    
            <Button
                android:layout_weight="1"
                android:id="@+id/rotate"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:text="@string/rotate" />
    
            <Button
                android:layout_width="0dp"
                android:layout_weight="1"
                android:id="@+id/btn_hide"
                android:layout_height="wrap_content"
                android:text="@string/hide_danmaku" />
    
            <Button
                android:id="@+id/btn_show"
                android:layout_width="0dp"
                android:layout_weight="1"
                android:layout_height="wrap_content"
                android:text="@string/show_danmaku" />
    
            <Button
                android:id="@+id/btn_pause"
                android:layout_width="0dp"
                android:layout_weight="1"
                android:layout_height="wrap_content"
                android:text="@string/pause_danmaku" />
    
            <Button
                android:id="@+id/btn_resume"
                android:layout_width="0dp"
                android:layout_weight="1"
                android:layout_height="wrap_content"
                android:text="@string/resume_danmaku" />
    
            <Button
                android:id="@+id/btn_send"
                android:layout_width="0dp"
                android:layout_weight="1"
                android:layout_height="wrap_content"
                android:text="@string/send_danmaku" />
    
            <Button
                android:id="@+id/btn_send_image_text"
                android:layout_width="0dp"
                android:layout_weight="1"
                android:layout_height="wrap_content"
                android:text="@string/send_danmaku_image_text" />
            
            <Button
                android:id="@+id/btn_send_danmakus"
                android:layout_width="0dp"
                android:layout_weight="1"
                android:layout_height="wrap_content"
                android:text="@string/send_danmakus" />
        </LinearLayout>
    
    </FrameLayout>

    写完布局,先写一个初始化的方法:

            //设置最大行数
            HashMap<Integer,Integer> maxLinesPair = new HashMap<>();
            maxLinesPair.put(BaseDanmaku.TYPE_SCROLL_RL,5);//滚动弹幕最大显示5行
            //设置是否禁止重叠
            HashMap<Integer, Boolean> overlappingEnablePair = new HashMap<>();
            overlappingEnablePair.put(BaseDanmaku.TYPE_SCROLL_RL, true);
            overlappingEnablePair.put(BaseDanmaku.TYPE_FIX_TOP, true);
            mDanmakuContext = DanmakuContext.create();//初始化上下文
            mDanmakuContext.setDanmakuStyle(IDisplayer.DANMAKU_STYLE_STROKEN,3);//设置弹幕类型
            mDanmakuContext.setDuplicateMergingEnabled(false);//设置是否合并重复弹幕
            mDanmakuContext.setScrollSpeedFactor(1.2f);//设置弹幕滚动速度
            mDanmakuContext.setScaleTextSize(1.2f);//设置弹幕字体大小
            mDanmakuContext.setCacheStuffer(new SpannedCacheStuffer(),mCacheStufferAdapter);//设置缓存绘制填充器 图文混排使用SpannedCacheStuffer  
            mDanmakuContext.setMaximumLines(maxLinesPair);//设置最大行数
            mDanmakuContext.preventOverlapping(overlappingEnablePair); //设置是否禁止重叠
            mParser = createParser(this.getResources().openRawResource(R.raw.comments));//加载弹幕资源文件
            mDvDanmaku.prepare(mParser, mDanmakuContext);
            mDvDanmaku.showFPS(true);
            mDvDanmaku.enableDanmakuDrawingCache(true);


    再贴一下绘制填充器的实现,主要实现了将图片和文字排列在一起的效果

        private SpannableStringBuilder createSpannable(Drawable drawable) {
            String text = "bitmap";
            SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(text);
            ImageSpan span = new ImageSpan(drawable);
            spannableStringBuilder.setSpan(span, 0, text.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
            spannableStringBuilder.append("图文混排");
            spannableStringBuilder.setSpan(new BackgroundColorSpan(Color.parseColor("#8A2233B1")), 0, spannableStringBuilder.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
            return spannableStringBuilder;
        }

    在BaseDanmaku这个类下,定义了弹幕的基本运动形式:TYPE_SCROLL_RL、TYPE_SCROLL_LR、TYPE_FIX_TOP、TYPE_FIX_BOTTOM和TYPE_SPECIAL。也就是从右入左出、左入右出(逆向弹幕)、顶部弹幕、底部弹幕以及高级弹幕。(除此之外还有脚本弹幕)

    在初始化的时候,需要传入一个特有的弹幕上下文DanmukuContext,通过上下文来初始化一些设置。弹幕资源是保存在xml文件下的,大致格式如下:

    .

    <i>
        <chatserver>chat.bilibili.com</chatserver>
        <chatid>2962351</chatid>
        <mission>0</mission>
        <maxlimit>1500</maxlimit>
        <source>k-v</source>
        <d p="145.91299438477,1,25,16777215,1422201001,0,D6673695,757075520">我从未见过如此厚颜无耻之人</d>
    </i>
    头信息不需要太多关注,来看看d标签下p对应参数的具体意义:

    第一个:弹幕出现的时间

    第二个:弹幕类型(1、从右至左;6、从左至右;5、顶部弹幕;4、底部弹幕;7、高级弹幕;8、脚本弹幕’)

    第三个:字号

    第四个:颜色

    第五个:时间戳

    第六个:弹幕池ID

    第七个:用户hash值

    第八个:弹幕id

    以下是弹幕具体解析代码


        /**
         * 从弹幕文件中提起弹幕
         * @param stream
         * @return
         */
        private BaseDanmakuParser createParser(InputStream stream) {
            if (stream == null) {
                return new BaseDanmakuParser() {
                    @Override
                    protected Danmakus parse() {
                        return new Danmakus();
                    }
                };
            }
            ILoader loader = DanmakuLoaderFactory.create(DanmakuLoaderFactory.TAG_BILI);//创建一个BiliDanmakuLoader实例来加载弹幕流文件
            try {
                loader.load(stream);
            } catch (IllegalDataException e) {
                e.printStackTrace();
            }
            BaseDanmakuParser parser = new BiliDanmukuParser();//弹幕解析者
            IDataSource<?> dataSource = loader.getDataSource();
            parser.load(dataSource);
            return parser;
        }

    具体解析方案:

                String tagName = localName.length() != 0 ? localName : qName;
                tagName = tagName.toLowerCase(Locale.getDefault()).trim();
                if (tagName.equals("d")) {
                    String pValue = attributes.getValue("p");
                    // parse p value to danmaku
                    String[] values = pValue.split(",");
                    if (values.length > 0) {
                        long time = (long) (Float.parseFloat(values[0]) * 1000); // 出现时间
                        int type = Integer.parseInt(values[1]); // 弹幕类型
                        float textSize = Float.parseFloat(values[2]); // 字体大小
                        int color = Integer.parseInt(values[3]) | 0xFF000000; // 颜色
                        item = mContext.mDanmakuFactory.createDanmaku(type, mContext);
                        if (item != null) {
                            item.setTime(time);
                            item.textSize = textSize * (mDispDensity - 0.6f);
                            item.textColor = color;
                            item.textShadowColor = color <= Color.BLACK ? Color.WHITE : Color.BLACK;
                        }
                    }
                }
    弹幕资源加载完毕后,就调用mDvDanmuku的prepare()方法,执行准备。当准备完毕的时候,就会调用DrawHandler.CallBack()回调中的prepared方法。然后在这个prepared方法中正式让弹幕启动。调用顺序如下:

    mDvDanmaku.prepare(mParser, mDanmakuContext);//传入解析完成的弹幕和上下文

    然后执行DanmukuView下的prepare()方法

        private void prepare() {
            if (handler == null)
                handler = new DrawHandler(getLooper(mDrawingThreadType), this, mDanmakuVisible);//创建一个Handler
        }
    通过这个Handler来实现进程间的通讯

            handler.setConfig(config);
            handler.setParser(parser);
            handler.setCallback(mCallback);
            handler.prepare();-》会让handler发送一个message  去执行正真的准备
    DrawHandler中有一个回调
        public interface Callback {
            public void prepared();
    
            public void updateTimer(DanmakuTimer timer);
    
            public void danmakuShown(BaseDanmaku danmaku);
    
            public void drawingFinished();
    
        }
    真正的准备

    mTimeBase = SystemClock.uptimeMillis();
                    if (mParser == null || !mDanmakuView.isViewReady()) {//没有准备好,延时0.1秒后再执行
                        sendEmptyMessageDelayed(PREPARE, 100);
                    } else {
                        prepare(new Runnable() {
                            @Override
                            public void run() {
                                pausedPosition = 0;
                                mReady = true;
                                if (mCallback != null) {
                                    mCallback.prepared();
                                }
                            }
                        });
                    }
    以上是弹幕View的启动调用流程。
    那么,怎么添加弹幕捏?元芳,你怎么看?(TM我怎么知道我怎么看)看下面
        private void addDanmaku(boolean islive) {
            BaseDanmaku danmaku = mDanmakuContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL);//添加一条从右到左的滚动弹幕
            if (danmaku == null || mDvDanmaku == null) {
                return;
            }
            danmaku.text = "这是一条弹幕" + System.nanoTime();
            danmaku.padding = 5;
            danmaku.priority = 0;  // 可能会被各种过滤器过滤并隐藏显示  设置为1的话,就一定会显示  适用于本机发送的弹幕
            danmaku.isLive = islive;
            danmaku.setTime(mDvDanmaku.getCurrentTime() + 1200);
            danmaku.textSize = 25f * (mParser.getDisplayer().getDensity() - 0.6f);
            danmaku.textColor = Color.RED;//默认设置为红色字体
            danmaku.textShadowColor = Color.WHITE;
            danmaku.borderColor = Color.GREEN;//为了区别其他弹幕与自己发的弹幕,再给自己发的弹幕加上边框
            mDvDanmaku.addDanmaku(danmaku);
        }

    以上,就是黑暗火焰使基本使用方法。

    展开全文
  • 最近项目中需要添加弹幕功能,就用了B站的开源框架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;

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

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

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

    展开全文
  • 开源框架合集

    2017-08-06 18:11:47
    BiliBili弹幕 WheelPicter roundImageView bottomBar pulltoReflush MaterialDialog 工具类相关 RetrofitUtils RxJavaUtils Gson py4j 网页解析 jsoup 内存泄漏检测工具 leakcanary https:/
  • 1,弹幕弹幕库 2,
  • OpenDanmaku是Android中第三方的弹幕控件,在播放视频和直播软件中过程中弹出用户的评论,并且以滚动的方式显示。 使用 下载地址:https://github.com/linsea/OpenDanmaku 项目关联库 Gradle dependencies { ...
  • 支持弹幕 支持基本的拖动,声音、亮度调节 支持边播边缓存 支持视频本身自带rotation的旋转(90,270之类),重力旋转与手动旋转的同步支持 支持列表播放,直接添加控件为封面,列表全屏动画,视频加载速度,列表小...
  • Android弹幕DanmakuFlameMaster源码解析

    千次阅读 2018-05-14 17:34:17
    最近项目中需要添加弹幕功能,就用了B站的开源框架DanmakuFlameMaster。用法比较简单,创建一个Parser添加数据源,prepare然后start就可以了。然而会用并不够,由于比较好奇弹幕是怎么动起来的,就着重看了下这一...
  • Android 弹幕制作

    2019-03-13 16:07:17
    使用开源框架 DanmaKuView 先上布局 &lt;?xml version="1.0" encoding="utf-8"?&gt; &lt;RelativeLayout xmlns:android="http://schemas.android.com/apk...
  • 1.Android开发常用的热门的第三方库Android - skin - support :一款用心去做的Android换肤框架,只需两行... litePal:LitePal是一款开源的Android数据库框架,它采用了对象关系映射(ORM)的模式,并将我们平时开发时最常
  • 前 言 ijkplayer框架是由B站在GitHub开源的一款比较好用的开源网络播放器框架...除此之外,ijkplayer框架支持网络视频播放时弹幕的推送等功能。 开发环境 Android Studio 3.1.2 JDK 1.8 开发前准备 在Android ...
  • 关于bitmap的一些知识

    2017-03-10 18:16:59
    最近在做一个弹幕的功能,涉及到弹幕头像、礼物等下载、缓存等。使用了开源的,做起来比较顺手。原因是该开源框架比较完善,调用者很容易上手,用起来和方便。在此就不说开源框架了,其实自己要写一个开源框架,最...
  • ijkplayer

    2020-02-23 21:36:50
    文章目录 相关资料 – github 地址 ...– 开源弹幕框架 DanmakuFlameMaster (android) https://github.com/ctiao/DanmakuFlameMaster iOS build 方法 参考 readme git clone https://github.com/Bi...
  • Android代码-web-bee

    2019-08-06 11:00:09
    webBee 为乐趣而爬 webBee 基于jdk8 是一个持续成长的垂直爬虫框架项目 webBee 遵循MIT开源协议 ...可对熊猫tv、斗鱼tv等弹幕网站弹幕监听分析 制作一个炫酷的官网实例 开源讨论群 147255248 开源协议 MIT
  • 想要使用它的原因是之前在github上看到bilibili开源弹幕框架,下载体验了一下,感觉很有趣,想着结合两者使用一下。下面先看一下怎么使用ijkplayer吧!1.引入:dependencies { # required, enough for most ...
  • 心血来潮. 突然想开发一个视频分享社区类的APP. 于是想了就开始做~ 博客就来记录开发过程吧. 一方面提高自己技术水平. 另一方面自娱自乐吧....后续部分打算加上弹幕. 一些数据整合. 还想加一些社区...
  • 直播相关

    2019-09-20 21:52:11
    一、直播现状简介 Linkee.10 ...技术相对都比较成熟,设备也都支持硬编码。IOS还提供现成的 Video ToolBox框架,可以对...github上有现成的开源实现,推流、美颜、水印、弹幕、点赞动画、滤镜、播放都有。...
  • 直播现壮

    千次阅读 2016-08-13 18:09:29
    原文出处: JIAAIR  ...IOS还提供现成的 Video ToolBox框架,可以对摄像头和流媒体数据结构进行处理,但Video ToolBox框架只兼容8.0以上版本,8.0...github上有现成的开源实现,推流、美颜、水印、弹幕、点
  • 宋少东 287 人赞同 技术层面: ...技术相对都比较成熟,设备也都支持硬编码。IOS还提供现成的 Video ToolBox框架,可以对摄像头和流媒体数据...github上有现成的开源实现,推流、美颜、水印、弹幕、点赞动画、滤
  • 一、直播现状简介 Linkee.10 ...IOS还提供现成的 Video ToolBox框架,可以对摄像头和流媒体数据结构进行处理,但Video ToolBox...github上有现成的开源实现,推流、美颜、水印、弹幕、点赞动画、滤镜、播放都有。
  • 直播源码开发行业发展到今天,技术相对都比较成熟,设备也都支持硬...视频直播系统源码开发公司基本都有现成的开源实现,推拉流、美颜、私信、弹幕、礼物动画、播放都有。而且现在很多云厂商都提供SDK,直播系统源码...
  • 映客 LFLiveKit 推流

    千次阅读 2017-07-12 16:47:19
    一、直播现状简介想做一套像映客的直播系统? Linkee.101.技术实现层面:技术相对都比较成熟,设备也都支持硬编码。...github上有现成的开源实现,推流、美颜、水印、弹幕、点赞动画、滤镜、播放都有。技术其实不
  • 直播技术

    2016-12-30 11:26:14
    一、直播现状简介 想做一套像映客的直播系统?Linkee.10 1.技术实现层面: 技术相对都比较成熟,设备也都支持硬编码。...github上有现成的开源实现,推流、美颜、水印、弹幕、点赞动画、滤镜、播放都有。技

空空如也

空空如也

1 2 3
收藏数 43
精华内容 17
关键字:

弹幕开源框架