Glide遇坑記之決戰(zhàn)
上述三種解決方法都避免了與真正BOSS---TransitionDrawable
的正面交鋒。TransitionDrawable
繼承自LayerDrawable
岛心。LayerDrawable
是一個特殊的Drawable
瓦宜,它內(nèi)部保持著一個Drawable數(shù)組
降宅,其中每一個Drawable
都是視圖中的一層擂橘。通過多個Drawable
的疊加、漸變二驰、旋轉(zhuǎn)等組合顯示出與單一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
對象實例,接著調(diào)用setCrossFadeEnabled()
和startTransition()
方法對TransitionDrawable
的成員變量進行設置娇掏。并在startTransition()
方法的最后一行呕寝,調(diào)用invalidateSelf()
方法嘗試進行視圖重繪。
public void invalidateSelf() {
final Callback callback = getCallback();
if (callback != null) {
callback.invalidateDrawable(this);
}
}
invalidateSelf()
方法先是獲取TransitionDrawable
注冊的Callback
實例婴梧,如果無則返回null
下梢。通過Callback
接口,一個Drawable
實例可以回調(diào)其客戶端來執(zhí)行動畫塞蹭。為了動畫可以被執(zhí)行孽江,所有的客戶端都應該支持這個Callback
接口。 View
類正是實現(xiàn)了Callback
接口番电,所以callback.invalidateDrawable()
其實調(diào)用的就是View
中的invalidateDrawable()
方法岗屏。 但此時TransitionDrawable
實例未注冊任何Callback
接口,invalidateSelf()
方法直接返回漱办。
緊接著animation()
中執(zhí)行adapter.setDrawable()
方法这刷,方法內(nèi)部通過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)
方法內(nèi)部先是通過updateDrawable(drawable)
更新成員變量mDrawable
娩井,同時修改其屬性暇屋。 接著調(diào)用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
關(guān)聯(lián)的Drawable
實例mDrawable
的Callback
置空率碾。接著把傳進來的Drawable
對象賦給成員變量mDrawable
。如果參數(shù)d
不為空的話屋彪,那么設置d
的Callback
為ImageView
實例所宰。通過d.getIntrinsicWidth()
獲取drawable
的width
賦值全局變量mDrawableWidth
。
Android視圖重繪機制
View
的源碼中會有數(shù)個invalidate()
方法的重載和一個invalidateDrawable()
方法畜挥,最終都是通過invalidateInternal()
方法來實現(xiàn)視圖重制仔粥。
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);
}
}
}
在這個方法中首先會調(diào)用skipInvalidate()
方法來判斷當前 View
是否需要重繪,判斷的邏輯也比較簡單,如果View
是不可見的且沒有執(zhí)行任何動畫躯泰,就認為不需要重繪了谭羔。之后會進行透明度的判斷,并給View
添加一些標記位麦向,然后調(diào)用ViewParent的invalidateChild()
方法瘟裸,這里的ViewParent
其實就是當前視圖的父視圖,因此會調(diào)用到ViewGroup
的invalidateChild()
方法中诵竭。省略若干循環(huán)調(diào)用话告。最終經(jīng)過多次輾轉(zhuǎn)的調(diào)用,最終會走到視圖繪制的入口方法performTraversals()
中卵慰,然后重新執(zhí)行繪制流程沙郭。
invalidate()
方法雖然最終會調(diào)用到performTraversals()
方法中,但這時measure
和layout
流程是不會重新執(zhí)行的裳朋,因為視圖沒有強制重新測量的標志位病线,而且大小也沒有發(fā)生過變化,所以這時只有draw
流程可以得到執(zhí)行鲤嫡。而如果你希望視圖的繪制流程可以完完整整地重新走一遍送挑,就不能使用invalidate()
方法,而應該調(diào)用requestLayout()
泛范。
繪制流程始于ViewRootImpl
的performDraw()
方法让虐,里面又調(diào)用了ViewRootImpl
的draw()
方法紊撕,經(jīng)過一系列調(diào)用罢荡,然后實例化Canvas
對象,鎖定該canvas
的區(qū)域并進行一系列的屬性賦值对扶,最后調(diào)用了mView.draw(canvas)
方法区赵,這個mView
就是DecorView
,也就是說從DecorView
開始繪制浪南。由于ViewGroup
沒有重寫draw
方法笼才,因此所有的View
都是通過調(diào)用View
的draw()
方法實現(xiàn)繪制。
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()
- 繪制內(nèi)容 --
onDraw()
- 繪制孩子 --
dispatchDraw()
- 繪制裝飾 --
onDrawScrollbars()
View
中的onDraw()
方法是一個空實現(xiàn),不同的View有著不同的內(nèi)容絮记,這需要我們自己去實現(xiàn)摔踱,即在自定義View
中重寫該方法來實現(xiàn)。
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()
在繪制前會根據(jù)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();
}
}
調(diào)用adapter.setDrawable(transitionDrawable)
猪勇,進行視圖重繪的流程中,實質(zhì)還是調(diào)用TransitionDrawable.draw()
方法完成自身繪制颠蕴。TransitionDrawable.draw()
方法的邏輯也是簡單明了埠对,d.setAlpha(alpha)
和d.draw(canvas)
,在不同階段為兩張Drawable
設置對應透明度以此實現(xiàn)兩個Drawable
之間的淡入淡出效果裁替。Drawable.draw()
本身是個抽象方法项玛,繪制具體邏輯由其子類實現(xiàn)。這里的drawable
分別為GlideBitmapDrawable
和BitmapDrawable
對象實例弱判,具體為什么襟沮,在了解Drawable
源碼之后你就清楚了。(GlideBitmapDrawable
則隱藏在Glide
網(wǎng)絡請求部分的源碼之中)
Drawable源碼分析
Drawable
是一個用于處理各種可繪制資源的抽象類昌腰。我們使用Drawable
最常見的情況就是將獲取到的資源繪制到屏幕上开伏。
Drawable
實例可能存在以下多種形式
-
Bitmap
:最簡單的Drawable
形式,PNG
或者JPEG
圖片遭商。 -
.9圖
:PNG
的一個擴展固灵,可以支持設置其如何填充內(nèi)容,如何被拉伸劫流。 -
Shape
:包含簡單的繪制指令巫玻,用于替代Bitmap
,某些情況下對大小調(diào)整有更好表現(xiàn)祠汇。 -
Layers
:一個復合的Drawable
仍秤,按照層級進行繪制,單個Drawable
實例繪制于其下層Drawable
實例集合之上可很。 -
States
:一個復合的Drawable
诗力,根據(jù)它的state
選擇一個Drawable
集合。 -
Levels
:一個復合的Drawable
我抠,根據(jù)它的level
選擇一個Drawable
集合苇本。 -
Scale
:一個復合的Drawable
和單個Drawable
實例構(gòu)成,它的總體尺寸由它的當前level
值決定菜拓。
Drawable
常見使用步驟
- 通過
Resource
獲取Drawable
實例 - 將獲取的
Drawable
實例當做背景設置給View
或者作為ImageView
的src
進行顯示:
getResources().getDrawable()
方法經(jīng)過多次輾轉(zhuǎn)的調(diào)用最終會通過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
參數(shù)。adapter.getCurrentDrawable()
方法內(nèi)部通過view.getDrawable()
來獲取與ImageView
關(guān)聯(lián)的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。
當使用占位符作為子圖層創(chuàng)建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()
本身是個抽象方法嚼蚀,繪制具體邏輯由其子類實現(xiàn)禁灼。TransitionDrawable.draw()
方法最終還是通過d.setAlpha(alpha)
和d.draw(canvas)
,在不同階段為Drawable
設置對應透明度以此實現(xiàn)兩個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
最終調(diào)用到View
中的invalidateDrawable()
方法守谓,之后就會按照我們前面分析的流程執(zhí)行重繪邏輯,以此改變視圖背景您单。
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
實例關(guān)聯(lián)的畫布上。
GlideBitmapDrawable
繪制邏輯與BitmapDrawable
基本相同虐秦,便不再贅述平酿。
至此,我們已經(jīng)了解了TransitionDrawable
實現(xiàn)漸變的原理羡疗,及與相關(guān)知識(Android視圖重繪機制染服、Drawable及其實現(xiàn)類源碼)。是不是覺得頭昏腦脹叨恨,不知所云~~~ 不不不,應該是雖然學到了很多知識挖垛,并沒有發(fā)現(xiàn)問題的存在~~~
還記得嗎痒钝,這個坑只會在使用CircleImageView
的情況下出現(xiàn),對于ImageView
痢毒,即便是在使用占位符和默認動畫的情況下Glide
仍可以正常工作送矩。那CircleImageView
與ImageView
兩者之間又是存在何種差異導致了問題的出現(xiàn)?
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
進行繪制并實現(xiàn)淡入淡出效果哪替。??有對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在網(wǎng)絡中獲取圖片并經(jīng)過解碼晌块、邏輯操作(包括對圖片的壓縮爱沟,甚至還有旋轉(zhuǎn)、圓角等邏輯處理)之后匆背,會最終回調(diào)到GlideDrawableImageViewTarget.onResourceReady()
方法來設置ImageView呼伸。一番處理之后,會調(diào)用父類ImageViewTarget
的onResourceReady()
方法钝尸。
onResourceReady()
方法通過對glideAnimation
進行判空括享,對glideAnimation.animate()
返回值進行分析,來決定是否執(zhí)行setResource()
方法珍促。根據(jù)animationFactory
引用工廠對象的不同铃辖,onResourceReady()
方法可能傳入DrawableCrossFadeViewAnimation
或NoAnimation
對象實例。
DrawableCrossFadeViewAnimation.animate()
方法內(nèi)部先是獲取先前通過placeHolder()
設置占位符占位符previous
猪叙。如previous
不為空澳叉,則通過TransitionDrawable
設置動畫并添加圖片至ImageView
。否則通過defaultAnimation
展示圖片沐悦。
CircleImageView.onDraw()
方法僅是通過canvas.drawCircle()
方法將Drawable
實例中的 Bitmap
經(jīng)過裁剪之后繪制到CircleImageView
實例關(guān)聯(lián)的畫布上成洗。沒錯,與ImageView.onDraw()
方法相比缺少了對mDrawable.draw()
方法的調(diào)用藏否,而mDrawable.draw()
方法則會不斷調(diào)用invalidateSelf()
方法獲取其關(guān)聯(lián)的View進行重復的視圖重繪操作瓶殃,通過不斷調(diào)用TransitionDrawable.draw()
方法,設置兩個Drawable透明度從而實現(xiàn)漸入漸出效果的實現(xiàn)副签。
除上述我個人的結(jié)論之外遥椿,網(wǎng)上也有一些分析的文章說:根本原因就是你的placeholder圖片和你要加載顯示的圖片寬高比不一樣,而Android的TransitionDrawable無法很好地處理不同寬高比的過渡問題淆储,這的確是個Bug冠场,是Android的也是Glide的。
文章實在是太長了??本砰,對此就先挖個坑碴裙,回頭再填~
至此Glide遇坑記之Glide與CircleImageView的分析就告一段落~~
以上分析均是個人見解。如果錯誤或疏忽請及時指出点额,O(∩_∩)O謝謝舔株!