Glide遇坑记之决战
上述三种解决方法都避免了与真正BOSS---TransitionDrawable的正面交锋。TransitionDrawable继承自LayerDrawable。LayerDrawable是一个特殊的Drawable,它内部保持着一个Drawable数组,其中每一个Drawable都是视图中的一层。通过多个Drawable的叠加、渐变、旋转等组合显示出与单一Drawable不同的效果。
@Override
public boolean animate(T current, ViewAdapter adapter) {
Drawable previous = adapter.getCurrentDrawable();
if (previous != null) {
TransitionDrawable transitionDrawable = new TransitionDrawable(new Drawable[] { previous, current });
transitionDrawable.setCrossFadeEnabled(true);
transitionDrawable.startTransition(duration);
adapter.setDrawable(transitionDrawable);
return true;
} else {
defaultAnimation.animate(current, adapter);
return false;
}
}
public void startTransition(int durationMillis) {
mFrom = 0;
mTo = 255;
mAlpha = 0;
mDuration = mOriginalDuration = durationMillis;
mReverse = false;
mTransitionState = TRANSITION_STARTING;
invalidateSelf();
}
DrawableCrossFadeViewAnimation.animate()方法先是获取TransitionDrawable对象实例,接着调用setCrossFadeEnabled()和startTransition()方法对TransitionDrawable的成员变量进行设置。并在startTransition()方法的最后一行,调用invalidateSelf()方法尝试进行视图重绘。
public void invalidateSelf() {
final Callback callback = getCallback();
if (callback != null) {
callback.invalidateDrawable(this);
}
}
invalidateSelf()方法先是获取TransitionDrawable注册的Callback实例,如果无则返回null。通过Callback接口,一个Drawable实例可以回调其客户端来执行动画。为了动画可以被执行,所有的客户端都应该支持这个Callback接口。 View类正是实现了Callback接口,所以callback.invalidateDrawable()其实调用的就是View中的invalidateDrawable()方法。 但此时TransitionDrawable实例未注册任何Callback接口,invalidateSelf()方法直接返回。
紧接着animation()中执行adapter.setDrawable()方法,方法内部通过view.setImageDrawable(drawable)来更新Drawable。
public void setImageDrawable(@Nullable Drawable drawable) {
if (mDrawable != drawable) {
mResource = 0;
mUri = null;
final int oldWidth = mDrawableWidth;
final int oldHeight = mDrawableHeight;
updateDrawable(drawable);
if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
requestLayout();
}
invalidate();
}
}
view.setImageDrawable(drawable)方法内部先是通过updateDrawable(drawable)更新成员变量mDrawable,同时修改其属性。 接着调用invalidate()方法正式开始视图重绘。
private void updateDrawable(Drawable d) {
if (mDrawable != null) {
sameDrawable = mDrawable == d;
mDrawable.setCallback(null);
unscheduleDrawable(mDrawable);
if (!sCompatDrawableVisibilityDispatch && !sameDrawable && isAttachedToWindow()) {
mDrawable.setVisible(false, false);
}
}
mDrawable = d;
if (d != null) {
d.setCallback(this);
d.setLayoutDirection(getLayoutDirection());
d.setLevel(mLevel);
configureBounds();
} else {
mDrawableWidth = mDrawableHeight = -1;
}
}
updateDrawble()首先对mDrawable做了一些检查,并将与ImageView关联的Drawable实例mDrawable的Callback置空。接着把传进来的Drawable对象赋给成员变量mDrawable。如果参数d不为空的话,那么设置d的Callback为ImageView实例。通过d.getIntrinsicWidth()获取drawable的width赋值全局变量mDrawableWidth。
Android视图重绘机制
View的源码中会有数个invalidate()方法的重载和一个invalidateDrawable()方法,最终都是通过invalidateInternal()方法来实现视图重制。
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, boolean fullInvalidate) {
if (mGhostView != null) {
mGhostView.invalidate(true);
return;
}
if (skipInvalidate()) {
return;
}
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
|| (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
|| (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED|| (fullInvalidate && isOpaque() != mLastIsOpaque)) {
if (fullInvalidate) {
mLastIsOpaque = isOpaque();
mPrivateFlags &= ~PFLAG_DRAWN;
}
mPrivateFlags |= PFLAG_DIRTY;
if (invalidateCache) {
mPrivateFlags |= PFLAG_INVALIDATED;
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
p.invalidateChild(this, damage);
}
}
}
在这个方法中首先会调用skipInvalidate()方法来判断当前 View是否需要重绘,判断的逻辑也比较简单,如果View是不可见的且没有执行任何动画,就认为不需要重绘了。之后会进行透明度的判断,并给View添加一些标记位,然后调用ViewParent的invalidateChild()方法,这里的ViewParent其实就是当前视图的父视图,因此会调用到ViewGroup的invalidateChild()方法中。省略若干循环调用。最终经过多次辗转的调用,最终会走到视图绘制的入口方法performTraversals()中,然后重新执行绘制流程。
invalidate()方法虽然最终会调用到performTraversals()方法中,但这时measure和layout流程是不会重新执行的,因为视图没有强制重新测量的标志位,而且大小也没有发生过变化,所以这时只有draw流程可以得到执行。而如果你希望视图的绘制流程可以完完整整地重新走一遍,就不能使用invalidate()方法,而应该调用requestLayout()。
绘制流程始于ViewRootImpl的performDraw()方法,里面又调用了ViewRootImpl的draw()方法,经过一系列调用,然后实例化Canvas对象,锁定该canvas的区域并进行一系列的属性赋值,最后调用了mView.draw(canvas)方法,这个mView就是DecorView,也就是说从DecorView开始绘制。由于ViewGroup没有重写draw方法,因此所有的View都是通过调用View的draw()方法实现绘制。
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// we're done...
return;
}
...
}
draw过程比较复杂,但是逻辑十分清晰,一般是遵循下面几个步骤:
- 绘制背景 --
drawBackground() - 绘制内容 --
onDraw() - 绘制孩子 --
dispatchDraw() - 绘制装饰 --
onDrawScrollbars()
View中的onDraw()方法是一个空实现,不同的View有着不同的内容,这需要我们自己去实现,即在自定义View中重写该方法来实现。
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mDrawable == null) {
return; // couldn't resolve the URI
}
if (mDrawableWidth == 0 || mDrawableHeight == 0) {
return; // nothing to draw (empty bounds)
}
if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {
mDrawable.draw(canvas);
} else {
final int saveCount = canvas.getSaveCount();
canvas.save();
canvas.translate(mPaddingLeft, mPaddingTop);
if (mDrawMatrix != null) {
canvas.concat(mDrawMatrix);
}
mDrawable.draw(canvas);
canvas.restoreToCount(saveCount);
}
}
ImageView的onDraw()方法首先对mDrawable进行检查,mDrawable是否为空、宽高是否具有意义。在设置了mDrawMatrix的一系列方法后,onDraw()在绘制前会根据mDrawMatrix设置的值对图片资源进行相应的变换操作。无论Drawable缩放与否在满足mDrawable != null && mDrawableWidth != 0 && mDrawableHeight != 0的绘制条件下,最终都是通过mDrawable.draw(canvas)方法对mDrawable进行绘制。这里的mDrawable为TransitionDrawable对象实例。
public void draw(Canvas canvas) {
boolean done = true;
switch (mTransitionState) {
case TRANSITION_STARTING:
mStartTimeMillis = SystemClock.uptimeMillis();
done = false;
mTransitionState = TRANSITION_RUNNING;
break;
case TRANSITION_RUNNING:
if (mStartTimeMillis >= 0) {
float normalized = (float)(SystemClock.uptimeMillis() - mStartTimeMillis) / mDuration;
done = normalized >= 1.0f;
normalized = Math.min(normalized, 1.0f);
mAlpha = (int) (mFrom + (mTo - mFrom) * normalized);
}
break;
}
final int alpha = mAlpha;
final boolean crossFade = mCrossFade;
final ChildDrawable[] array = mLayerState.mChildren;
if (done) {
if (!crossFade || alpha == 0) {
array[0].mDrawable.draw(canvas);
}
if (alpha == 0xFF) {
array[1].mDrawable.draw(canvas);
}
return;
}
Drawable d;
d = array[0].mDrawable;
if (crossFade) {
d.setAlpha(255 - alpha);
}
d.draw(canvas);
if (crossFade) {
d.setAlpha(0xFF);
}
if (alpha > 0) {
d = array[1].mDrawable;
d.setAlpha(alpha);
d.draw(canvas);
d.setAlpha(0xFF);
}
if (!done) {
invalidateSelf();
}
}
调用adapter.setDrawable(transitionDrawable),进行视图重绘的流程中,实质还是调用TransitionDrawable.draw()方法完成自身绘制。TransitionDrawable.draw()方法的逻辑也是简单明了,d.setAlpha(alpha)和d.draw(canvas),在不同阶段为两张Drawable设置对应透明度以此实现两个Drawable之间的淡入淡出效果。Drawable.draw()本身是个抽象方法,绘制具体逻辑由其子类实现。这里的drawable分别为GlideBitmapDrawable和BitmapDrawable对象实例,具体为什么,在了解Drawable源码之后你就清楚了。(GlideBitmapDrawable则隐藏在Glide网络请求部分的源码之中)
Drawable源码分析
Drawable是一个用于处理各种可绘制资源的抽象类。我们使用Drawable最常见的情况就是将获取到的资源绘制到屏幕上。
Drawable实例可能存在以下多种形式
-
Bitmap:最简单的Drawable形式,PNG或者JPEG图片。 -
.9图:PNG的一个扩展,可以支持设置其如何填充内容,如何被拉伸。 -
Shape:包含简单的绘制指令,用于替代Bitmap,某些情况下对大小调整有更好表现。 -
Layers:一个复合的Drawable,按照层级进行绘制,单个Drawable实例绘制于其下层Drawable实例集合之上。 -
States:一个复合的Drawable,根据它的state选择一个Drawable集合。 -
Levels:一个复合的Drawable,根据它的level选择一个Drawable集合。 -
Scale:一个复合的Drawable和单个Drawable实例构成,它的总体尺寸由它的当前level值决定。
Drawable常见使用步骤
- 通过
Resource获取Drawable实例 - 将获取的
Drawable实例当做背景设置给View或者作为ImageView的src进行显示:
getResources().getDrawable()方法经过多次辗转的调用最终会通过ResourcesImpl实例的drawableFromBitmap()方法加载资源图片。
private static Drawable drawableFromBitmap(Resources res, Bitmap bm, byte[] np, Rect pad, Rect layoutBounds, String srcName) {
if (np != null) {
return new NinePatchDrawable(res, bm, np, pad, layoutBounds, srcName);
}
return new BitmapDrawable(res, bm);
}
drawableFromBitmap()方法对于.9图返回1个NinePatchDrawable实例,普通图片返回1个BitmapDrawable实例。
public boolean animate(T current, ViewAdapter adapter) {
Drawable previous = adapter.getCurrentDrawable();
if (previous != null) {
TransitionDrawable transitionDrawable = new TransitionDrawable(new Drawable[] { previous, current });
return true;
}
}
TransitionDrawable在实例化时传入的previous、current分别来自adapter.getCurrentDrawable()方法和animate()方法传入的current参数。adapter.getCurrentDrawable()方法内部通过view.getDrawable()来获取与ImageView关联的Drawable实例。这个Drawable则来自getPlaceholderDrawable()方法。
private Drawable getPlaceholderDrawable() {
if (placeholderDrawable == null && placeholderResourceId > 0) {
placeholderDrawable = context.getResources().getDrawable(placeholderResourceId);
}
return placeholderDrawable;
}
getPlaceholderDrawable()方法,通过Resource实例加载占位符placeHolder图片资源。没错,getPlaceholderDrawable()方法就像👆Drawable常见使用步骤,最终会通过ResourcesImpl.drawableFromBitmap()加载资源图片,返回1个BitmapDrawable实例。通过target.onLoadStarted(getPlaceholderDrawable())将获取的Drawable实例当做背景设置给ImageView。
LayerDrawable&Callback
Android LayerDrawable and Drawable.Callback
文章中之所以提到Callback是因为为ImageView设置占位符时ImageView的Callback指向当前的Drawable。
当使用占位符作为子图层创建LayerDrawable实例时
LayerDrawable(@NonNull Drawable[] layers, @Nullable LayerState state) {
this(state, null);
final int length = layers.length;
final ChildDrawable[] r = new ChildDrawable[length];
for (int i = 0; i < length; i++) {
r[i] = new ChildDrawable(mLayerState.mDensity);
r[i].mDrawable = layers[i];
layers[i].setCallback(this);
mLayerState.mChildrenChangingConfigurations |= layers[i].getChangingConfigurations();
}
}
BitmapDrawable&GlideBitmapDrawable
Drawable.draw()本身是个抽象方法,绘制具体逻辑由其子类实现。TransitionDrawable.draw() 方法最终还是通过d.setAlpha(alpha)和d.draw(canvas),在不同阶段为Drawable设置对应透明度以此实现两个Drawable之间的淡入淡出效果。
public void setAlpha(int alpha) {
final int oldAlpha = mBitmapState.mPaint.getAlpha();
if (alpha != oldAlpha) {
mBitmapState.mPaint.setAlpha(alpha);
invalidateSelf();
}
}
在LayerDrawable中,每层视图(Drawable)都会将LayerDrawable注册为它的Drawable.Callback。从而允许Drawable在需要重绘自己的时候能够告知LayerDrawable重绘它。LayerDrawable最终调用到View中的invalidateDrawable()方法,之后就会按照我们前面分析的流程执行重绘逻辑,以此改变视图背景。
public void draw(Canvas canvas) {
final Bitmap bitmap = mBitmapState.mBitmap;
if (bitmap == null) {
return;
}
final BitmapState state = mBitmapState;
final Paint paint = state.mPaint;
final Shader shader = paint.getShader();
if (shader == null) {
if (needMirroring) {
canvas.save();
canvas.translate(mDstRect.right - mDstRect.left, 0);
canvas.scale(-1.0f, 1.0f);
}
canvas.drawBitmap(bitmap, null, mDstRect, paint);
} else {
updateShaderMatrix(bitmap, paint, shader, needMirroring);
canvas.drawRect(mDstRect, paint);
}
}
BitmapDrawable.draw()方法先是对画笔Paint和画布Canvas进行相应设置,接着将Drawable实例中的Bitmap绘制到View实例关联的画布上。
GlideBitmapDrawable绘制逻辑与BitmapDrawable基本相同,便不再赘述。
至此,我们已经了解了TransitionDrawable实现渐变的原理,及与相关知识(Android视图重绘机制、Drawable及其实现类源码)。是不是觉得头昏脑胀,不知所云~~~ 不不不,应该是虽然学到了很多知识,并没有发现问题的存在~~~
还记得吗,这个坑只会在使用CircleImageView的情况下出现,对于ImageView,即便是在使用占位符和默认动画的情况下Glide仍可以正常工作。那CircleImageView与ImageView两者之间又是存在何种差异导致了问题的出现?
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mDrawable == null) {
return; // couldn't resolve the URI
}
if (mDrawableWidth == 0 || mDrawableHeight == 0) {
return; // nothing to draw (empty bounds)
}
if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {
mDrawable.draw(canvas);
} else {
final int saveCount = canvas.getSaveCount();
canvas.save();
canvas.translate(mPaddingLeft, mPaddingTop);
if (mDrawMatrix != null) {
canvas.concat(mDrawMatrix);
}
mDrawable.draw(canvas);
canvas.restoreToCount(saveCount);
}
}
ImageView.onDraw()方法通过mDrawable.draw()方法对mDrawable进行绘制并实现淡入淡出效果。👆有对ImageView.onDraw()方法更为详细的分析过程,没错就是Android视图重绘机制哪里~
protected void onDraw(Canvas canvas) {
if (mDisableCircularTransformation) {
onDraw(canvas);
return;
}
if (mBitmap == null) {
return;
}
if (mFillColor != Color.TRANSPARENT) {
canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mFillPaint);
}
canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint);
if (mBorderWidth > 0) {
canvas.drawCircle(mBorderRect.centerX(), mBorderRect.centerY(), mBorderRadius, mBorderPaint);
}
}
通过上面的分析,我们终于可以得出只显示占位符placeHolder的原因。Glide在网络中获取图片并经过解码、逻辑操作(包括对图片的压缩,甚至还有旋转、圆角等逻辑处理)之后,会最终回调到GlideDrawableImageViewTarget.onResourceReady()方法来设置ImageView。一番处理之后,会调用父类ImageViewTarget的onResourceReady()方法。
onResourceReady()方法通过对glideAnimation进行判空,对glideAnimation.animate()返回值进行分析,来决定是否执行setResource()方法。根据animationFactory引用工厂对象的不同,onResourceReady()方法可能传入DrawableCrossFadeViewAnimation或NoAnimation对象实例。
DrawableCrossFadeViewAnimation.animate()方法内部先是获取先前通过placeHolder()设置占位符占位符previous。如previous不为空,则通过TransitionDrawable设置动画并添加图片至ImageView。否则通过defaultAnimation展示图片。
CircleImageView.onDraw()方法仅是通过canvas.drawCircle()方法将Drawable实例中的 Bitmap经过裁剪之后绘制到CircleImageView实例关联的画布上。没错,与ImageView.onDraw()方法相比缺少了对mDrawable.draw()方法的调用,而mDrawable.draw()方法则会不断调用invalidateSelf()方法获取其关联的View进行重复的视图重绘操作,通过不断调用TransitionDrawable.draw()方法,设置两个Drawable透明度从而实现渐入渐出效果的实现。
除上述我个人的结论之外,网上也有一些分析的文章说:根本原因就是你的placeholder图片和你要加载显示的图片宽高比不一样,而Android的TransitionDrawable无法很好地处理不同宽高比的过渡问题,这的确是个Bug,是Android的也是Glide的。 文章实在是太长了😂,对此就先挖个坑,回头再填~
至此Glide遇坑记之Glide与CircleImageView的分析就告一段落~~
以上分析均是个人见解。如果错误或疏忽请及时指出,O(∩_∩)O谢谢!