當前位置:
首頁 > 最新 > 詳解glide中crossfade引發的默認圖變形

詳解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:一葉谷 |