详解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。

如果记录尺寸无效(< 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

官方补丁代码:

https://github.com/TWiStErRob/glide-support/tree/master/src/glide3/java/com/bumptech/glide/supportapp/stackoverflow/_32235413_crossfade_placeholder

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容