美文网首页开卷有益Android开发经验谈Android开发
详解glide中crossfade引发的默认图变形

详解glide中crossfade引发的默认图变形

作者: 虎七 | 来源:发表于2018-03-02 21:15 被阅读229次


    最近因为版权问题,要把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

    相关文章

      网友评论

        本文标题:详解glide中crossfade引发的默认图变形

        本文链接:https://www.haomeiwen.com/subject/exaaxftx.html