詳解glide中crossfade引發的默認圖變形
引
最近因為版權問題,要把fresco替換成glide(3.5)。
可是在執行crossfade後,本來正常的默認圖(place holder)發生了拉伸形變。
Glide.with(context)
.load(url)
.fitCenter()
.placeholder(R.drawable.glide_placeholder)
.crossFade(2000)
.into(imageView);
百思不得其解,於是看了一遍源碼,找到了原因。
crossFade流程
crassFade使用了一個工廠類,如下:
public DrawableRequestBuilder crossFade(int duration) {
super.animate(new DrawableCrossFadeFactory(duration));
return this;
}
該工廠類的構造類中有個參數,參數使用了一個默認的Animation工廠,如下:
public DrawableCrossFadeFactory(int duration) {
this(new ViewAnimationFactory(new DefaultAnimationFactory()), duration);
}
默認工廠類生成Animation的build方法,主要是構建了一個AlphaAnimation,就是最終呈現出來的淡入淡出效果,如下:
private static class DefaultAnimationFactory implements ViewAnimation.AnimationFactory {
@Override
public Animation build() {
AlphaAnimation animation = new AlphaAnimation(0f, 1f);
animation.setDuration(DEFAULT_DURATION_MS /2);
return animation;
}
}
再看下DrawableCrossFadeFactory是如何生成GlideAnimation的;它使用上面的默認工廠構造了一個defaultAnimation,然後又用了一個DrawableCrossFadeViewAnimation將defaultAnimation包裝起來生成新的GlideAnimation(這裡使用了裝飾器模式),如下:
@Override
public GlideAnimation build(boolean isFromMemoryCache, boolean isFirstResource) {
if (isFromMemoryCache) {
return NoAnimation.get();
}
if (animation ==null) {
GlideAnimation defaultAnimation = animationFactory.build(false, isFirstResource);
animation = new DrawableCrossFadeViewAnimation(defaultAnimation, duration);
}
return animation;
}
繼續看下DrawableCrossFadeViewAnimation是如何執行動畫的;
它先判斷adapter當前有沒有Drawable存在,如果沒有的話,就使用之前構造好的默認動畫,就是前面提到的包含AlphaAnimation的動畫執行器。
如果有的話,就將已經存在和當前需要動畫的兩個Drawable作為參數,構造出一個TransitionDrawable,然後將這個TransitionDrawable設置為要顯示的Drawable;這裡的previous顯然是有的,因為使用Glide時設置了placeholder,這裡的previous拿到的就是place holder的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;
}
}
目前為止沒看出什麼問題,我們繼續看這個TransitionDrawable,讀讀它的源碼。
TransitionDrawable源碼
源碼地址:
http://androidxref.com/8.0.0_r4/xref/frameworks/base/graphics/java/android/graphics/drawable/
上面提到的兩個參數(previous, current),最後是以Drawable[]形式構造TransitionDrawable的,如下:
它調用了重載方法,而這個重載方法只是調用了父類LayerDrawable的構造方法。
在LayerDrawable的構造方法中,傳入的layers參數,被循環遍歷,每個Drawable元素構造出了一個ChildDrawable對象,這個對象的mDrawable屬性記錄了最開始傳入的Drawable參數;這些ChildDrawable形成一個數組,保存在狀態變數的mChildren屬性。
看看DrawableLayer是怎麼繪製的,它遍歷上面的ChildDrawable列表,對每個Drawable對象進行繪製;這裡不對Drawable設置區域範圍,所以遇到的默認圖形變問題肯定不在這裡。
那麼我們繼續看下DrawableLayer是如何進行邊界更新的,如下:
最終調用到updateLayerBoundsInternal方法中,如下:
它總體還是對ChildDrawable列表進行了遍歷;對每個ChildDrawable的處理,先是獲取到Drawable對象,然後拿到對應的inset信息,這個inset信息是Drawable的邊界信息。(開始嗅到問題的味道了...)
接著先是在重新設置了臨時變數container,這是一個區域對象Rect,設置的方法是在給定參數bounds(外部賦予LayerDrawable對象的區域)的基礎上,做inset偏移;
然後獲取到d的原始尺寸和記錄的尺寸,從這些信息中獲取到一個gravity值;
然後就是最關鍵的,通過gravity,記錄尺寸信息來計算出最終的區域,給Drawable設定區域。
這裡的幾個信息點: inset,原始尺寸(intrinsicW, intrinsicH),記錄尺寸(r.mWidth, r.mHeight),gravity。
如果記錄尺寸無效(
默認圖發生了形變,意味著這個區域的尺寸不再是(intrinsicW, intrinsicH),按照這段的代碼邏輯,原因很可能是:記錄尺寸(r.mWidth, r.mHeight)被設置了,或者gravity不對,又或者inset影響了。
下面代碼是重構gravity的;如果width(這裡傳入參數是記錄尺寸)無效,那麼gravity會填充整個橫向區域,height則是豎向區域;這段邏輯好可怕,如果設置的記錄尺寸(和Drawable原始尺寸)有效,那麼就用記錄尺寸,否則就填充整個視圖?
繼續看看記錄尺寸的屬性都有哪些地方修改:構造函數里默認無效(-1),別處解析attr時會設置,可是測試代碼里沒有用設置該屬性。
還有一處就是對外介面了:
這個介面必須API 23以上的才支持;而且上面看到的調用流程中,沒有調用該api的地方。
到這裡為止,默認圖的形變原因基本可以定論了:
placeholder在crossfade過程中,和load好的圖片同處於一個TransitionDrawable里;
它沒有被設置任何外部尺寸信息,gravity也沒有初始化,所以在計算尺寸時gravity被加入了填充信息(FILL_XXX),導致它的區域是和inset過的區域一致的;
而它又沒有被設置任何inset信息(邊界信息),自然和整個視圖的尺寸保持了一致,當它原本小於視圖尺寸的情況下自然而然就被拉伸了。
那麼怎麼解決這個問題了?看來我們的救命稻草,只能著手於inset信息了:
這個API,不用擔心像上面提到的setLayerSize,setLayerGravity等新的api問題了。
還有一種方式,就是把Drawable本身的邊界信息改變,也是一樣的效果。
glide官方給出的方案就是這樣的,我們來看看吧。
官方的解決方案
下面的代碼是一個ViewAdapter子類PaddingViewAdapter;
它以原有adapter和給定的尺寸為參數做成一個包裝類,類似代理模式;在獲取當前Drawable的時候,它先是把這個Drawable做了InsetDrawable的包裝,這個包裝對象的尺寸能將Drawable居中顯示在給定的尺寸中。
import android.os.Build.*;
import android.view.View;
class PaddingViewAdapter implements ViewAdapter{
private final ViewAdapterre alAdapter;
private final int targetWidth;
private final int targetHeight;
public PaddingViewAdapter(ViewAdapter adapter,int targetWidth,int targetHeight) {
this.realAdapter = adapter;
this.targetWidth = targetWidth;
this.targetHeight = targetHeight;
}
@Override
public View getView() {
return realAdapter.getView();
}
@Override
public Drawable getCurrentDrawable() {
Drawable drawable = realAdapter.getCurrentDrawable();
if (drawable != null) {
int padX = Math.max(0, targetWidth-drawable.getIntrinsicWidth())/2;
int padY=Math.max(0, targetHeight-drawable.getIntrinsicHeight())/2;
if(padX>0||padY>0) {
drawable=new InsetDrawable(drawable, padX, padY, padX, padY);
}
}
return drawable;
}
@Override
public void setDrawable(Drawable drawable) {
if(VERSION.SDK_INT>=VERSION_CODES.M && drawable instanceof TransitionDrawable) {
//For some reason padding is taken into account differently on M than before in LayerDrawable
//PaddingMode was introduced in 21 and gravity in 23, I think NO_GRAVITY default may play
//a role in this, but didn"t have time to dig deeper than this.
((TransitionDrawable)drawable).setPaddingMode(TransitionDrawable.PADDING_MODE_STACK);
}
realAdapter.setDrawable(drawable);
}
}
下面的代碼是一個GlideAnimation子類PaddingAnimation,也是一個代理類;
它在執行動畫的時候,首先拿到了當前要做動畫對象的尺寸,然後使用上面的代理類PaddingViewAdapter,針對這個尺寸對Drawable做預處理;
我們回憶一下最初看到的crossFade流程,是不是就是通過adapter.getCurrentDrawable()拿到previous的?那麼placeholder通過這個代理類,就被預先處理成了帶正確inset的Drawable,這樣就不會形變了。
class PaddingAnimation implements GlideAnimation {
private final GlideAnimation realAnimation;
public PaddingAnimation(GlideAnimation animation) {
this.realAnimation=animation;
}
@Override
public boolean animate(T current, final View Adapteradapter) {
int width = current.getIntrinsicWidth();
int height = current.getIntrinsicHeight();
return realAnimation.animate(current, newPaddingViewAdapter(adapter, width, height));
}
}
下面的代碼是更改後的代碼,使用後默認圖不再發生形變了;
這裡只是把into(imageView),更改為into(new GlideDrawableImageViewTarget(imageView),同時在onResourceReady的重寫中使用了代理類PaddingAnimation。
Glide.with(context)
.load(url)
.fitCenter()
.placeholder(R.drawable.glide_placeholder)
.crossFade(2000)
.into(new GlideDrawableImageViewTarget(imageView) {
@Override
public void onResourceReady(GlideDrawable resource, GlideAnimation animation) { super.onResourceReady(resource, new PaddingAnimation(animation));
}
})
參考
問題討論:
https://stackoverflow.com/questions/32235413/glide-load-drawable-but-dont-scale-placeholder
官方補丁代碼:
https://github.com/TWiStErRob/glide-support/tree/master/src/glide3/java/com/bumptech/glide/supportapp/stackoverflow/_32235413_crossfade_placeholder
TAG:一葉谷 |