引
最近因为版权问题,要把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。
如果记录尺寸无效(< 0),那么会使用原始尺寸;通过这个尺寸和gravity(布局方式),来重新调整前面计算过一次的区域(inset),最终形成一个区域。
默认图发生了形变,意味着这个区域的尺寸不再是(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.graphics.drawable.*;
import android.os.Build.*;
import android.view.View;
import com.bumptech.glide.request.animation.GlideAnimation.ViewAdapter;
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,这样就不会形变了。
import android.graphics.drawable.Drawable;
import com.bumptech.glide.request.animation.GlideAnimation;
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
官方补丁代码:
网友评论