Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace image on an imageview without showing white background? #527

Closed
minimalviking opened this issue Jul 7, 2015 · 30 comments
Closed
Labels

Comments

@minimalviking
Copy link

I have one ImageView and a button which, when pressed, loads new image (from disk, not from network) into the same ImageView. Unfortunately it causes the first image to disappear for a second (showing white background) and then it loads the new image. How can I avoid that?
I tried:

Glide.with(this).load(photoToLoad.getPath()).dontAnimate().dontTransform().into(imageView);

But the "blinking" still exists.
The only solution I can think of is using two ImageViews with load listeners and showing/hiding them once the new image is loaded.

Is there any better way to do this?

@sjudd
Copy link
Collaborator

sjudd commented Jul 7, 2015

Basically this isn't supported directly, but you can do it with either two different Views or, if you're careful, with one View and two different targets. I believe #132 has some more information.

@TWiStErRob
Copy link
Collaborator

TWiStErRob commented Jul 7, 2015

I can think of two ways. Both theoretical.

  1. .preload() the second image right after the first one finished (listener): get the just-finished target size and use that for preload size. It should match what the second normal load will be and will use memory cache which means immediate load.
  2. Start the second load like this: Glide....placeholder(iv.getDrawable()).into(iv);
    Edit: This is not safe, your app will crash if you use it, see 2-3 comments below.
  3. Edit: after much time I found a better way: Replace image on an imageview without showing white background? #527 (comment), it can even crossfade GIFs.

@minimalviking
Copy link
Author

minimalviking commented Jul 7, 2015

@TWiStErRob's second solution works perfectly, here's the code for anyone wondering:

 Glide.with(this).load(photoToLoad.getPath()).placeholder(imageView.getDrawable()).into(imageView);

Edit: This is not safe, your app will crash if you use it, see 2-3 comments below.

@sjudd
Copy link
Collaborator

sjudd commented Jul 7, 2015

@minimalviking Unfortunately that's not safe. As soon as you start your second load, the Bitmap/Drawable from the first load may be re-used or recycled. Most of the time it will work fine, but sometimes you will get unlucky and the wrong image will be displayed briefly, or your application will throw because it tries to draw a recycled Bitmap.

@TWiStErRob
Copy link
Collaborator

@minimalviking Sam's right, please for the sake of your users, don't go with that solution.
Here's how my first suggestion would go down. I know it's a little more convoluted, but it's much much safer and highly likely works. You can hide its uglyness in a factory method or class.

final Context context = this; // or Fragment context = this;
final ImageView imageView = ...;
 Glide
        .with(context)
        .load(photoToLoad.getPath())
        .listener(new RequestListener<String, GlideDrawable>() {
            @Override public boolean onException(
                    Exception e, String model, Target<GlideDrawable> target, boolean isFirstResource) {
                return false;
            }
            @Override public boolean onResourceReady(
                    GlideDrawable resource, String model, Target<GlideDrawable> target,
                    boolean isFromMemoryCache, boolean isFirstResource) {  // execution order: 2
                target.getSize(new SizeReadyCallback() {
                    @Override public void onSizeReady(int width, int height) {  // execution order: 3
                        Glide.with(context).load(photoToLoad2.getPath()).preload(width, height);
                    }
                });
                return false;
            }
        })
        .into(imageView); // execution order: 1
findViewById(R.id.button).setOnClickListener(new OnClickListener() {
    @Override public void onClick(View v) { // execution order: 4
        Glide.with(context).load(photoToLoad2.getPath()).into(imageView);
    }
});

I also thought of another way while discussing the unsafeness with Sam. It mimics the second solution in a safer way, but it's still a big hack as those Drawables may have other parameters that affects the visuals.

