Skip to content

Add crossfade between subsequent image results in AsyncImage#3141

Merged
colinrtwhite merged 18 commits into
coil-kt:mainfrom
tak8997:crossfade_improvement
Sep 4, 2025
Merged

Add crossfade between subsequent image results in AsyncImage#3141
colinrtwhite merged 18 commits into
coil-kt:mainfrom
tak8997:crossfade_improvement

Conversation

@tak8997
Copy link
Copy Markdown
Contributor

@tak8997 tak8997 commented Aug 23, 2025

Description

Currently, AsyncImage's crossfade only applies from placeholder → result.
When switching between different image models, the new image replaces the old one immediately ("jump cut") without a crossfade.
This PR introduces an optional parameter to support crossfading from the previous successful image result → the new result, skipping the placeholder.

Motivation

  • Addresses #2994.
  • Many developers expect true crossfade between subsequent requests (not only placeholder → result).

Notes

  • Default behavior remains unchanged to avoid breaking existing apps.
  • When enabled, fade duration now properly applies to subsequent requests.
  • Tested with multiple consecutive image requests to confirm smooth transition.


internal const val DEFAULT_CROSSFADE_MILLIS = 200

fun ImageRequest.Builder.crossfadeBetweenImages(enable: Boolean) = apply {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a short doc comment for this method?

Also we should add ImageLoader.Builder.crossfadeBetweenImages so this can be configured per-ImageLoader.

Copy link
Copy Markdown
Contributor Author

@tak8997 tak8997 Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bed4934
3b6c58c
please check this commit

extras[crossfadeBetweenImagesKey] = enable
}

val ImageRequest.crossfadeBetweenImages: Boolean
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please annotate this and crossfadeBetweenImages with @ExperimentalCoilApi.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

129111e
please check this commit

val ImageRequest.crossfadeBetweenImages: Boolean
get() = getExtra(crossfadeBetweenImagesKey)

private val crossfadeBetweenImagesKey = Extras.Key(default = false)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also add:

val Extras.Key.Companion.crossfadeBetweenImages: Extras.Key<Boolean>
    get() = crossfadeBetweenImagesKey

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ba668de
please check this commit

Copy link
Copy Markdown
Member

@colinrtwhite colinrtwhite left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for implementing this. I think this looks good for Compose, but do you know how we would also add support to Views?

Ideally, I'd like for this ImageRequest param to automatically support Views and Compose. Currently I could see someone using this param with an ImageView and being confused it doesn't work.

@tak8997 tak8997 force-pushed the crossfade_improvement branch from f2a7820 to e887971 Compare August 26, 2025 07:55
@tak8997 tak8997 force-pushed the crossfade_improvement branch from 7780b60 to 3b6c58c Compare August 27, 2025 04:56
@tak8997 tak8997 closed this Aug 27, 2025
@tak8997 tak8997 reopened this Aug 27, 2025
@tak8997
Copy link
Copy Markdown
Contributor Author

tak8997 commented Aug 27, 2025

Thanks for implementing this. I think this looks good for Compose, but do you know how we would also add support to Views?

Ideally, I'd like for this ImageRequest param to automatically support Views and Compose. Currently I could see someone using this param with an ImageView and being confused it doesn't work.

340a445
please check this commit

@tak8997 tak8997 requested a review from colinrtwhite August 28, 2025 01:54
Copy link
Copy Markdown
Member

@colinrtwhite colinrtwhite left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this. Thinking more about it - I think we should make this solution more generic. Currently we're coupling this behaviour to crossfade, but really we want the ability to use the existing image as the placeholder for the next image request. If we do that then both the view and Compose-based crossfade implementations should automatically crossfade from what the previous image is. It would also allow users to skip setting the placeholder if they want to go from first loaded image -> second loaded image without setting a placeholder and without a crossfade.

Unfortunately we don't have knowledge of what the existing image is with views, but we do with Compose/AsyncImagePainter. As such, I think it's OK to only support this functionality in Compose. Here's what we should do:

  • Revert the changes to Target to support views.
  • Rename the config option crossfadeBetweenImages to useExistingImageAsPlaceholder(enable: Boolean) (open to better names) and move it to an imageRequest.kt file in coil-compose-core (as it's only supported in Compose). We should note in the method description that this config option will only work in Compose.
  • Update AsyncImagePainter to use the existing painter in onStart if placeholder == null and useExistingImageAsPlaceholder is enabled on the ImageRequest.

Thanks again for working and iterating on this!

/**
* Called when the request starts.
*/
fun onStart(placeholder: Image?) {}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't make this change as it breaks binary compatibility.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to convert the existing view drawable into an image in RealImageLoader's onStart method when there is no placeholder, but unfortunately, there seems to be no way to do it.🥲
Anyway, I have reverted the changes on the 'View' side.

please check this!
97c734b
f47f8f8
48658df

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@colinrtwhite Could you please take a look?🙂

…rawable in GenericViewTarget. Additionally, opt-in option is provided."

This reverts commit 340a445.
* Rename `crossfadeBetweenImages` to `useExistingImageAsPlaceholder` for clarity
* Move from `coil-core` to `coil-compose-core` to restrict to Compose usage only
* Update documentation to explicitly state Compose-only support
* Remove from core module to prevent incorrect usage in XML views
@tak8997 tak8997 requested a review from colinrtwhite September 2, 2025 07:24
Copy link
Copy Markdown
Member

@colinrtwhite colinrtwhite left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for implementing this!

@colinrtwhite colinrtwhite merged commit 252a4b9 into coil-kt:main Sep 4, 2025
10 checks passed
@tak8997 tak8997 deleted the crossfade_improvement branch September 5, 2025 03:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants