coil-kt / coil

Image loading for Android and Compose Multiplatform.
https://coil-kt.github.io/coil/
Apache License 2.0
10.77k stars 659 forks source link

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

Open lfielke opened 5 years ago

lfielke commented 5 years ago

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.

colinrtwhite commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 3 years ago

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 commented 3 years ago

Still waiting for this :(

frel commented 3 years ago

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 commented 2 years ago

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 commented 2 years ago

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

kasem-sm commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

@0n4li Hey, thanks for improving that code 😊

0n4li commented 2 years ago

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 commented 2 years ago

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 commented 1 year ago

any update on this ??

khygu0919 commented 1 year ago
        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 commented 1 year ago

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

devazimjon commented 11 months ago

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 commented 4 months ago

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