Glide.with(context).load(photoToLoad.getPath()).asBitmap().into(imageView);
findViewById(R.id.button).setOnClickListener(new OnClickListener() {
    @Override public void onClick(View v) {
        Drawable continuityPlaceholder = copy(imageView.getDrawable());
        Glide.with(context).load(photoToLoad2.getPath()).placeholder(continuityPlaceholder).into(imageView);
    }
    private Drawable copy(Drawable drawable) { // notice the .asBitmap() on the first load
        Bitmap bitmap = null;
        if (drawable instanceof BitmapDrawable) {
            bitmap = ((BitmapDrawable)drawable).getBitmap();
        } else if (drawable instanceof GlideBitmapDrawable) {
            bitmap = ((GlideBitmapDrawable)drawable).getBitmap();
        }
        if (bitmap != null) {
            bitmap = bitmap.copy(bitmap.getConfig(), bitmap.isMutable());
            return new BitmapDrawable(getResources(), bitmap);
        } else {
            return null;
        }
    }
});

@minimalviking
Copy link
Author

@TWiStErRob thanks for such a detailed response!
I'll go with your second solution as it seems cleaner to me.

@TWiStErRob
Copy link
Collaborator

@minimalviking I found a much simpler and no-hack way:

Glide
    .with(context)
    .load(imageUrl)
    .thumbnail(Glide // this thumbnail request has to have the same RESULT cache key
            .with(context) // as the outer request, which usually simply means
            .load(oldImage) // same size/transformation(e.g. centerCrop)/format(e.g. asBitmap)
            .fitCenter() // have to be explicit here to match outer load exactly
    )
    .listener(new RequestListener<String, GlideDrawable>() {
        @Override public boolean onResourceReady(GlideDrawable resource, String model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {
            if (isFirstResource) {
                return false; // thumbnail was not shown, do as usual
            }
            return new DrawableCrossFadeFactory<Drawable>(/* customize animation here */)
                    .build(false, false) // force crossFade() even if coming from memory cache
                    .animate(resource, (ViewAdapter)target);
        }
    })
    //.fitCenter() // this is implicitly added when .into() is called if there's no scaleType in xml or the value is fitCenter there
    .into(imageView)
;
oldImage = imageUrl;

How does it work? you ask. When an image is loaded into a target (ImageView) it's an active resource. When a new load is started (into) the target is cleared, pushing the active resource in it into memory cache. Thumbnail has a higher priority than the load it's attached to. Starting a load with thumbnail finds the just-memory-cached because the thumbnail's params match the outer load's. This happens immediately with memory cached resources which means that the clear that put it into memory cache and the load that puts it back into target are both run immediately one after the other on the main thread which leaves no time for the ImageView to re-draw itself empty, hence no white flash.

@mikhail709
Copy link

mikhail709 commented Nov 10, 2016

I found this way to solve the problem:

Glide.with(AppAdapter.context())
                     .load(<your_url_here>)
                    .centerCrop()
                    .crossFade()
                    .placeholder(holder.mLogo.getDrawable() != null ? holder.mLogo.getDrawable() : null)
                    .error(R.drawable.error_logo)
                    .into(holder.mLogo);

@TWiStErRob
Copy link
Collaborator

@mikhail709 this was addressed before in this very issue and it is not safe (may crash).
Please read the 4 comments starting with #527 (comment)

@wezley98
Copy link

@TWiStErRob is there an updated version of your solution for v4? GlideDrawable will not resolve in the new version.

@TWiStErRob
Copy link
Collaborator

@wezley98 read the compile errors, it lists the expected types.

@0mega
Copy link

0mega commented Jul 24, 2017

Had a use case to refresh webcam images from a static URI. The problem for static URI is that is you want the image to refresh you have to either skip the cache or make up your own key for the resource. Following @TWiStErRob approach with a little adaptation works perfectly. I'll leave here if anyone is interested in future.
First you should do an initial fetch and store the timestamp (used as cache key):

oldPhotoKey = new ObjectKey(String.valueOf(System.currentTimeMillis()));
        GlideApp.with(getActivity())
                .load(static_uri)
                .signature(oldPhotoKey)
                .into(imageView);

Then the refresh code, maybe called from a CoutDownTimer:

ObjectKey newPhotoKey = new ObjectKey(String.valueOf(System.currentTimeMillis()));
GlideApp.with(getActivity())
                .load(static_uri)
                .signature(newPhotoKey)
                .thumbnail(GlideApp.with(getActivity())
                        .load(static_uri)
                        .signature(oldPhotoKey)
                        .fitCenter()
                )
                .into(imageView);
oldPhotoKey = newPhotoKey;

What happens here is that we assign the key for the cache when we store the image and than reuse that previous image for thumbnail just like mentioned in the suggested implementation by @TWiStErRob, but instead of only using URI as caching key we append a timestamp to it. In my case I'm fetching images every four seconds and the memory looks stable.

@DanteAndroid
Copy link

DanteAndroid commented Sep 6, 2019

@TWiStErRob Hi, it doesn't load anything in my case:

        GlideApp
            .with(this)
            .load(image.url)
            .thumbnail(
                Glide // this thumbnail request has to have the same RESULT cache key
                    .with(this) // as the outer request, which usually simply means
                    .load(image.url) // same size/transformation(e.g. centerCrop)/format(e.g. asBitmap)
                    .fitCenter() // have to be explicit here to match outer load exactly
            )
            //.fitCenter() // this is implicitly added when .into() is called if there's no scaleType in xml or the value is fitCenter there
            .into(detailImage)

---- Turns out it's the TouchImageView's problem -----

@taichushouwang
Copy link

@TWiStErRob Hi, it doesn't load anything in my case:

        GlideApp
            .with(this)
            .load(image.url)
            .thumbnail(
                Glide // this thumbnail request has to have the same RESULT cache key
                    .with(this) // as the outer request, which usually simply means
                    .load(image.url) // same size/transformation(e.g. centerCrop)/format(e.g. asBitmap)
                    .fitCenter() // have to be explicit here to match outer load exactly
            )
            //.fitCenter() // this is implicitly added when .into() is called if there's no scaleType in xml or the value is fitCenter there
            .into(detailImage)

---- Turns out it's the TouchImageView's problem -----

@DanteAndroid If you use Glide 4.0, you should add signature()

@prathameshmm02
Copy link

prathameshmm02 commented Dec 17, 2021

The below code uses the same logic as given by @TWiStErRob while it works perfectly fine, for error in loads, the thumbnails of previous loads don't get replaced by error drawable, it seems like it's the default behavior of Glide. So, I would like to know if this can be overridden.

GlideApp.with(this)
            .load(songCoverUri)
            .error(DEFAULT_SONG_IMAGE)
            .thumbnail(lastRequest)
            .fitCenter().also {
                lastRequest = it.clone()
               // This is an extension function for crossfade listener as above 
                it.crossfadeListener()
                .into(binding.albumCover)
            }

@Vic-wkx
Copy link

Vic-wkx commented Dec 13, 2022

I met the same issue when using Glide to load bitmap, after testing the above solutions, only the solution of @minimalviking works in my case. And I add an extra try-catch here since it's unsafe per @sjudd 's comments. It's ugly but I didn't find a better way, note it here in case someone needs it.

fun load(context: Context, bitmap: Bitmap, appCompatImageView: AppCompatImageView) {
    try {
        Log.d("Glide", "Load bitmap: $bitmap")
        Glide.with(context)
            .load(bitmap)
            .override(bitmap.width, bitmap.height)
            // There's a blink issue without this placeholder, this is a workaround but unfortunately it's unsafe per https://github.com/bumptech/glide/issues/527
            // But I didn't find a better way to avoid the blink, I choose to leave it here since it works well during my test and add a fallback workaround in catch block when it happens
            .placeholder(appCompatImageView.drawable)
            .into(appCompatImageView)
    } catch (t: Throwable) {
        Log.e("Glide", "Caught throwable when loading bitmap $bitmap, exception: $t, fallback to the solution without placeholder")
        Glide.with(context)
            .load(bitmap)
            .override(bitmap.width, bitmap.height)
            .into(appCompatImageView)
    }
}

@TWiStErRob
Copy link
Collaborator

@wkxjc how about #527 (comment)?

@Vic-wkx

This comment was marked as outdated.

@TWiStErRob
Copy link
Collaborator

You deviated a lot from the intent of the code, see the trailing comments. Every line has a specific job in the solution. e.g. .build(dataSource, isFirstResource) -> .build(DataSource.XXX, false) and match the transformation on the thumbnail (you must have an implicit fitCenter / centerCrop from the ImageView.scaleType). If the thumbnail load doesn't hit the memory cache, it won't work.

@Vic-wkx
Copy link

Vic-wkx commented Dec 14, 2022

@TWiStErRob Thanks for reply, may I ask if this version is correct?

var oldImage: Bitmap? = null
fun load(context: Context, bitmap: Bitmap, appCompatImageView: AppCompatImageView) {
    Glide.with(context)
        .load(bitmap)
        .thumbnail(
            Glide.with(context).load(oldImage).fitCenter()
        )
        .listener(object : RequestListener<Drawable> {
            override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
                return false
            }
            override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean
                if (isFirstResource) return false
                return DrawableCrossFadeFactory.Builder()
                    .build()
                    .build(DataSource.MEMORY_CACHE, false)
                    .transition(resource, target as Transition.ViewAdapter)
            }
        })
        .fitCenter()
        .into(appCompatImageView)
    oldImage = bitmap
}

