26

Is there a way to use Glide to assign a placeholder but keep this image to its original scale? I have an ImageView with variable size (depending on the incoming image) which I set before calling Glide.with().load().into() and I want to use a placeholder for it but don't want the placeholder to be resized to the size of the ImageView, I want it to keep its original size.

So far I couldn't find a way to do this.

Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
Fernando
  • 362
  • 1
  • 3
  • 9

3 Answers3

21

There's a known Glide issue of placeholders distorting loaded images and vice versa. However I think you are not affected by that.

It sounds like what you want is to show the placeholder drawable with scaleType="center" and you want the loaded image to be scaleType="fitCenter". Here's how you express that in Glide. Add android:scaleType="center" in the XML and use the following Glide load line:

Glide.with(...).load(...).placeholder(R.drawable....).fitCenter().into(imageView);

The trick is that the placeholder is set via setImageDrawable() so the ImageView will just display it as usual, but you tell Glide to use the FitCenter explicitly which will fit the loaded image nicely within the ImageView's laid out size via a Transformation and then set it via setImageDrawable(). Since the fitted image is a perfect fit, center will just draw the image covering the whole area of the view.

You can also use .centerCrop() the same way.

If something is wrong you can try .asBitmap() and .dontAnimate(), they help most of the time in one way or another.

TWiStErRob
  • 44,762
  • 26
  • 170
  • 254
  • 1
    Problem with this solution: While it initially appears to work, notice that during the transition when Glide fades the placeholder to the real image, the placeholder image is scaled up to fitCenter. This is a big problem since it's this fade transition that I'm trying to preserve here. So dontAnimate() defeats the purpose of trying to do this. Any ideas? – Greg Ennis Apr 29 '16 at 14:38
14

Re: Gregs comment: I gave it another shot (with almost a year of extra XP) and came up with this:

<ImageView android:scaleType="center" ... />

similar to my other solution and the following animation wrapper:

...
    .fitCenter()
    .animate(new PaddingAnimationFactory<>(new DrawableCrossFadeFactory<GlideDrawable>(2000)))
    .into(imageView)
;

class PaddingAnimationFactory<T extends Drawable> implements GlideAnimationFactory<T> {
    private final DrawableCrossFadeFactory<T> realFactory;
    @Override public GlideAnimation<T> build(boolean isFromMemoryCache, boolean isFirstResource) {
        return new PaddingAnimation<>(realFactory.build(isFromMemoryCache, isFirstResource));
    }
}

class PaddingAnimation<T extends Drawable> implements GlideAnimation<T> {
    private final GlideAnimation<? super T> realAnimation;
    @Override public boolean animate(T current, final ViewAdapter adapter) {
        int width = current.getIntrinsicWidth();
        int height = current.getIntrinsicHeight();
        return realAnimation.animate(current, new PaddingViewAdapter(adapter, width, height));
    }
}

class PaddingViewAdapter implements ViewAdapter {
    @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);
    }
}

Trivial parts of the implementations are omitted, each class's constructor initializes the fields from arguments. Full code available on GitHub in TWiStErRob/glide-support.

crossFaded placeholder


If you're stuck on an older version of Glide (before 3.8.0), the same effect can be achieved by:

.fitCenter()
.placeholder(R.drawable.glide_placeholder)
.crossFade(2000)
.into(new GlideDrawableImageViewTarget(imageView) {
    @Override public void onResourceReady(GlideDrawable resource,
            GlideAnimation<? super GlideDrawable> animation) {
        super.onResourceReady(resource, new PaddingAnimation<>(animation));
    }
})

Note how the two solutions require the same amount of classes, but the post-3.8.0 solution has better separation of concerns and it can be cached in a variable to prevent allocations.

Community
  • 1
  • 1
TWiStErRob
  • 44,762
  • 26
  • 170
  • 254
  • This solution is working for us on android 5.x and below. On 6 and N however it looks like the PaddingAnimation isn't behaving as expected and we're once again seeing the behavior placeholder grows during the transition into the centerCrop view. We'll be doing research, but if anyone else has seen the glitch on 6 & N I'd love to hear any solutions found. – Jake Hall May 27 '16 at 15:27
  • @JakeHall can you please share a screen cap of that? Does InsetDrawable look as you expect if used in a standalone ImageView? Is it possible that your placeholder has inconsistent sizes between those versions (i.e. newer version -> higher dpi -> different asset). I think the math and `scaleType=center` only works if the placeholder is smaller than the view/full loaded resource; notice that padXY may become negative for example. – TWiStErRob May 27 '16 at 17:37
  • @TWiStErRob here you go, one from our app where you see the placeholder animate larger and one from the demo app where you can see the placeholder pop to the larger size. This only happens on 23+, on lower versions of android the animations work as designed. https://www.dropbox.com/s/3mzco9coiokyoa1/transition_issue.mp4?dl=0 https://www.dropbox.com/s/hgzfv3njfg0egq3/transition_2.mov?dl=0 – Jake Hall May 31 '16 at 17:10
  • @TWiStErRob thanks! update works great, you're the best. – Jake Hall May 31 '16 at 21:03
  • 1
    @TWiStErRob, you're true hero! I updated the code in Kotlin and Glide 4+, where "Animation" is renamed to Transition. https://gist.github.com/sevar83/03fc54baa15967f69bf58b6229526946 – WindRider Nov 24 '17 at 13:55
