问题描述
在完成SpEditTool的时候使用了android-gif-drawable一行代码让TextView中ImageSpan支持Gif,出现过两次GifDrawable不刷新的现象
- 一次是自己限制了TextView的刷新间隔,导致刷新频率很快的gif刷新了TextView还没有刷新
- 一次是EditText刷新了但是GifDrawable没刷新,需要
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
才起作用
原因
跑了下android-gif-drawable,发现上面两个问题的原因都是同一个
先来看下代码
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);
}
}
}
这个是GifDrawable用来渲染的主要代码,RenderTask的控制是交给GifDrawable的,GifDrawable在startAnimation()和draw()这两个方法中去调度下一次渲染
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);
}
}
问题就出在这个draw()上面,Drawable的draw方法依赖于View或者ImageSpan等外部对象的调用,所以View、ImageSpan如果不刷新,RenderTask就没有了下一次渲染的机会了,gif也就停了
EditText的问题
对于上面我说的不刷新的第一种情况大家好理解,TextView没刷新嘛,GifDrawable按照上面的分析肯定也刷新不了
那EditText自己刷新了为啥也是draw()里面调度绘制的问题呢,而且设置了View.LAYER_TYPE_SOFTWARE
就好了呢?
还是看代码好了
GifDrawable的draw()方法是在DynamicDrawableSpan.draw()中被调用的
@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()
这么一条调用栈 -
TextLine.draw()方法只在
Layout.draw()
->Layout.drawText()
中调用 -
接下来只要找调用了
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的话会调用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);
}
}
从上面的代码可以看到如果有离屏渲染且开启了硬件加速,这是默认的情况,渲染会走drawHardwareAccelerated()
,反之view的LayerType为View.LAYER_TYPE_SOFTWARE的话会调用layout.draw()
最终调用到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的时候才会调用Layout.drawText()
结论
别看这一系列调用栈很长,简单的说就是EditText默认状况下(离屏渲染加硬件加速),没有调用Layout.draw()
从而导致GifDrawable.draw()没走,因为RenderTask是通过GifDrawable.draw()调度的,所以gif就停止不动了
最后
通过上面的分析我觉得这样的将RenderTask
和Gifdrawable.draw()
关联的处理方式不太合理,所以给作者提了issue和pr,建议在Drawable的invalidateSelf()中调度下次刷新,这样gif显示就不会依赖于外部对象了
作者接受了我的建议,下个版本中应该就不会出现这样的问题了,碰到同样问题又急着用的同志可以fork一个版本先自己改下
Fix GifDrawable
invalidation. Rework of #511. Fixes #510.
网友评论