@Vic-wkx
Copy link

Vic-wkx commented Dec 14, 2022

@TWiStErRob
The blink issue of switching two pictures disappears with the above version, but it still exists when there are some animations of ImageView, which is more closer to the issue I met. I think it's because the oldImage cannot keep up to date all the time.

I just created a repository to show the issue I met: https://github.com/wkxjc/TestGlide , could you help me find a better way to solve it since I also don't like the ugly try-catch version, any help is appreciated, thanks.

Maybe I should create a new issue to show this scenario, but I think it still fits the title of this issue, so I leave it here for now.

@TWiStErRob

This comment was marked as outdated.

@Vic-wkx

This comment was marked as outdated.

@TWiStErRob
Copy link
Collaborator

Wait a minute, I just realized that it's the Bitmap input. I've been suspicious of that for a while, but couldn't get a grip on it. I do now. Bitmap is a random object, a new one is allocated every time BitmapFactory.decodeResource is returned and it doesn't have an equals. This means that the keys don't match and memory-caching is not possible, but the loading of a Bitmap is usually really fast, so mostly ok, but sometimes not.

I played around a bit and I noticed that the flashing happens after the anim is completed. This is because the size of the view changes and that's part of the cache key.

Here's what you need to look out for: make sure that the thumbnail's key is the same as the previous normal load's key to hit memory cache.

  • this includes using a model that has a good equals!

    If you *need* to load `Bitmap`s, use a model that wraps it
    Glide.get(applicationContext).registry.append(BModel::class.java, Bitmap::class.java, BModelLoader.Factory())
    
    class BModel(val bitmap: Bitmap, val id: String) {
        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (other !is BModel) return false
            if (id != other.id) return false
            return true
        }
        override fun hashCode(): Int =
            id.hashCode()
    }
    class BModelLoader : ModelLoader<BModel, Bitmap> {
        override fun handles(model: BModel): Boolean = true
        override fun buildLoadData(model: BModel, width: Int, height: Int, options: Options): LoadData<Bitmap> =
            LoadData(ObjectKey(model.id), BModelFetcher(model))
    
        private class BModelFetcher(private val model: BModel) : DataFetcher<Bitmap> {
            override fun getDataClass(): Class<Bitmap> = Bitmap::class.java
            override fun getDataSource(): DataSource = LOCAL
            override fun loadData(priority: Priority, callback: DataCallback<in Bitmap>) {
                callback.onDataReady(model.bitmap)
            }
            override fun cleanup() {}
            override fun cancel() {}
        }
    
        class Factory : ModelLoaderFactory<BModel, Bitmap> {
            override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<BModel, Bitmap> =
                BModelLoader()
            override fun teardown() {}
        }
    }
  • this includes matching the view size
    (you can use override to force a specific size for the thumb)

