Glide遇坑记---Glide与CircleImageView
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谢谢!