弹幕框架DanmakuFlameMaster浅要分析
在我们TV端的弹幕模块,我们使用了bilibili的开源框架DanmakuFlameMaster,主要特征如下:
1. 使用多种方式(View/SurfaceView/TextureView)实现高效绘制(我们工程里面使用的是一个继承View的DanmakuView)
2. xml弹幕格式解析(我们并未使用)
3. 支持自定义字体
4. 支持多种显示效果选项实时切换
5. 支持多种方式的弹幕屏蔽
等
基本使用
初始化
添加一个第三方,我们都需要初始化它,弹幕初始化主要是针对一些我们需要的一些设置和样式进行设置
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。第一个参数如果不设置参数,那么弹幕就只能显示简单的文字,第二个参数主要是用于图文混排的时候去异步加载图片,和在弹幕显示完成之后去释放资源的比如ImageSpan的text中的一些占用内存的资源 例如drawable。如果想要实现带有背景图的弹幕,需要继承SpannedCacheStuffer重写一个BackgroundCacheStuffer类
“`java
/**
* 绘制背景(自定义弹幕样式)
*/
private class BackgroundCacheStuffer extends SpannedCacheStuffer {
// 通过扩展SimpleTextCacheStuffer或SpannedCacheStuffer个性化你的弹幕样式
final Paint paint = new Paint();
@Override
public void measure(BaseDanmaku danmaku, TextPaint paint, boolean fromWorkerThread) {
if (danmaku.showBg != 0) {
danmaku.padding = 15; // 在背景绘制模式下增加padding
}
super.measure(danmaku, paint, fromWorkerThread);
}
@Override
public void drawBackground(BaseDanmaku danmaku, Canvas canvas, float left, float top) {
if (danmaku.showBg == 1) {
Bitmap bitmap = BitmapFactory.decodeResource(DemoActivity.this.getResources(), R.drawable.im_message_bg_to_normal);
RectF destRect = new RectF(left, top, left + danmaku.paintWidth, top + danmaku.paintHeight);
canvas.drawBitmap(bitmap, null, destRect, paint);
bitmap.recycle();
} else if (danmaku.showBg == 2) {
RectF destRect = new RectF(left, top, left + danmaku.paintWidth, top + danmaku.paintHeight);
Shader mShader = new LinearGradient(left - 5, top - 5, left + danmaku.paintWidth,
top, new int[]{Color.parseColor("#00baff"), Color.TRANSPARENT}, new float[]{0, 0.9f},
Shader.TileMode.MIRROR);
paint.setShader(mShader);
canvas.drawRoundRect(destRect, 25, 25, paint);
} else {
super.drawBackground(danmaku, canvas, left, top);
}
}
@Override
public void drawStroke(BaseDanmaku danmaku, String lineText, Canvas canvas, float left, float top, Paint paint) {
// 禁用描边绘制
}
}
“`
弹幕的数据源,可以是json格式或者xml格式的数据源`
- 添加控件
想要显示,我们就需要添加控件,上面说到了主要是三个方式DanmakuView/DanmakuTextureView/DanmakuSurfaceView,使用其中三个任意一个都可以。我们选个DanmakuView方便分析。
我们可以动态添加进我们的显示区域也可以在xml文件中引用。
<RelativeLayout
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_demo"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
>
<master.flame.danmaku.ui.widget.DanmakuView
android:id="@+id/sv_danmaku"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
- 启动弹幕
mDanmakuView.setCallback(new DrawHandler.Callback() {
@Override
public void updateTimer(DanmakuTimer timer) {
}
@Override
public void drawingFinished() {
}
@Override
public void danmakuShown(BaseDanmaku danmaku) {
//Log.d("DFM", "danmakuShown(): text=" + danmaku.text);
}
@Override
public void prepared() {
Log.d("DFM", "MainActivity inline callback's method prepared");
mDanmakuView.start();
}
});
mDanmakuView.prepare(mParser, mContext);
mDanmakuView.enableDanmakuDrawingCache(true);
流程解析
该工程流程十分复杂,内部缓冲也使用了c++文件使用native方法,在这里我们只简单的分析android部分。
初始
初始包括配置和数据源的配置两部分,初始配置主要是DanmakuContext类的操作,数据源主要是来至于一开始的preper中,或者后面的addDanmu方法添加进去的。
private final 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);
}
return this;
}
启动弹幕
弹幕启动是通过
mDanmakuView.prepare(mParser, mContext);
进入prepare方法,我们可以看到
private void prepare() {
if (handler == null)
handler = new DrawHandler(getLooper(mDrawingThreadType), this, mDanmakuVisible);
}
@Override
public void prepare(BaseDanmakuParser parser, DanmakuContext config) {
prepare();
handler.setConfig(config);
handler.setParser(parser);
handler.setCallback(mCallback);
handler.prepare();
}
@Override
public void handleMessage(Message msg) {
int what = msg.what;
switch (what) {
case PREPARE:
mTimeBase = SystemClock.uptimeMillis();
if (mParser == null || !mDanmakuView.isViewReady()) {
sendEmptyMessageDelayed(PREPARE, 100);
} else {
prepare(new Runnable() {
@Override
public void run() {
pausedPosition = 0;
mReady = true;
if (mCallback != null) {
mCallback.prepared();
}
}
});
}
break;
private DanmakuTimer timer = new DanmakuTimer();//已经初始化timer
private void prepare(final Runnable runnable) {
if (drawTask == null) {//会继续调用createDrawTask方法
drawTask = createDrawTask(mDanmakuView.isDanmakuDrawingCacheEnabled(), timer,
mDanmakuView.getContext(), mDanmakuView.getWidth(), mDanmakuView.getHeight(),
mDanmakuView.isHardwareAccelerated(), new IDrawTask.TaskListener() {
@Override
public void ready() {
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();//这个才是重点,调用CacheManagingDrawTask的prepare方法
obtainMessage(NOTIFY_DISP_SIZE_CHANGED, false).sendToTarget();
return task;
}
上述过程最后一个调用了createDrawTask方法,这里先初始化了一下AndroidDisplayer配置,就相当于一个显示器
设置好弹幕显示相关的参数,然后就是创建绘制任务IDrawTask 了。这里有两个选择,如果使用缓存就创建CacheManagingDrawTask,不使用就创建DrawTask。不过CacheManagingDrawTask比DrawTask复杂很多。
DrawTask绘制任务
我们的useDrwaingCache为true(其实把它改为false也没关系,并且这样就用不上那些so库了),则创建CacheManagingDrawTask绘制任务,然后调用prepare方法。
public CacheManagingDrawTask(DanmakuTimer timer, DanmakuContext config, TaskListener taskListener, int maxCacheSize) {//传入定时器timer,config,listener,还有三分一应用分配内存大小的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);
}
//看看父类的构造方法
public DrawTask(DanmakuTimer timer, DanmakuContext context,
TaskListener taskListener) {
......
mContext = context;
mDisp = context.getDisplayer();
mTaskListener = taskListener;
mRenderer = new DanmakuRenderer(context);
......
initTimer(timer);//初始化相关定时器
......
}
protected void initTimer(DanmakuTimer timer) {
mTimer = timer;
mCacheTimer = new DanmakuTimer();
mCacheTimer.update(timer.currMillisecond);
}
CacheManagingDrawTask的构造方法设置了一些变量。其中NativeBitmapFactory.loadLibs()加载了用于创建bitmap的so文件,就是用skia图形处理库直接创建bitmap,Android对2D图形处理采用的就是skia,3D图形处理用的是OpenGL ES。这样通过native层创建bitmap直接跳过Dalvik,毕竟java层内存用多了很容易oom。native层我们在这就不研究讨论了,有兴趣的可以自己去看一下!
在上面生成task之后,调用了prepare方法
“`java
@Override
public void prepare() {
assert (mParser != null);
loadDanmakus(mParser);
mCacheManager.begin();
}
protected void loadDanmakus(BaseDanmakuParser parser) {
danmakuList = parser.setConfig(mContext).setDisplayer(mDisp).setTimer(mTimer).getDanmakus();
mContext.mGlobalFlagValues.resetAll();
if(danmakuList != null) {
mLastDanmaku = danmakuList.last();
}
}
parser设置完DanmakuContext,AndroidDisplayer,DanmakuTimer之后,再调用getDanmakus取出弹幕信息:
public IDanmakus getDanmakus() {
if (mDanmakus != null)
return mDanmakus;
mContext.mDanmakuFactory.resetDurationsData();//重新内置一些变量为null
mDanmakus = parse();//解析弹幕
releaseDataSource();//关闭JSONSource
mContext.mDanmakuFactory.updateMaxDanmakuDuration();//修正弹幕最大时长
return mDanmakus;
}
java
由于我们没有使用解析的这种情况,所以我们这里不讨论下面的解析代码,我们工程里面是使用createDanmaku 方式然后add进去的,所以我们来看弹幕工厂DanmakuFactory创建弹幕的方法:
public BaseDanmaku createDanmaku(int type, DanmakuContext context) {
if (context == null)
return null;
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);
} else if (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) {
case 1: // 从右往左滚动
instance = new R2LDanmaku(MAX_Duration_Scroll_Danmaku);
break;
case 4: // 底端固定
instance = new FBDanmaku(MAX_Duration_Fix_Danmaku);
break;
case 5: // 顶端固定
instance = new FTDanmaku(MAX_Duration_Fix_Danmaku);
break;
case 6: // 从左往右滚动
instance = new L2RDanmaku(MAX_Duration_Scroll_Danmaku);
break;
case 7: // 特殊弹幕
instance = new SpecialDanmaku();
sSpecialDanmakus.addItem(instance);
break;
}
return instance;
}
//修正试图宽高,缩放比,弹幕时长
public boolean updateViewportState(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;
}
//修正弹幕最长时长
public void updateMaxDanmakuDuration() {
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);
}
java
DanmakuFactory创建弹幕主要是计算了弹幕时长,然后根据不同类型创建不同的弹幕。继续回到刚才的prepare方法,往下继续执行:
@Override
public void prepare() {
assert (mParser != null);
loadDanmakus(mParser);//走完了
mCacheManager.begin();//走这个
}
//CacheManager的方法
public void begin() {
mEndFlag = false;
//创建一个HandlerThread用于在工作线程处理事务
if (mThread == null) {
mThread = new HandlerThread(“DFM Cache-Building Thread”);
mThread.start();
}
//创建一个Handler和HandlerThread搭配用
if (mHandler == null)
mHandler = new CacheHandler(mThread.getLooper());
mHandler.begin();// 走到这里
}
//HandlerThread的begin方法
public void begin() {
sendEmptyMessage(PREPARE);
……
}
java
我们可以看到创建了一个HandlerThread,然后创建了一个CacheHandler,所以CacheHandler发送消息后,处理消息内容都是在子线程。然后发送了PREPARE消息,然后就是回调handleMessage方法:
DrawingCachePoolManager mCachePoolManager = new DrawingCachePoolManager();
//创建一个缓存个数上限为800的FinitePool池
Pool mCachePool = Pools.finitePool(mCachePoolManager, 800);
//Pools的finitePool方法
public static
//弹幕的基类都是BaseDanmaku,只有子类R2LDanmaku重写了measure方法
@Override //R2LDanmaku的measure方法
public void measure(IDisplayer displayer, boolean fromWorkerThread) {
super.measure(displayer, fromWorkerThread);//调用了父类的方法
mDistance = (int) (displayer.getWidth() + paintWidth);//滚动弹幕的距离都是 视图宽度+弹幕宽度,很好理解
mStepX = mDistance / (float) duration.value; //每秒步长就是总滚动距离除以弹幕时长
}
//父类BaseDanmaku的measure方法
public void measure(IDisplayer displayer, boolean fromWorkerThread) {
displayer.measure(this, fromWorkerThread);//AndroidDisplayer的measure方法
this.measureResetFlag = flags.MEASURE_RESET_FLAG;//设置已经测量过了的标签
}
接着会调用AndroidDisplayer的measure方法:
@Override
public void measure(BaseDanmaku danmaku, boolean fromWorkerThread) {
...设置画笔style,color,alpha,省略...
calcPaintWH(danmaku, paint, fromWorkerThread);//计算宽高
...设置画笔style,color,alpha,省略...
}
private BaseCacheStuffer sStuffer = new SimpleTextCacheStuffer();//默认是SimpleTextCacheStuffer
private void calcPaintWH(BaseDanmaku danmaku, TextPaint paint, boolean fromWorkerThread) {
sStuffer.measure(danmaku, paint, fromWorkerThread);//sStuffer就是我们在MainActivity里配置DanmakuContext时设置的,默认是SimpleTextCacheStuffer
...加上描边,padding等额外值,省略...
}
2) 在mCaches缓存的20条内查找和目标弹幕样式完全一样的弹幕(文字、大小、边框、下划线、颜色完全相同): 先回到buildCache方法中这个位置。
“`java
private byte buildCache(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
private byte buildCache(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;//先清除过时弹幕的缓存
//再根据目标弹幕样式,重新设置缓存(为每条弹幕创建一个bitmap和canvas,然后画出边框、下划线、文字等等)
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;
}
}
return null;
}
如果在上述第二策略中,在过时的缓存中找到了和目标弹幕宽高差不多的缓存项,则根据目标弹幕样式,重新设置缓存(为每条弹幕创建一个bitmap和canvas,然后画出边框、下划线、文字等等),调用DanmakuUtils.buildDanmakuDrawingCache(item, mDisp, cache)方法:
public static 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 组建弹幕缓存:
java
//DrawingCache的build方法
public void build(int w, int h, int density, boolean checkSizeEquals) {//checkSizeEquals为false
final DrawingCacheHolder holder = mHolder;
//每个DrawingCache都有一个DrawingCacheHolder
holder.buildCache(w, h, density, checkSizeEquals);//DrawingCacheHolder的buildCache方法
mSize = mHolder.bitmap.getRowBytes() * mHolder.bitmap.getHeight();//返回创建的bitmap的大小
}
//DrawingCacheHolder的buildCache方法
public void buildCache(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 绘制弹幕内容:
“`java
public synchronized void drawDanmaku(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);
}
}
//设置画笔
private void applyPaintConfig(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);
}
}
java
No.3 切割超过一屏的弹幕:
//DrawingCacheHolder的splitWith方法
public void splitWith(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();
//切割bitmap到bitmapArray中
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;
}
“`
到这里我们buildCache(item, false)的策略二中的重新设置缓存DanmakuUtils.buildDanmakuDrawingCache(item, mDisp, cache)就走完了。然后将这个目标弹幕的引用放入缓存Danmakus中(mCaches),同时更新已使用大小mRealSize。同时注意mCaches内部成员items是TreeSet类型,不能添加相同的对象。
4)如果上述两次查找缓存都没找到,则从FinitePool中取出一个,没有就new一个,然后同上配置DrawingCache:
继续回到buildCache方法这个位置:
private byte buildCache(BaseDanmaku item, boolean forceInsert) {//item, false
...测量过了...
...第一策略已经pass...
...第二策略已经pass...
//如果上述两次查找缓存都没找到,则进入下面逻辑
// guess cache size
if (!forceInsert) {//如果forceInsert为false,则表示不检测内存超出
//计算此弹幕bitmap的大小,width * height * 4
//(因为用native创建的Bitmap的Config为ARGB_8888,所以一个像素占4个字节)
int cacheSize = DanmakuUtils.getCacheSize((int) item.paintWidth,
(int) item.paintHeight);
//如果当前已经使用大小 + 此弹幕缓存大小 > 设置的最大内存(2/3 应用内存)
if (mRealSize + cacheSize > mMaxSize) {//没有超
return RESULT_FAILED;
}
}
//从FinitePool中的300个DrawingCache对象中取出来一个
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;
......
}
//FinitePool的acquire方法,从缓存链表头取出一个对象
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方法中,最后更新一下缓存定时器的时间,到缓存的最后一条弹幕的出现时间:
private long prepareCaches(boolean repositioned) {
...截取三倍弹幕时间内所有弹幕,并为他们一一建立缓存...
if (item != null) {//截取的最后一条弹幕,更新缓存定时器时间到它的出现时间
mCacheTimer.update(item.time);
} else {
mCacheTimer.update(end);
}
}
prepareCaches方法走完后,回到处理原先处理BUILD_CACHES消息的逻辑中,继续执行剩余部分:
java
//CacheHandler的handleMessage方法
public void handleMessage(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;
......
}
//DrawHandler的prepare方法
private void prepare(final Runnable runnable) {
if (drawTask == null) {
drawTask = createDrawTask(mDanmakuView.isDanmakuDrawingCacheEnabled(), timer,
mDanmakuView.getContext(), mDanmakuView.getWidth(), mDanmakuView.getHeight(),
mDanmakuView.isHardwareAccelerated(), new IDrawTask.TaskListener() {
@Override
public void ready() {
initRenderingConfigs();//初始化一些渲染参数
runnable.run();//执行runnable的run方法,继续追踪
}
......
});
} else {
runnable.run();
}
}
//DrawHandler的initRenderingConfigs方法
private void initRenderingConfigs() {
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逻辑处:
继续追踪mCallback.prepared(),会回到MainActivity当中我们设置DanmakuView的地方:
我们里面设置了mDanmakuView.start();
然后就是DrawHandler发送START消息:
//DrawHandler的handleMessage方法
public void handleMessage(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();//CacheManagingDrawTask的start方法
......
} else {
......
}
break;
case UPDATE:
if (mUpdateInNewThread) {//在DrawHandler构造方法里赋值的变量,只有当可用CPU个数大于3时才为true
updateInNewThread();//四核,八核的请进
} else {
updateInCurrentThread();//单核,双核的请进
}
break;
......
}
上述逻辑最后会进入RESUME消息处理中,先调用CacheManagingDrawTask的start方法,然后处理UPDATE消息。我们先看看CacheManagingDrawTask的start方法:
“`java
//CacheManagingDrawTask的start方法
public void start() {
……
mCacheManager.resume();//CacheManager的resume方法
}
//继续跟CacheManager的resume方法
public void resume() {
……
mHandler.resume();//CacheManagingDrawTask的resume方法
……
}
//继续跟CacheManagingDrawTask的resume方法
public void resume() {
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消息处理逻辑:
```java
//CacheHandler的handleMessage方法
public void handleMessage(Message msg) {
.......
case CLEAR_TIMEOUT_CACHES:
clearTimeOutCaches();//继续跟这个
break;
......
}
//调用 clearTimeOutCaches方法
private void clearTimeOutCaches() {
clearTimeOutCaches(mTimer.currMillisecond);//调用重载方法,参数为主定时器当前时间
}
//调用重载方法,参数为主定时器当前时间
private void clearTimeOutCaches(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;
}
}
}
<div class="se-preview-section-delimiter"></div>
CLEAR_TIMEOUT_CACHES消息处理就分析完了,就是移除缓存弹幕mCache中过时的弹幕,并且销毁他们持有的DrawingCache,同时销毁内部的bitmap、canvas等。
绘制弹幕
CacheManagingDrawTask的start方法就分析完了,继续回到DrawHandler的handleMessage方法,接着处理UPDATE消息:
//DrawHandler的handleMessage方法
public void handleMessage(Message msg) {
case UPDATE:
if (mUpdateInNewThread) {//在DrawHandler构造方法里赋值的变量,只有当可用CPU个数大于3时才为true
updateInNewThread();//四核,八核的请进
} else {
updateInCurrentThread();//单核,双核的请进
}
break;
......
}
<div class="se-preview-section-delimiter"></div>
到这里,我们应该能猜到接下要进行应该就是绘制工作了。其实updateInNewThread和updateInCurrentThread做的事情是一样的,只不过其中一个新开了子线程去做这些事情。两者的工作原理都是更新定时器,然后postInvalidate,使DanmakuView重绘,然后再发UPDATE消息,重复上述过程。
private void updateInNewThread() {
if (mThread != null) {
return;
}
mThread = new UpdateThread("DFM Update") {
@Override
public void run() {
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;
}
//上面逻辑是为了延时,稳定帧率
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();
}
<div class="se-preview-section-delimiter"></div>
updateInNewThread主要做了两件事:延时然后同步主定时器时间,然后通知DanmakuView重绘。
我们先看同步主定时器时间:
private final long syncTimer(long startMS) {
......
long d = 0;
long time = startMS - mTimeBase;//当前时间到初始时间的时间差
......
long gapTime = time - timer.currMillisecond;//总时间差减去上一次绘制完成时间,得到绘制间隙时间
long averageTime = Math.max(mFrameUpdateRate, getAverageRenderingTime());//计算绘制间隙平均时间,大于等于16(getAverageRenderingTime方法是计算加入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;
}
//计算平均绘制间隔时间
private synchronized long getAverageRenderingTime() {
int frames = mDrawTimes.size();
if(frames <= 0)
return 0;
long dtime = mDrawTimes.getLast() - mDrawTimes.getFirst();
return dtime / frames;
}
<div class="se-preview-section-delimiter"></div>
syncTimer主要是计算了一下绘制间隔时间,然后同步一下主定时器。
然后我们看看通知DanmakuView重绘部分:
//DanmakuView的drawDanmakus方法
public long drawDanmakus() {
long stime = SystemClock.uptimeMillis();
lockCanvas();//再看看lockCanvas
return SystemClock.uptimeMillis() - stime;//返回等待时间差
}
//DanmakuView的lockCanvas方法
private void lockCanvas() {
......
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,一边下次进入方法后再次进入上述等待逻辑
}
}
private void postInvalidateCompat() {
mRequestRender = true;//将mRequestRender 标志位置为true,一遍onDraw方法逻辑执行
//通知view重绘
if(Build.VERSION.SDK_INT >= 16) {
this.postInvalidateOnAnimation();
} else {
this.postInvalidate();
}
}
<div class="se-preview-section-delimiter"></div>
这样就能保证保证每隔一定时间(这个时间通过syncTimer计算),更新主定时器(就是从0开始,往后每次加上(间隔时间 + 绘制时间)),然后执行postInvalidate通知DanmakuView重绘
postInvalidate后,View重绘,会重走onDraw方法,所以我们进入DanmakuView的onDraw方法看看:
“`java
//DanmakuView的onDraw方法
protected void onDraw(Canvas canvas) {
if ((!mDanmakuVisible) && (!mRequestRender)) {//如果没有请求重绘则mRequestRender为false,不会绘制弹幕
super.onDraw(canvas);
return;
}
……
if (handler != null) {
RenderingState rs = handler.draw(canvas);//DrawHandler的draw方法
……
}
……
//绘制结束后将mRequestRender 标志位重新设为false,
//以便下一次发绘制消息时进入等待逻辑等候绘制结束,这个上面DanmakuView的drawDanmakus方法提到过
mRequestRender = false;
unlockCanvasAndPost();//通知UpdateThread绘制完成
}
private void unlockCanvasAndPost() {
synchronized (mDrawMonitor) {
mDrawFinished = true;//将mDrawFinished 置为true,以便DanmakuView的lockCanvas方法跳出循环,这个上面也提到过
mDrawMonitor.notifyAll();
}
}
public RenderingState draw(Canvas canvas) {
……
mDisp.setExtraData(canvas);//将canvas一些信息设置给AndroidDisplayer
mRenderingState.set(drawTask.draw(mDisp));//绘制部分是drawTask.draw(mDisp)
recordRenderingTime();//记录绘制结束时间
return mRenderingState;
}
//还记得上面的DrawHandler的syncTimer方法吗?里面调用了getAverageRenderingTime计算绘制平均间隔时间,
//其中用到的mDrawTimes变量就是在这里添加元素的
private synchronized void recordRenderingTime() {
long lastTime = SystemClock.uptimeMillis();
mDrawTimes.addLast(lastTime);//将绘制结束时间加入到类型为LinkedList的mDrawTimes集合中
int frames = mDrawTimes.size();
if (frames > MAX_RECORD_SIZE) {//最大容量为500个绘制时间,超出了则移除第一个
mDrawTimes.removeFirst();
}
}
“`
上述逻辑中,我的注释部分先分析了记录绘制结束时间部分,填了上边syncTimer时的坑。
然后应该进入主要绘制部分了drawTask.draw(mDisp),也就是CacheManagingDrawTask的draw方法
//CacheManagingDrawTask的draw方法
public RenderingState draw(AbsDisplayer displayer) {
RenderingState result = super.draw(displayer);//会调用父类的draw方法
......
return result;
}
//DrawTask的draw方法
public synchronized RenderingState draw(AbsDisplayer displayer) {
return drawDanmakus(displayer,mTimer);//又调用了drawDanmakus方法
}
//DrawTask的drawDanmakus方法
protected RenderingState drawDanmakus(AbsDisplayer disp, DanmakuTimer timer) {
......
if (danmakuList != null) {
Canvas canvas = (Canvas) disp.getExtraData();//取出DanmakuView的canvas
//当前时间 - 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);
......
}
}
<div class="se-preview-section-delimiter"></div>
截取完弹幕数据后,就是绘制了,继续执行下面逻辑(mRenderer.draw(mDisp, danmakus, mStartRenderTime)),开始绘制工作:
//DanmakuRenderer的draw方法
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绘制的
......
} else if(renderingType == IRenderer.TEXT_RENDERING) {//如果使用缓存绘制失败,则会使用原声方法Canvas去draw
......
if (mCacheManager != null) {
mCacheManager.addDanmaku(drawItem);//再次为词条弹幕构建缓存,以便下次使用缓存bitmap绘制
}
}
......
}
<div class="se-preview-section-delimiter"></div>
从截取的弹幕中遍历每一个,然后一一绘制。绘制步骤有如下几步:
- 如果弹幕还没有到出现时间,则检查它有没有缓存,如果没有则为它建立缓存;
//调用CacheManagingDrawTask的addDanmaku方法
public void addDanmaku(BaseDanmaku danmaku) {
if (mHandler != null) {
......
//CacheHandler
mHandler.obtainMessage(CacheHandler.ADD_DANMAKKU, danmaku).sendToTarget();
......
}
}
//CacheHandler
public void handleMessage(Message msg) {
case ADD_DANMAKKU:
BaseDanmaku item = (BaseDanmaku) msg.obj;
addDanmakuAndBuildCache(item);//调用了addDanmakuAndBuildCache方法
break;
}
//调用了addDanmakuAndBuildCache方法
private final void addDanmakuAndBuildCache(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方法我们上面分析过,就是用来建立缓存的)
}
}
<div class="se-preview-section-delimiter"></div>
- measure 测量,我们之前prepareCache已经为他们在buildCache时测量过了;
- layout 布局,计算弹幕在屏幕上应该显示的位置;
//调用DanmakusRetainer的fix方法
public void fix(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方法:
//保存需要显示的弹幕容器类(保存的一行只有一条弹幕,下面会说明的),内部持有一个以弹幕的y坐标排序的TreeSet集合,这个需要注意
protected Danmakus mVisibleDanmakus = new Danmakus(Danmakus.ST_BY_YPOS);
//AlignTopRetainer的fix方法
public void fix(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 添加还未布局的新弹幕
}
} else if (overwriteInsert && minRightRow != null) {//没有空行可以插入
topPos = minRightRow.getTop();//暂时放到最最左边的弹幕那一行(excuse me ???)
checkEdge = false;//不做范围检查
shown = false;
} else if (lastItem != null) {//找不到插入的位置
topPos = lastItem.getBottom();//暂时放到最低位置的弹幕下面,下面检测边界时会酌情河蟹
willHit = false;//置false碰壁标志
} else if (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;
} else if (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);
}
}
//清除容器,重新放入新的内容
public void clear() {
mCancelFixingFlag = true;
mVisibleDanmakus.clear();
}
<div class="se-preview-section-delimiter"></div>
思路其实就是这样
1). 先往第一行添加一条弹幕,把它存到一个容器里(这个容器会把新添加进来的弹幕按照y值从小到大排序,而且容器只保存每一行的最后一条弹幕)。
2). 然后添加第二条弹幕,从第一行开始添加,先判断和第一条弹幕会不会碰壁,如果不会碰壁则添加到这一行,然后容器内移除之前第一条的弹幕,保存这一条弹幕;如果会碰壁则添加到下一行,然后容器保存这条弹幕
3). 然后添加第三条,继续从第一行开始添加,先判断和第一条……(重复第二条的逻辑)……;
上面逻辑完成了弹幕定位规则(内部那个layout接下来再讲),限于篇幅,我只挑一个检查碰撞的代码贴出来分析,其它的请有兴趣者自行跟踪。
public static boolean willHitInDuration(IDisplayer disp, BaseDanmaku d1, BaseDanmaku d2,
long duration, long currTime) {//disp, item, drawItem, drawItem.getDuration(), drawItem.getTimer().currMillisecond
final int type1 = d1.getType();
final int type2 = d2.getType();
// allow hit if different type 不同类型的弹幕允许碰撞
if(type1 != type2)
return false;
if(d1.isOutside()){//item已经跑出视图了,不存在碰撞问题
return false;
}
long dTime = d2.time - d1.time;
if (dTime <= 0)//drawItem在item前面,已经碰撞了
return true;
//两者出现时间已经相差一条弹幕时间了 || item超时跑出去了 || drawItem超时 ,都不会碰撞
if (Math.abs(dTime) >= duration || d1.isTimeOut() || d2.isTimeOut()) {
return false;
}
//item和drawItem都是顶部或者底部固定弹幕,因为在同一行,必定碰撞
if (type1 == BaseDanmaku.TYPE_FIX_TOP || type1 == BaseDanmaku.TYPE_FIX_BOTTOM) {
return true;
}
//调用checkHitAtTime方法
return checkHitAtTime(disp, d1, d2, currTime)
|| checkHitAtTime(disp, d1, d2, d1.time + d1.getDuration());
}
//调用checkHitAtTime方法
private static boolean checkHitAtTime(IDisplayer disp, BaseDanmaku d1, BaseDanmaku d2, long time){//time = currTime || time = item.time + item.duration
final float[] rectArr1 = d1.getRectAtTime(disp, time);//time获得item在视图的(l,t,r,b)
final float[] rectArr2 = d2.getRectAtTime(disp, time);//time获得drawItem在视图的(l,t,r,b)
if (rectArr1 == null || rectArr2 == null)
return false;
return checkHit(d1.getType(), d2.getType(), rectArr1, rectArr2);
}
//调用checkHit方法
private static boolean checkHit(int type1, int type2, float[] rectArr1,
float[] rectArr2) {
if(type1 != type2)
return false;
if (type1 == BaseDanmaku.TYPE_SCROLL_RL) {//只要drawItem的left小于item的right就碰撞了
// hit if left2 < right1
return rectArr2[0] < rectArr1[2];
}
if (type1 == BaseDanmaku.TYPE_SCROLL_LR){
// hit if right2 > left1
return rectArr2[2] > rectArr1[0];
}
return false;
}
//R2LDanmaku的getRectAtTime方法
public float[] getRectAtTime(IDisplayer displayer, long time) {//time = currTime || time = item.time + item.duration
if (!isMeasured())
return null;
float left = getAccurateLeft(displayer, time);//获得此时弹幕在视图的x坐标
if (RECT == null) {
RECT = new float[4];
}
RECT[0] = left;//left
RECT[1] = y;//top
RECT[2] = left + paintWidth;//right
RECT[3] = y + paintHeight;//bottom
return RECT;
}
//R2LDanmaku的getAccurateLeft方法
protected float getAccurateLeft(IDisplayer displayer, long currTime) {//currTime = timer.currTime || currTime = item.time + item.duration
long elapsedTime = currTime - time;//当前时间 - 弹幕出现时间
......
//因此返回弹幕位于视图的x坐标,即视图宽度 - 弹幕已经显示了多少秒 * 每秒移动步长
return displayer.getWidth() - elapsedTime * mStepX;
}
<div class="se-preview-section-delimiter"></div>
简单来说就是先根据当前时间就算出两条弹幕的位置(l1,t1,r1,b1),看看是否前面弹幕的 r1 小于后面弹幕的 l1;再根据前面弹幕的结束时间,计算出两条弹幕的位置(l2,t2,r2,b2)再次看看是否前面弹幕的 r2小于后面弹幕的 l2。只有两条都满足才不会碰撞。
继续回到AlignTopRetainer的fix方法,还有一个drawItem.layout(disp, drawItem.getLeft(), topPos);没讲呢,这才是真正确认弹幕位置的地方,继续查看L2RDanmaku的layout方法:
public void layout(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);
}
<div class="se-preview-section-delimiter"></div>
- draw 绘制弹幕。
//DanmakuRenderer的draw方法
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绘制的
......
} else if(renderingType == IRenderer.TEXT_RENDERING) {//如果使用缓存绘制失败,则会使用原声方法Canvas去draw
......
if (mCacheManager != null) {
mCacheManager.addDanmaku(drawItem);//再次为词条弹幕构建缓存,以便下次使用缓存bitmap绘制
}
}
......
}
// 继续跟踪int renderingType = drawItem.draw(disp)
//BaseDanmaku的draw方法
public int draw(IDisplayer displayer) {
return displayer.draw(this);//调用AndroidDisplayer的draw方法
}
//调用AndroidDisplayer的draw方法
public int draw(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) {
//DrawingCacheHolder的draw方法,我们在上面的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的流程简单分析过程了。thanks