To debug memory caching do the following:

  • adb shell setprop log.tag.Glide VERBOSE
  • adb shell setprop log.tag.Engine VERBOSE
  • kill the app and restart or redeploy it, then check LogCat output when you interact.
  • Optionally, put a breakpoint in Engine.loadFromMemory,
    this method must return a non-null resource (usually from cache) in all cases to ensure no blinkling. Looking at the EngineKey instance and comparing it with the memory cache you can see why it's not matching.
    • active resource: one that's already on screen visible somewhere
    • cache: in this case is a resource that was visible on screen and it was cleared because a new load started

@Vic-wkx
Copy link

Vic-wkx commented Dec 16, 2022

Thanks a lot, I was wondering why the blink issue only exists on bitmap but not on file resources, your answer makes sense to me.
I have to load bitmap since our business needs to draw something else on the graphic, I'll try your solution later.

@TWiStErRob
Copy link
Collaborator

You can do the drawing from a model object (data class) and register some stuff into Glide to make it render and cache in the background. The loader/fetcher/transcoder are really powerful in customisation.

@XBigTK13X
Copy link

I have come back to this thread (and others) many times from searching on the web to stop image flickering when trying to load another image. At the end of the day, none of the suggestions here worked on the latest version of Glide.

When I ignore Glide entirely and directly handle decoding the images myself, the flicker is gone. Just leaving this here in case someone else comes along pulling their hair out.

