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

Add support for loading placeholder image from cache via URL #27

Open
lfielke opened this issue Aug 14, 2019 · 19 comments
Open

Add support for loading placeholder image from cache via URL #27

lfielke opened this issue Aug 14, 2019 · 19 comments
Labels
enhancement New feature or request

Comments

@lfielke
Copy link

lfielke commented Aug 14, 2019

First up, congrats on shipping!

A feature request for consideration. Sometimes an app will show a list of small image thumbnails, which are clickable to open the image full screen. The thumbnails are physically small (in dimension and file size) so the list of thumbnails loads quickly. The full images (loaded from a different URL to the thumbnail) are much bigger, so might take a few seconds to load from the network.

Currently, Coil supports specifying a placeholder Drawable using the placeholder method. It would be good if that was extended to allow passing in the URL of the thumbnail, which presumably would still be in the memory cache because it was being displayed on the previous screen.

The desired effect would look like the image sampling mentioned on the docs website. So the low-res thumbnail would be shown until the full-res image has loaded. The API difference being the first low-res image has come from a different URL, rather than the same one.

I've used "URL" above to keep the wording simple, but I mean all the different "sources" in general that are parameters to the ImageLoader.load methods (String, Uri, HttpUrl, etc), or the loadAny method that uses a Mapper.

@lfielke lfielke added the enhancement New feature or request label Aug 14, 2019
@colinrtwhite
Copy link
Member

Thanks! I've thought about adding a placeholder(data) method, however have shied away from it so far. Adding it would create more work on the main thread, which could reduce performance. Also, it could result in some unexpected behaviour where you expect something to be in the memory cache and it isn't.

@lfielke
Copy link
Author

lfielke commented Aug 14, 2019

Good points. It sounds like what I'm talking about isn't really a "placeholder", because Coil assumes that placeholders are available straight away, synchronously on the main thread. Have I understood that correctly?

Thinking about this overnight, could you do two loads one-after the other, the first one loading the thumbnail URL but specifying a read only cache policies?

Something like this:

imageView.load(thumbUrl) {
  diskCachePolicy = READ_ONLY
  networkCachePolicy = DISABLED
  placeholder = R.drawable.image_placeholder
  crossfade(true) // crossfade from image_placeholder to thumbnail if it loads from disk
}
imageView.load(fullSizeUrl) {
  crossfade(true) // crossfade from thumbnail to full image
}

These requests could technically race, which could be a problem in general with this approach. I'm not sure what the desired behaviour even is when you have two loads for the same target. In this example, you want the first load to proceed, but only until the second load completes. But loading into a RecyclerView you want the opposite, where the first load should be cancelled as soon as the view is reused and the second load starts.

@colinrtwhite
Copy link
Member

Ah I see what you mean. This would add a decent chunk of complexity to RealImageLoader so I'm not sure I'd want to add this in. However, you could create the same effect with get:

coroutineScope.launch(Dispatchers.Main.immediate) {
    val thumb = async { imageLoader.get(thumbUrl) }
    val fullSize = async { imageLoader.get(fullSizeUrl) }
    fullSize.invokeOnCompletion { thumb.cancel() }
    
    // There's probably a better way to do this.
    val thumbDrawable = try {
        thumb.await()
    } catch (e: CancellationException) {
        null
    }
    imageView.setDrawable(thumbDrawable)

    val fullSizeDrawable = fullSize.await()
    imageView.setDrawable(fullSizeDrawable)
}

Replace imageLoader with Coil if you use the singleton. To figure out whether or not to crossfade, you can add a Request.Listener and check the returned DataSource in onSuccess.

Disclaimer: the above code is completely untested and unoptimized.

@Pitel
Copy link

Pitel commented Nov 7, 2019

I'd like something similar. I'd like to load image from cache (almost) immediately, but also check the network in the background for change.

Our image server supports ETag, and 304 Unchaged response. But because the URL is still the same, Coil will store the image in memory, and never asks the network again, so the user does not see the updated image. So I have the memory cache disabled, but then, when scrolling long list up and down, the images are always blank first and load later, which is distracting.

@elye
Copy link

elye commented Dec 20, 2020

Just to check if this is already supported? In glide, I can do as below (placeholder -> initial-fast-load -> full-load)

image

With codes as below

val requestOption = RequestOptions()
        .placeholder(R.drawable.placeholder).centerCrop()
Glide.with(this).load(fullImageUrl)
       .transition(DrawableTransitionOptions.withCrossFade())
       .thumbnail(Glide.with(this)
            .load(fastLoadUrl)
            .apply(requestOption))
       .apply(requestOption)
       .into(my_image_view)

