問題描述
在完成SpEditTool的時(shí)候使用了android-gif-drawable一行代碼讓TextView中ImageSpan支持Gif,出現(xiàn)過兩次GifDrawable不刷新的現(xiàn)象
- 一次是自己限制了TextView的刷新間隔寞忿,導(dǎo)致刷新頻率很快的gif刷新了TextView還沒有刷新
- 一次是EditText刷新了但是GifDrawable沒刷新尺借,需要
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
才起作用
原因
跑了下android-gif-drawable,發(fā)現(xiàn)上面兩個(gè)問題的原因都是同一個(gè)
先來看下代碼
class RenderTask extends SafeRunnable {
RenderTask(GifDrawable gifDrawable) {
super(gifDrawable);
}
@Override
public void doWork() {
final long invalidationDelay = mGifDrawable.mNativeInfoHandle.renderFrame(mGifDrawable.mBuffer);
if (invalidationDelay >= 0) {
mGifDrawable.mNextFrameRenderTime = SystemClock.uptimeMillis() + invalidationDelay;
if (mGifDrawable.isVisible() && mGifDrawable.mIsRunning && !mGifDrawable.mIsRenderingTriggeredOnDraw) {
mGifDrawable.mExecutor.remove(this);
mGifDrawable.mRenderTaskSchedule = mGifDrawable.mExecutor.schedule(this, invalidationDelay, TimeUnit.MILLISECONDS);
}
if (!mGifDrawable.mListeners.isEmpty() && mGifDrawable.getCurrentFrameIndex() == mGifDrawable.mNativeInfoHandle.getNumberOfFrames() - 1) {
mGifDrawable.mInvalidationHandler.sendEmptyMessageAtTime(mGifDrawable.getCurrentLoop(), mGifDrawable.mNextFrameRenderTime);
}
} else {
mGifDrawable.mNextFrameRenderTime = Long.MIN_VALUE;
mGifDrawable.mIsRunning = false;
}
if (mGifDrawable.isVisible() && !mGifDrawable.mInvalidationHandler.hasMessages(MSG_TYPE_INVALIDATION)) {
mGifDrawable.mInvalidationHandler.sendEmptyMessageAtTime(MSG_TYPE_INVALIDATION, 0);
}
}
}
這個(gè)是GifDrawable用來渲染的主要代碼盈电,RenderTask的控制是交給GifDrawable的,GifDrawable在startAnimation()和draw()這兩個(gè)方法中去調(diào)度下一次渲染
void startAnimation(long lastFrameRemainder) {
if (mIsRenderingTriggeredOnDraw) {
mNextFrameRenderTime = 0;
mInvalidationHandler.sendEmptyMessageAtTime(MSG_TYPE_INVALIDATION, 0);
} else {
cancelPendingRenderTask();
mRenderTaskSchedule = mExecutor.schedule(mRenderTask, Math.max(lastFrameRemainder, 0), TimeUnit.MILLISECONDS);
}
}
public void draw(@NonNull Canvas canvas) {
final boolean clearColorFilter;
if (mTintFilter != null && mPaint.getColorFilter() == null) {
mPaint.setColorFilter(mTintFilter);
clearColorFilter = true;
} else {
clearColorFilter = false;
}
if (mTransform == null) {
canvas.drawBitmap(mBuffer, mSrcRect, mDstRect, mPaint);
} else {
mTransform.onDraw(canvas, mPaint, mBuffer);
}
if (clearColorFilter) {
mPaint.setColorFilter(null);
}
if (mIsRenderingTriggeredOnDraw && mIsRunning && mNextFrameRenderTime != Long.MIN_VALUE) {
final long renderDelay = Math.max(0, mNextFrameRenderTime - SystemClock.uptimeMillis());
mNextFrameRenderTime = Long.MIN_VALUE;
mExecutor.remove(mRenderTask);
mRenderTaskSchedule = mExecutor.schedule(mRenderTask, renderDelay, TimeUnit.MILLISECONDS);
}
}
問題就出在這個(gè)draw()上面,Drawable的draw方法依賴于View或者ImageSpan等外部對(duì)象的調(diào)用蝴簇,所以View、ImageSpan如果不刷新匆帚,RenderTask就沒有了下一次渲染的機(jī)會(huì)了熬词,gif也就停了
EditText的問題
對(duì)于上面我說的不刷新的第一種情況大家好理解,TextView沒刷新嘛,GifDrawable按照上面的分析肯定也刷新不了
那EditText自己刷新了為啥也是draw()里面調(diào)度繪制的問題呢互拾,而且設(shè)置了View.LAYER_TYPE_SOFTWARE
就好了呢歪今?
還是看代碼好了
GifDrawable的draw()方法是在DynamicDrawableSpan.draw()中被調(diào)用的
@Override
public void draw(Canvas canvas, CharSequence text,
int start, int end, float x,
int top, int y, int bottom, Paint paint) {
Drawable b = getCachedDrawable();
canvas.save();
int transY = bottom - b.getBounds().bottom;
if (mVerticalAlignment == ALIGN_BASELINE) {
transY -= paint.getFontMetricsInt().descent;
}
canvas.translate(x, transY);
b.draw(canvas);
canvas.restore();
}
順探摸瓜找到了
TextLine.draw()
->TextLine.drawRun()
->TextLine.handleRun()
->TextLine.handleReplacement()
->DynamicDrawableSpan.draw()
這么一條調(diào)用棧TextLine.draw()方法只在
Layout.draw()
->Layout.drawText()
中調(diào)用接下來只要找調(diào)用了
Layout.draw()
的地方
TextView.onDraw()方法
@Override
protected void onDraw(Canvas canvas) {
...
if (mEditor != null) {
mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
} else {
layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
}
..
}
...
如果是EditText的話會(huì)調(diào)用Editor.onDraw()方法
void onDraw(Canvas canvas, Layout layout, Path highlight, Paint highlightPaint,
int cursorOffsetVertical) {
...
if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) {
drawHardwareAccelerated(canvas, layout, highlight, highlightPaint,
cursorOffsetVertical);
} else {
layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
}
}
從上面的代碼可以看到如果有離屏渲染且開啟了硬件加速,這是默認(rèn)的情況,渲染會(huì)走drawHardwareAccelerated()
,反之view的LayerType為View.LAYER_TYPE_SOFTWARE的話會(huì)調(diào)用layout.draw()
最終調(diào)用到GifDrawable.draw()
看下drawHardwareAccelerated()
private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
Paint highlightPaint, int cursorOffsetVertical) {
...
} else {
// Boring layout is used for empty and hint text
layout.drawText(canvas, firstLine, lastLine);
}
}
只在EditText顯示hint的時(shí)候才會(huì)調(diào)用Layout.drawText()
結(jié)論
別看這一系列調(diào)用棧很長(zhǎng)颜矿,簡(jiǎn)單的說就是EditText默認(rèn)狀況下(離屏渲染加硬件加速)寄猩,沒有調(diào)用Layout.draw()
從而導(dǎo)致GifDrawable.draw()沒走,因?yàn)镽enderTask是通過GifDrawable.draw()調(diào)度的或衡,所以gif就停止不動(dòng)了
最后
通過上面的分析我覺得這樣的將RenderTask
和Gifdrawable.draw()
關(guān)聯(lián)的處理方式不太合理焦影,所以給作者提了issue和pr,建議在Drawable的invalidateSelf()中調(diào)度下次刷新封断,這樣gif顯示就不會(huì)依賴于外部對(duì)象了
作者接受了我的建議陌选,下個(gè)版本中應(yīng)該就不會(huì)出現(xiàn)這樣的問題了,碰到同樣問題又急著用的同志可以fork一個(gè)版本先自己改下
Fix GifDrawable
invalidation. Rework of #511. Fixes #510.