Hey!
My use case is that I would like to load a thumbnail image, then a high res image into the same view. This is how my code looks like:
DrawableRequestBuilder thumbnailRequest = Glide.with(getContext())
.load(thumbnailSource.getUrl())
.centerCrop()
.placeholder(R.color.placeholder);
Glide.with(getContext())
.load(highResSource.getUrl())
.thumbnail(thumbnailRequest)
.centerCrop()
.placeholder(R.drawable.placeholder)
.into(imageView);
The problem is that if the the thumbnail is loaded first (not from memory cache) and the high res image is loaded second (not from memory) the high res image is being animated causing a flickering due to the cross fade animation from placeholder to the high res image.
Setting the high res request to not having an animation or placeholder is not an option because the high res request may complete before the thumbnail request.
I tried to work around with the request listener interface but in the onResourceReady(...) callback of the high res request I'm not able to determine if the thumbnail request has already been completed or not. If I would be able to determine if the thumbnail request has been completed I can set the high res image without animation on the imageView. In case of the thumbnail request is still being executed I can start the animation to set the high res image on the imageView.
Note: If the thumbnail image and the high res image are cached in the memory, I don't see the flickering. This makes sense, as both images are retrieved immediately from the memory cache and being set without cross fade animation on the imageView.
I had the same question, take a look at #919
Basically use animate(...).
I don't really see what can cause that flickering, can you describe (or video) the flicker in more detail?
I think it is possible that the placeholder on the thumbnail request is interfering, try to remove that.
In the listener you can decide if the thumb was shown with the isFirstResource argument.
Thank you both for the fast replies. I've done screen recordings for both cases:
cross fade: https://youtu.be/eTO9i8VI_IY
fade in as suggested by @jush : https://youtu.be/bXQkmAL-fpY
By switching to fade in animation the flickering appears now on the first set if the image.
@TWiStErRob What is the behavior if I remove the placeholder on the thumbnail request and keep the placeholder on the high res request? Will the placeholder still be visible if the thumbnail request is finished first?
EDIT: Tried removing the placeholder on the thumbnail request and the result is the same.
The fade-in version on the video looks better, but I agree there's a little too much brightness at the start of the crossfading.... and it's because there's no crossfade. The placeholder is visible, then the placeholder is cleared so the view becomes transparent making the brighter background visible and then the image opacity is increased to 100%. Try it without placeholder or a placeholder that's just a border to have better visuals.
Placeholder is set only if none of the resources are found in memory cache, then both requests are fired. The one that finished first crossfades from placeholder. If the first one was the thumbnail then it should crossfade from thumb to HQ image to reduce that little jump that you see in the fade-in video. So you only need one placeholder, I'm not sure it even matters if it's defined on the thumbnail request, but worth a try removing it; especially if it's not used.
Try to increase the animation duration so you see what's going on: crossFade(3000) for your original code. Also try to set different durations for thumb and main, maybe that reveals something (for example only one of the animations is used). You can also use DelayTransformation in #919 to have more control over timings.
It's true, the fade-in version looks better than the cross fade version but on the device the flickering is still very strong.
I now tried the following configuration (please not changed placeholders and durations)...
DrawableRequestBuilder thumbnailRequest = Glide.with(getContext())
.load(thumbnailSource.getUrl())
.centerCrop()
.placeholder(new ColorDrawable(Color.GREEN))
.crossFade(2000);
Glide.with(getContext())
.load(highResSource.getUrl())
.thumbnail(thumbnailRequest)
.centerCrop()
.placeholder(new ColorDrawable(Color.RED))
.crossFade(4000)
.into(imageView);
...and received an interesting result: https://youtu.be/RDldeSM2YP8
1) As you said, the placeholder on the thumbnail request does not matter.
2) The cross fade animation on the high res request causes the flicker by applying a white shimmer on top of the thumbnail image which disappears with the animation. I assume the cross fade animation uses not 0 as the starting alpha for the entering image. What do you think @TWiStErRob ?
It looks like two crossfades. 1-3s and 3-7s which look like your 2000+4000. What happens if you replace them crossFade(int) calls with dontAnimate() one at a time? You may be able to get a better effect. It's possible that putting a transition drawable inside a transition drawable causes this issue. That reminds me: are you using the stock ImageView?
I hope you can find a suitable workaround, I'll try to take a look next week.
I'm using a custom view which extends from ImageView and also measure and layouts itself. When I changed back to a ImageView the behavior didn't change.
So, based on your solution to switch off one animation I was able to produce the effect I was aiming for. Here is the code I use with some additional comments about it.
DrawableRequestBuilder thumbnailRequest = Glide.with(getContext())
.load(thumbnailSource.getUrl())
.centerCrop()
//.placeholder(R.color.placeholder); /* Glide does not use placeholder in thumbnail requests - use high res request for setting the placeholder */
//.crossFade() /* keep default cross fade animation */
.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) {
// do not set the thumbnail image if the high res request has been completed
if (!isFirstResource) {
return true;
}
// run default cross fade animation
return false;
}
});
Glide.with(getContext())
.load(highResSource.getUrl())
.thumbnail(thumbnailRequest)
.centerCrop()
.placeholder(R.drawable.placeholder) /* this placeholder is used for both, thumbnail and high res requests */
.dontAnimate() /* disable animation - if we need an animation, we do it in the onResourceReady() callback */
.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) {
// high res image is coming directly from the memory cache -> set this image without animation
if (isFromMemoryCache) {
return false;
}
// high res image has been completed after the thumbnail request -> set this image without animation
if (!isFirstResource) {
return false;
}
// at this point the image coming not from the memory and the placeholder is shown -> cross fade this image
new DrawableCrossFadeFactory<>().build(isFromMemoryCache, isFirstResource).animate(resource, (GlideAnimation.ViewAdapter) target);
return true;
}
})
.into(imageView);
Awesome, thanks for sharing.
You can probably cache the anim to reduce allocations. It's always build(false, true).
Ok, so I tracked the issue down to TransitionDrawable: when the cross-fade happens between the two images (thumb and full) the background becomes visible for a moment (depending on duration this could be more prevalent). I tried setting the ImageView background to red and black and the white flashing color seen in the video changed as well, with black the image became darker instead of lighter.
All this happens because TransitionDrawable draws the both images with low opacity, so at 25% of the animation the first one is drawn with 0.75 alpha on top of what's already there on the screen (including background) and the second one is drawn with 0.25 alpha on top of what's already there on the screen (including first image). Since both let the background through it's mixed in: at this point the background is visible with about 0.25 * 0.75 = 0.18 alpha. The highest it goes is 0.25 at 50%. A real cross-fade would calculate all pixels as (1-progress)*src + progress*dst and not mixing the background.
Weirdly, when I checked how the TransitionDrawable is created in DrawableCrossFadeViewAnimation it calls setCrossFadeEnabled(true) whose doc says:
Enables or disables the cross fade of the drawables. When cross fade is disabled, the first drawable is always drawn opaque. With cross fade enabled, the first drawable is drawn with the opposite alpha of the second drawable. Cross fade is disabled by default.
I set the value to false in the debugger and the cross-fade looked like as I would have expected. @sjudd I think we should not call this method or explicitly call it with false to document this issue.
Here's a repro-able case with workaround commented out:
imageView.setBackgroundColor(Color.RED);
Glide
.with(getContext())
.load(url)
.skipMemoryCache(true)
.centerCrop()
.thumbnail(Glide
.with(getContext())
.load(url)
.skipMemoryCache(true)
.centerCrop()
.sizeMultiplier(.1f)
.crossFade(0) // dontAnimate doesn't work here, see GRB.buildRequestRecursive
)
.placeholder(new ColorDrawable(Color.BLUE))
.crossFade(5000)
.into(new GlideDrawableImageViewTarget(imageView) {
@Override public void setDrawable(Drawable drawable) {
if (drawable instanceof TransitionDrawable) {
//((TransitionDrawable)drawable).setCrossFadeEnabled(false); // fix
}
super.setDrawable(drawable);
}
});
With true (not calling the method) the image becomes red-ish while cross-fading. With false the image cross-fades nicely. Try it with an image of a green scenery for best repro.
@sjudd what do you think about setting crossFadeEnabled to false? The documentation is a little confusing, I think it looks much more like a crossFade when it's disabled on the TransitionDrawable.
Good idea, done here: https://github.com/bumptech/glide/commit/4aa684fa9a303bb614536a565fa1b7506253ae88
I think this is fixed then in v4 and there's a fairly straightforward (instanceof) workaround for v3.
@sjudd would it be too big a change to just flip the argument in 3.8.0? I can't really think of a scenario where true is a good option because it bleeds through.
Hmm, I just found out that if the argument is false, then the transparent parts of the top image allow the bottom image's parts to come through after the cross-fade has completed. I still think it's a good idea to default to false, because it looks better for standard images. In v4 it's easy to flip this switch via DrawableCrossFadeFactory.Builder in case transparency needs to be accounted for.