Refer to this article https://medium.com/mobile-app-development-publication/glide-image-loader-the-basic-798db220bb44

Hopes Coil can do the same.

@kasem-sm
Copy link

Still waiting for this :(

@frel
Copy link

frel commented Aug 16, 2021

I would also like to see this feature implemented. I've created a library named AirBrush that uses this feature in Glide, but I'm planning to support Coil due to JetPack Compose. I can hack around it and use TransitionDrawable but I would prefer to use the API :)

@Leeonardoo
Copy link

This feature is the only thing keeping me using Glide for bigger projects. Would love to see a way to do that using Coil

@rakshitsoni02
Copy link

can anyone suggest any workaround for the coil to achieve the same?

@kasem-sm
Copy link

kasem-sm commented Feb 6, 2022

can anyone suggest any workaround for the coil to achieve the same?

Here you go. It is used by me in my prod app and no issues/complaints till now. If anyone finds a better solution, please let me know.

@0n4li
Copy link

0n4li commented Feb 21, 2022

can anyone suggest any workaround for the coil to achieve the same?

Slight changes to kasem-sm's reply:

fun ImageView.loadWithQuality(
    highQuality: String,
    lowQuality: String,
    placeholderRes: Int? = null,
    errorRes: Int? = null
) {

    placeholderRes?.let {
        setImageResource(placeholderRes)
    }

    var isHighQualityLoaded = false

    class CallbackImageViewTarget(val callback: () -> Boolean) : ImageViewTarget(this) {
        override fun onSuccess(result: Drawable) {
            if (callback()) {
                super.onSuccess(result)
            }
        }
    }

    val lowQualityRequest = ImageRequest.Builder(context).apply {
        data(lowQuality)
        target(CallbackImageViewTarget(
            callback = {
                return@CallbackImageViewTarget !isHighQualityLoaded
            }
        ))
    }.build()

    val lowQualityLoader = context.imageLoader.enqueue(lowQualityRequest)

    val highQualityRequest = ImageRequest.Builder(context).apply {
        data(highQuality)
        errorRes?.let { error(errorRes) }
        target(CallbackImageViewTarget(
            callback = {
                isHighQualityLoaded = true
                if (!lowQualityLoader.isDisposed) {
                    lowQualityLoader.dispose()
                }
                return@CallbackImageViewTarget true
            }
        ))
    }.build()

    context.imageLoader.enqueue(highQualityRequest)

}
@kasem-sm
Copy link

@0n4li Hey, thanks for improving that code 😊

@0n4li
Copy link

0n4li commented Feb 22, 2022

Probably the below code works better. Need to test:

fun ImageView.loadWithQuality(
    highQuality: String,
    lowQuality: String,
    placeholderRes: Int? = null,
    errorRes: Int? = null
) {
    load(lowQuality) {
        placeholderRes?.let { placeholder(placeholderRes) }
        listener(onSuccess = { _, _ ->
            load(highQuality) {
                placeholder(drawable) // If there was a way to not clear existing image before loading, this would not be required
                errorRes?.let { error(errorRes) }
            }
        })
    }
}
@sechiyo97
Copy link

sechiyo97 commented Sep 21, 2022

Is there any plan to support this feature?

This feature is the only thing keeping me using Glide for bigger projects. Would love to see a way to do that using Coil

same Here :'(

@rizwan3742
Copy link

any update on this ??

@khygu0919
Copy link

khygu0919 commented Mar 23, 2023

        val requestBuilder = Glide.with(context)
            .asDrawable()
            .sizeMultiplier(0.5f)
        GlideImage(
            model = contentUri,
            contentDescription = null,
            modifier = Modifier.fillMaxSize(),
            contentScale = ContentScale.Crop,
        ){
            it
                .override(this.maxWidth.value.dpToPx(), this.maxHeight.value.dpToPx())
                .thumbnail(requestBuilder)
        }

does coil support thumbnail size loading as glide does? when i load lot of image in grid in compose, it seems to be delayed compared to scroll action while loading image.

@timkabot
Copy link

timkabot commented Aug 18, 2023

Any updates on this?
Interrested in the way to implement this behaviour with AsyncImage in compose)

@devazimjon
Copy link

devazimjon commented Nov 15, 2023

I tried this and it worked for me in compose:

AsyncImage(
        model = "high resolution image",
        placeholder = rememberAsyncImagePainter("low resolution image"),
        contentScale = ContentScale.Crop,
        contentDescription = "",
        )
@dazza5000
Copy link

Anyone have an idea on how to do this? Maybe we can throw up an MR to add this support.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request