3

I do it like mentioned below:

The idea is to set the scale type to as required by the place holder initially & attach listener to change the scale type again to as required by the downloaded image after the image is downloaded.

//ivBuilderLogo = Target ImageView
//Set the scale type to as required by your place holder
//ScaleType.CENTER_INSIDE will maintain aspect ration and fit the placeholder inside the image view
holder.ivBuilderLogo.setScaleType(ImageView.ScaleType.CENTER_INSIDE); 

//AnimationDrawable is required when you are using transition drawables
//You can directly send resource id to glide if your placeholder is static
//However if you are using GIFs, it is better to create a transition drawable in xml 
//& use it as shown in this example
AnimationDrawable animationDrawable;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
     animationDrawable=(AnimationDrawable)context.getDrawable(R.drawable.anim_image_placeholder);
else
     animationDrawable=(AnimationDrawable)context.getResources().getDrawable(R.drawable.anim_image_placeholder);
animationDrawable.start();

Glide.with(context).load(logo_url)
                   .placeholder(animationDrawable)
                   .listener(new RequestListener<String, GlideDrawable>() {
                        @Override
                        public boolean onException(Exception e, String model, Target<GlideDrawable> target, boolean isFirstResource)
                        {
                           return false;
                        }

                        //This is invoked when your image is downloaded and is ready 
                        //to be loaded to the image view
                        @Override
                        public boolean onResourceReady(GlideDrawable resource, String model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource)
                        {   
                        //This is used to remove the placeholder image from your ImageView 
                        //and load the downloaded image with desired scale-type(FIT_XY in this case)
                        //Changing the scale type from 'CENTER_INSIDE' to 'FIT_XY' 
                        //will stretch the placeholder for a (very) short duration,
                        //till the downloaded image is loaded
                        //setImageResource(0) removes the placeholder from the image-view 
                        //before setting the scale type to FIT_XY and ensures that the UX 
                        //is not spoiled, even for a (very) short duration
                            holder.ivBuilderLogo.setImageResource(0);
                            holder.ivBuilderLogo.setScaleType(ImageView.ScaleType.FIT_XY);
                            return false;
                        }
                    })
                    .into( holder.ivBuilderLogo);

My transition drawable (R.drawable.anim_image_placeholder) :

(not required if using a static image)

<?xml version="1.0" encoding="utf-8"?>
<animation-list
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">
    <item android:drawable="@drawable/loading_frame1" android:duration="100" />
    <!--<item android:drawable="@drawable/loading_frame2" android:duration="100" />-->
    <item android:drawable="@drawable/loading_frame3" android:duration="100" />
    <!--<item android:drawable="@drawable/loading_frame4" android:duration="100" />-->
    <item android:drawable="@drawable/loading_frame5" android:duration="100" />
    <!--<item android:drawable="@drawable/loading_frame6" android:duration="100" />-->
    <item android:drawable="@drawable/loading_frame7" android:duration="100" />
    <!--<item android:drawable="@drawable/loading_frame8" android:duration="100" />-->
    <item android:drawable="@drawable/loading_frame9" android:duration="100" />
    <!--<item android:drawable="@drawable/loading_frame10" android:duration="100" />-->
</animation-list>
Swas_99
  • 2,350
  • 1
  • 15
  • 19
  • 1
    Your solution helped me – Edijae Crusar Mar 20 '17 at 08:32
  • It feels like you could achieve the same behavior if you just did `.animate(android.R.anim.fade_in)` without any `scaleType` hacks, however I didn't test this theory. I think this simple animation will work, because `setImageResource` essentially clears the `ImageView`, so the `.crossFade()` is already disabled, and it falls back to fade-in (see `DrawableCrossFadeFactory.DefaultAnimationFactory` and `DrawableCrossFadeViewAnimation.animate`). – TWiStErRob Mar 21 '17 at 23:44