// In a Util class
// Based on the Android docs: https://developer.android.com/topic/performance/graphics/load-bitmap#java
    public static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
        // Raw height and width of image
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {

            final int halfHeight = height / 2;
            final int halfWidth = width / 2;

            // Calculate the largest inSampleSize value that is a power of 2 and keeps both
            // height and width larger than the requested height and width.
            while ((halfHeight / inSampleSize) >= reqHeight
                    && (halfWidth / inSampleSize) >= reqWidth) {
                inSampleSize *= 2;
            }
        }

        return inSampleSize;
    }

    private static Integer maxImageHeight;
    private static Integer maxImageWidth;

    public static Bitmap subsample(Uri resourceUri) {
        if(maxImageHeight == null){
            DisplayMetrics displayMetrics = new DisplayMetrics();
            MainActivity.getInstance().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
            maxImageHeight = displayMetrics.heightPixels;
            maxImageWidth = displayMetrics.widthPixels;
        }

        try {
            final BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            final InputStream measureStream = MainActivity.getInstance().getContentResolver().openInputStream(resourceUri);
            BitmapFactory.decodeStream(measureStream, null, options);
            options.inSampleSize = calculateInSampleSize(options, maxImageWidth, maxImageHeight);
            options.inJustDecodeBounds = false;
            final InputStream imageStream = MainActivity.getInstance().getContentResolver().openInputStream(resourceUri);
            return BitmapFactory.decodeStream(imageStream, null, options);
        }
// In a fragment
if (!imageLocked) {
                  imageLocked = true;
                  currentPageImage.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                     @Override
                     public boolean onPreDraw() {
                        imageLocked = false;
                        resetZoom();
                        currentPage = page;
                        currentPageImage.getViewTreeObserver().removeOnPreDrawListener(this);
                        return false;
                     }
                  });
                  try {
                     Bitmap bitmap = Util.fullImage(page);
                     currentPageImage.setImageBitmap(bitmap);
                  }
                  catch(Exception fullFail){
                     try {
                        Bitmap subsampled = Util.subsample(page);
                        currentPageImage.setImageBitmap(subsampled);
                     }
                     catch(Exception subsampleFail){
                        Util.log(TAG, "Unable to load full and subsample image. Page "+bookView.CurrentPageIndex);
                        Util.error(TAG,subsampleFail);
                        Util.toast("Unable to load page.");
                     }
                  }
               }
            }

@TWiStErRob
Copy link
Collaborator

@XBigTK13X if you do your image loading (I/O) on the main thread, of course it doesn't flicker, because it has no time to do so. Your frame times should suffer, and scrolling should be choppy. If you're not experiencing these, you need to try a lower-end device to test on.

@XBigTK13X
Copy link

I'm sure there is room for improvement in my snippet. Glide works fine for lists of thumbnails, but I couldn't after years get it to work in the following case.

There is one fragment that has an image view. When the user swipes, either go to the next image or previous and load the content into the same image view.

I tried all the suggestions above, there is ALWAYS noticable flicker after swiping. At least on the devices I target, my current implementation has zero flicker. If there's a new approach with the latest version of glide I'm all ears.

Just wanted to put it out there for other people that land on this issue from Google.

@TWiStErRob
Copy link
Collaborator

@XBigTK13X Sounds interesting, would you mind putting a small independent example together with Glide that includes the flicker? (i.e. a repro) I will give it a go.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests