From 33ea5bc6ffded29f737ab1c6b31d98881f99b8d1 Mon Sep 17 00:00:00 2001 From: Colin White Date: Wed, 26 Jun 2024 10:27:51 -0700 Subject: [PATCH] Attempt to execute image request in Android Studio previews. (#2329) * Attempt to execute image request in Android Studio previews. * Fix tests. * Inline. * Rework API. * Spotless. --- .../api/android/coil-compose-core.api | 8 ++- .../api/coil-compose-core.klib.api | 4 +- .../api/jvm/coil-compose-core.api | 8 ++- .../coil3/compose/AsyncImagePainterTest.kt | 30 ++++---- .../compose/AsyncImagePainter.android.kt | 24 ------- .../coil3/compose/ImagePainter.android.kt | 26 +++++++ .../kotlin/coil3/compose/AsyncImagePainter.kt | 29 ++------ .../kotlin/coil3/compose/ImagePainter.kt | 11 +++ .../compose/LocalAsyncImagePreviewHandler.kt | 26 +++++-- .../compose/AsyncImagePainter.nonAndroid.kt | 19 ------ .../coil3/compose/ImagePainter.nonAndroid.kt | 21 ++++++ .../composescreenshot/PreviewScreenshots.kt | 68 +++++++++---------- 12 files changed, 149 insertions(+), 125 deletions(-) diff --git a/coil-compose-core/api/android/coil-compose-core.api b/coil-compose-core/api/android/coil-compose-core.api index ddfe3ba17f..818fc271df 100644 --- a/coil-compose-core/api/android/coil-compose-core.api +++ b/coil-compose-core/api/android/coil-compose-core.api @@ -89,7 +89,7 @@ public final class coil3/compose/AsyncImagePainterKt { public abstract interface class coil3/compose/AsyncImagePreviewHandler { public static final field Companion Lcoil3/compose/AsyncImagePreviewHandler$Companion; public static final field Default Lcoil3/compose/AsyncImagePreviewHandler; - public abstract fun handle (Lcoil3/ImageLoader;Lcoil3/request/ImageRequest;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun handle (Lcoil3/ImageLoader;Lcoil3/request/ImageRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public final class coil3/compose/AsyncImagePreviewHandler$Companion { @@ -126,7 +126,13 @@ public final class coil3/compose/ImagePainter : androidx/compose/ui/graphics/pai public fun getIntrinsicSize-NH-jbRc ()J } +public final class coil3/compose/ImagePainter_androidKt { + public static final fun asPainter-55t9-rM (Lcoil3/Image;Landroid/content/Context;I)Landroidx/compose/ui/graphics/painter/Painter; + public static synthetic fun asPainter-55t9-rM$default (Lcoil3/Image;Landroid/content/Context;IILjava/lang/Object;)Landroidx/compose/ui/graphics/painter/Painter; +} + public final class coil3/compose/LocalAsyncImagePreviewHandlerKt { + public static final fun AsyncImagePreviewHandler (Lkotlin/jvm/functions/Function2;)Lcoil3/compose/AsyncImagePreviewHandler; public static final fun getLocalAsyncImagePreviewHandler ()Landroidx/compose/runtime/ProvidableCompositionLocal; } diff --git a/coil-compose-core/api/coil-compose-core.klib.api b/coil-compose-core/api/coil-compose-core.klib.api index 1fec68ee40..9ef1599e25 100644 --- a/coil-compose-core/api/coil-compose-core.klib.api +++ b/coil-compose-core/api/coil-compose-core.klib.api @@ -7,7 +7,7 @@ // Library unique name: abstract fun interface coil3.compose/AsyncImagePreviewHandler { // coil3.compose/AsyncImagePreviewHandler|null[0] - abstract suspend fun handle(coil3/ImageLoader, coil3.request/ImageRequest, kotlin/Function1): coil3.compose/AsyncImagePainter.State // coil3.compose/AsyncImagePreviewHandler.handle|handle(coil3.ImageLoader;coil3.request.ImageRequest;kotlin.Function1){}[0] + abstract suspend fun handle(coil3/ImageLoader, coil3.request/ImageRequest): coil3.compose/AsyncImagePainter.State // coil3.compose/AsyncImagePreviewHandler.handle|handle(coil3.ImageLoader;coil3.request.ImageRequest){}[0] final object Companion { // coil3.compose/AsyncImagePreviewHandler.Companion|null[0] final val Default // coil3.compose/AsyncImagePreviewHandler.Companion.Default|{}Default[0] final fun (): coil3.compose/AsyncImagePreviewHandler // coil3.compose/AsyncImagePreviewHandler.Companion.Default.|(){}[0] @@ -116,6 +116,7 @@ final class coil3.compose/ImagePainter : androidx.compose.ui.graphics.painter/Pa final fun (): androidx.compose.ui.geometry/Size // coil3.compose/ImagePainter.intrinsicSize.|(){}[0] } final fun (coil3.compose/SubcomposeAsyncImageScope).coil3.compose/SubcomposeAsyncImageContent(androidx.compose.ui/Modifier?, androidx.compose.ui.graphics.painter/Painter?, kotlin/String?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // coil3.compose/SubcomposeAsyncImageContent|SubcomposeAsyncImageContent@coil3.compose.SubcomposeAsyncImageScope(androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.painter.Painter?;kotlin.String?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] +final fun (coil3/Image).coil3.compose/asPainter(coil3/PlatformContext, androidx.compose.ui.graphics/FilterQuality = ...): androidx.compose.ui.graphics.painter/Painter // coil3.compose/asPainter|asPainter@coil3.Image(coil3.PlatformContext;androidx.compose.ui.graphics.FilterQuality){}[0] final fun coil3.compose/AsyncImage(kotlin/Any?, kotlin/String?, coil3/ImageLoader, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, kotlin/Function1?, kotlin/Function1?, kotlin/Function1?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality, kotlin/Boolean, coil3.compose/EqualityDelegate?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/AsyncImage|AsyncImage(kotlin.Any?;kotlin.String?;coil3.ImageLoader;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;kotlin.Function1?;kotlin.Function1?;kotlin.Function1?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality;kotlin.Boolean;coil3.compose.EqualityDelegate?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0] final fun coil3.compose/AsyncImage(kotlin/Any?, kotlin/String?, coil3/ImageLoader, androidx.compose.ui/Modifier?, kotlin/Function1?, kotlin/Function1?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality, kotlin/Boolean, coil3.compose/EqualityDelegate?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/AsyncImage|AsyncImage(kotlin.Any?;kotlin.String?;coil3.ImageLoader;androidx.compose.ui.Modifier?;kotlin.Function1?;kotlin.Function1?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality;kotlin.Boolean;coil3.compose.EqualityDelegate?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0] final fun coil3.compose/DrawScopeSizeResolver(): coil3.compose/DrawScopeSizeResolver // coil3.compose/DrawScopeSizeResolver|DrawScopeSizeResolver(){}[0] @@ -123,6 +124,7 @@ final fun coil3.compose/SubcomposeAsyncImage(kotlin/Any?, kotlin/String?, coil3/ final fun coil3.compose/SubcomposeAsyncImage(kotlin/Any?, kotlin/String?, coil3/ImageLoader, androidx.compose.ui/Modifier?, kotlin/Function1?, kotlin/Function4?, kotlin/Function4?, kotlin/Function4?, kotlin/Function1?, kotlin/Function1?, kotlin/Function1?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality, kotlin/Boolean, coil3.compose/EqualityDelegate?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/SubcomposeAsyncImage|SubcomposeAsyncImage(kotlin.Any?;kotlin.String?;coil3.ImageLoader;androidx.compose.ui.Modifier?;kotlin.Function1?;kotlin.Function4?;kotlin.Function4?;kotlin.Function4?;kotlin.Function1?;kotlin.Function1?;kotlin.Function1?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality;kotlin.Boolean;coil3.compose.EqualityDelegate?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0] final fun coil3.compose/rememberAsyncImagePainter(kotlin/Any?, coil3/ImageLoader, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, kotlin/Function1?, kotlin/Function1?, kotlin/Function1?, androidx.compose.ui.layout/ContentScale?, androidx.compose.ui.graphics/FilterQuality, coil3.compose/EqualityDelegate?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int): coil3.compose/AsyncImagePainter // coil3.compose/rememberAsyncImagePainter|rememberAsyncImagePainter(kotlin.Any?;coil3.ImageLoader;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;kotlin.Function1?;kotlin.Function1?;kotlin.Function1?;androidx.compose.ui.layout.ContentScale?;androidx.compose.ui.graphics.FilterQuality;coil3.compose.EqualityDelegate?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0] final fun coil3.compose/rememberAsyncImagePainter(kotlin/Any?, coil3/ImageLoader, kotlin/Function1?, kotlin/Function1?, androidx.compose.ui.layout/ContentScale?, androidx.compose.ui.graphics/FilterQuality, coil3.compose/EqualityDelegate?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): coil3.compose/AsyncImagePainter // coil3.compose/rememberAsyncImagePainter|rememberAsyncImagePainter(kotlin.Any?;coil3.ImageLoader;kotlin.Function1?;kotlin.Function1?;androidx.compose.ui.layout.ContentScale?;androidx.compose.ui.graphics.FilterQuality;coil3.compose.EqualityDelegate?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] +final inline fun coil3.compose/AsyncImagePreviewHandler(crossinline kotlin.coroutines/SuspendFunction1): coil3.compose/AsyncImagePreviewHandler // coil3.compose/AsyncImagePreviewHandler|AsyncImagePreviewHandler(kotlin.coroutines.SuspendFunction1){}[0] final val coil3.compose.internal/coil3_compose_internal_AsyncImageState$stableprop // coil3.compose.internal/coil3_compose_internal_AsyncImageState$stableprop|#static{}coil3_compose_internal_AsyncImageState$stableprop[0] final val coil3.compose.internal/coil3_compose_internal_ConstraintsSizeResolver$stableprop // coil3.compose.internal/coil3_compose_internal_ConstraintsSizeResolver$stableprop|#static{}coil3_compose_internal_ConstraintsSizeResolver$stableprop[0] final val coil3.compose.internal/coil3_compose_internal_ContentPainterElement$stableprop // coil3.compose.internal/coil3_compose_internal_ContentPainterElement$stableprop|#static{}coil3_compose_internal_ContentPainterElement$stableprop[0] diff --git a/coil-compose-core/api/jvm/coil-compose-core.api b/coil-compose-core/api/jvm/coil-compose-core.api index 4609c7dd22..8a859924b4 100644 --- a/coil-compose-core/api/jvm/coil-compose-core.api +++ b/coil-compose-core/api/jvm/coil-compose-core.api @@ -89,7 +89,7 @@ public final class coil3/compose/AsyncImagePainterKt { public abstract interface class coil3/compose/AsyncImagePreviewHandler { public static final field Companion Lcoil3/compose/AsyncImagePreviewHandler$Companion; public static final field Default Lcoil3/compose/AsyncImagePreviewHandler; - public abstract fun handle (Lcoil3/ImageLoader;Lcoil3/request/ImageRequest;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun handle (Lcoil3/ImageLoader;Lcoil3/request/ImageRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public final class coil3/compose/AsyncImagePreviewHandler$Companion { @@ -126,7 +126,13 @@ public final class coil3/compose/ImagePainter : androidx/compose/ui/graphics/pai public fun getIntrinsicSize-NH-jbRc ()J } +public final class coil3/compose/ImagePainter_nonAndroidKt { + public static final fun asPainter-55t9-rM (Lcoil3/Image;Lcoil3/PlatformContext;I)Landroidx/compose/ui/graphics/painter/Painter; + public static synthetic fun asPainter-55t9-rM$default (Lcoil3/Image;Lcoil3/PlatformContext;IILjava/lang/Object;)Landroidx/compose/ui/graphics/painter/Painter; +} + public final class coil3/compose/LocalAsyncImagePreviewHandlerKt { + public static final fun AsyncImagePreviewHandler (Lkotlin/jvm/functions/Function2;)Lcoil3/compose/AsyncImagePreviewHandler; public static final fun getLocalAsyncImagePreviewHandler ()Landroidx/compose/runtime/ProvidableCompositionLocal; } diff --git a/coil-compose-core/src/androidInstrumentedTest/kotlin/coil3/compose/AsyncImagePainterTest.kt b/coil-compose-core/src/androidInstrumentedTest/kotlin/coil3/compose/AsyncImagePainterTest.kt index 5e8d9376b5..5d195bd56b 100644 --- a/coil-compose-core/src/androidInstrumentedTest/kotlin/coil3/compose/AsyncImagePainterTest.kt +++ b/coil-compose-core/src/androidInstrumentedTest/kotlin/coil3/compose/AsyncImagePainterTest.kt @@ -479,21 +479,25 @@ class AsyncImagePainterTest { fun previewPlaceholder() { assumeSupportsCaptureToImage() + val previewHandler = AsyncImagePreviewHandler(ImageRequest::placeholder) + composeTestRule.setContent { CompositionLocalProvider(LocalInspectionMode provides true) { - Image( - painter = rememberAsyncImagePainter( - model = ImageRequest.Builder(LocalContext.current) - .data("https://example.com/image") - .placeholder(R.drawable.red_rectangle) - .build(), - imageLoader = imageLoader, - ), - contentDescription = null, - modifier = Modifier - .size(128.dp) - .testTag(Image), - ) + CompositionLocalProvider(LocalAsyncImagePreviewHandler provides previewHandler) { + Image( + painter = rememberAsyncImagePainter( + model = ImageRequest.Builder(LocalContext.current) + .data("https://example.com/image") + .placeholder(R.drawable.red_rectangle) + .build(), + imageLoader = imageLoader, + ), + contentDescription = null, + modifier = Modifier + .size(128.dp) + .testTag(Image), + ) + } } } diff --git a/coil-compose-core/src/androidMain/kotlin/coil3/compose/AsyncImagePainter.android.kt b/coil-compose-core/src/androidMain/kotlin/coil3/compose/AsyncImagePainter.android.kt index c6d7db7f6b..d8b1f707e3 100644 --- a/coil-compose-core/src/androidMain/kotlin/coil3/compose/AsyncImagePainter.android.kt +++ b/coil-compose-core/src/androidMain/kotlin/coil3/compose/AsyncImagePainter.android.kt @@ -1,16 +1,7 @@ package coil3.compose import android.graphics.drawable.Drawable -import androidx.compose.ui.graphics.FilterQuality -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.graphics.painter.BitmapPainter -import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale -import coil3.BitmapImage -import coil3.DrawableImage -import coil3.Image -import coil3.PlatformContext -import coil3.asDrawable import coil3.compose.internal.CrossfadePainter import coil3.request.GlobalLifecycle import coil3.request.ImageRequest @@ -19,7 +10,6 @@ import coil3.request.lifecycle import coil3.request.transitionFactory import coil3.transition.CrossfadeTransition import coil3.transition.TransitionTarget -import com.google.accompanist.drawablepainter.DrawablePainter internal actual fun validateRequestProperties(request: ImageRequest) { require(request.target == null) { "request.target must be null." } @@ -30,20 +20,6 @@ internal actual fun ImageRequest.Builder.applyGlobalLifecycle() { lifecycle(GlobalLifecycle) } -internal actual fun Image.toPainter( - context: PlatformContext, - filterQuality: FilterQuality, -): Painter = when (this) { - is BitmapImage -> BitmapPainter( - image = bitmap.asImageBitmap(), - filterQuality = filterQuality, - ) - is DrawableImage -> DrawablePainter( - drawable = asDrawable(context.resources).mutate(), - ) - else -> ImagePainter(this) -} - internal actual fun maybeNewCrossfadePainter( previous: AsyncImagePainter.State, current: AsyncImagePainter.State, diff --git a/coil-compose-core/src/androidMain/kotlin/coil3/compose/ImagePainter.android.kt b/coil-compose-core/src/androidMain/kotlin/coil3/compose/ImagePainter.android.kt index d9bfaa6e11..bba85e1006 100644 --- a/coil-compose-core/src/androidMain/kotlin/coil3/compose/ImagePainter.android.kt +++ b/coil-compose-core/src/androidMain/kotlin/coil3/compose/ImagePainter.android.kt @@ -1,6 +1,32 @@ package coil3.compose import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.FilterQuality +import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.Painter +import coil3.BitmapImage +import coil3.DrawableImage +import coil3.Image +import coil3.PlatformContext +import coil3.annotation.ExperimentalCoilApi +import coil3.asDrawable +import com.google.accompanist.drawablepainter.DrawablePainter + +@ExperimentalCoilApi +actual fun Image.asPainter( + context: PlatformContext, + filterQuality: FilterQuality, +): Painter = when (this) { + is BitmapImage -> BitmapPainter( + image = bitmap.asImageBitmap(), + filterQuality = filterQuality, + ) + is DrawableImage -> DrawablePainter( + drawable = asDrawable(context.resources).mutate(), + ) + else -> ImagePainter(this) +} internal actual val Canvas.nativeCanvas get() = nativeCanvas diff --git a/coil-compose-core/src/commonMain/kotlin/coil3/compose/AsyncImagePainter.kt b/coil-compose-core/src/commonMain/kotlin/coil3/compose/AsyncImagePainter.kt index 16f4f39ae8..6969673303 100644 --- a/coil-compose-core/src/commonMain/kotlin/coil3/compose/AsyncImagePainter.kt +++ b/coil-compose-core/src/commonMain/kotlin/coil3/compose/AsyncImagePainter.kt @@ -29,7 +29,6 @@ import androidx.compose.ui.unit.Constraints import androidx.compose.ui.util.trace import coil3.Image import coil3.ImageLoader -import coil3.PlatformContext import coil3.annotation.Poko import coil3.compose.AsyncImagePainter.Companion.DefaultTransform import coil3.compose.AsyncImagePainter.Input @@ -244,7 +243,7 @@ class AsyncImagePainter internal constructor( if (previewHandler != null) { // If we're in inspection mode use the preview renderer. _input.mapLatest { - computePreviewState(it.imageLoader, it.request, previewHandler) + previewHandler.handle(it.imageLoader, it.request) } } else { // Else, execute the request as normal. @@ -276,7 +275,7 @@ class AsyncImagePainter internal constructor( return request.newBuilder() .target( onStart = { placeholder -> - val painter = placeholder?.toPainter(request.context, filterQuality) + val painter = placeholder?.asPainter(request.context, filterQuality) updateState(State.Loading(painter)) }, ) @@ -314,27 +313,13 @@ class AsyncImagePainter internal constructor( onState?.invoke(current) } - private suspend fun computePreviewState( - imageLoader: ImageLoader, - request: ImageRequest, - previewHandler: AsyncImagePreviewHandler, - ): State { - return previewHandler.handle( - imageLoader = imageLoader, - request = request.newBuilder() - .defaults(imageLoader.defaults) - .build(), - toPainter = { toPainter(request.context, filterQuality) }, - ) - } - private fun ImageResult.toState() = when (this) { is SuccessResult -> State.Success( - painter = image.toPainter(request.context, filterQuality), + painter = image.asPainter(request.context, filterQuality), result = this, ) is ErrorResult -> State.Error( - painter = image?.toPainter(request.context, filterQuality), + painter = image?.asPainter(request.context, filterQuality), result = this, ) } @@ -421,12 +406,6 @@ internal expect fun validateRequestProperties(request: ImageRequest) /** Set the request's lifecycle to `GlobalLifecycle` on Android to avoid dispatching. */ internal expect fun ImageRequest.Builder.applyGlobalLifecycle() -/** Convert this [Image] into a [Painter] using Compose primitives if possible. */ -internal expect fun Image.toPainter( - context: PlatformContext, - filterQuality: FilterQuality, -): Painter - /** Create and return a [CrossfadePainter] if requested. */ internal expect fun maybeNewCrossfadePainter( previous: State, diff --git a/coil-compose-core/src/commonMain/kotlin/coil3/compose/ImagePainter.kt b/coil-compose-core/src/commonMain/kotlin/coil3/compose/ImagePainter.kt index 9c4c92a599..268e3fa1b3 100644 --- a/coil-compose-core/src/commonMain/kotlin/coil3/compose/ImagePainter.kt +++ b/coil-compose-core/src/commonMain/kotlin/coil3/compose/ImagePainter.kt @@ -1,12 +1,16 @@ package coil3.compose +import androidx.compose.foundation.Image import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.DrawScope.Companion.DefaultFilterQuality import androidx.compose.ui.graphics.drawscope.scale import androidx.compose.ui.graphics.painter.Painter import coil3.Image +import coil3.PlatformContext import coil3.annotation.ExperimentalCoilApi /** @@ -40,4 +44,11 @@ class ImagePainter( } } +/** Convert this [Image] into a [Painter] using Compose primitives if possible. */ +@ExperimentalCoilApi +expect fun Image.asPainter( + context: PlatformContext, + filterQuality: FilterQuality = DefaultFilterQuality, +): Painter + internal expect val Canvas.nativeCanvas: coil3.Canvas diff --git a/coil-compose-core/src/commonMain/kotlin/coil3/compose/LocalAsyncImagePreviewHandler.kt b/coil-compose-core/src/commonMain/kotlin/coil3/compose/LocalAsyncImagePreviewHandler.kt index cb5e1adaa4..d66251e97c 100644 --- a/coil-compose-core/src/commonMain/kotlin/coil3/compose/LocalAsyncImagePreviewHandler.kt +++ b/coil-compose-core/src/commonMain/kotlin/coil3/compose/LocalAsyncImagePreviewHandler.kt @@ -1,19 +1,23 @@ package coil3.compose import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalInspectionMode import coil3.Image import coil3.ImageLoader import coil3.annotation.ExperimentalCoilApi +import coil3.compose.AsyncImagePainter.State.Error +import coil3.compose.AsyncImagePainter.State.Loading +import coil3.compose.AsyncImagePainter.State.Success +import coil3.request.ErrorResult import coil3.request.ImageRequest +import coil3.request.SuccessResult import kotlin.jvm.JvmField @ExperimentalCoilApi val LocalAsyncImagePreviewHandler = staticCompositionLocalOf { AsyncImagePreviewHandler.Default } /** - * Controls what [AsyncImage], [SubcomposeAsyncImage], and [AsyncImagePainter] renders when + * Controls what [AsyncImage], [SubcomposeAsyncImage], and [AsyncImagePainter] render when * [LocalInspectionMode] is true. */ @ExperimentalCoilApi @@ -22,12 +26,24 @@ fun interface AsyncImagePreviewHandler { suspend fun handle( imageLoader: ImageLoader, request: ImageRequest, - toPainter: Image.() -> Painter, ): AsyncImagePainter.State companion object { - @JvmField val Default = AsyncImagePreviewHandler { _, request, toPainter -> - AsyncImagePainter.State.Loading(request.placeholder()?.toPainter()) + @JvmField val Default = AsyncImagePreviewHandler { imageLoader, request -> + when (val result = imageLoader.execute(request)) { + is SuccessResult -> Success(result.image.asPainter(request.context), result) + is ErrorResult -> Error(result.image?.asPainter(request.context), result) + } } } } + +/** + * Convenience function that creates an [AsyncImagePreviewHandler] that returns an [Image]. + */ +@ExperimentalCoilApi +inline fun AsyncImagePreviewHandler( + crossinline image: suspend (request: ImageRequest) -> Image?, +) = AsyncImagePreviewHandler { _, request -> + Loading(image(request)?.asPainter(request.context)) +} diff --git a/coil-compose-core/src/nonAndroidMain/kotlin/coil3/compose/AsyncImagePainter.nonAndroid.kt b/coil-compose-core/src/nonAndroidMain/kotlin/coil3/compose/AsyncImagePainter.nonAndroid.kt index 602a5a3a6c..fea835748a 100644 --- a/coil-compose-core/src/nonAndroidMain/kotlin/coil3/compose/AsyncImagePainter.nonAndroid.kt +++ b/coil-compose-core/src/nonAndroidMain/kotlin/coil3/compose/AsyncImagePainter.nonAndroid.kt @@ -1,19 +1,11 @@ package coil3.compose -import androidx.compose.ui.graphics.FilterQuality -import androidx.compose.ui.graphics.asComposeImageBitmap -import androidx.compose.ui.graphics.painter.BitmapPainter -import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale -import coil3.BitmapImage -import coil3.Image -import coil3.PlatformContext import coil3.compose.internal.CrossfadePainter import coil3.decode.DataSource import coil3.request.ImageRequest import coil3.request.SuccessResult import coil3.request.crossfadeMillis -import coil3.toBitmap internal actual fun validateRequestProperties(request: ImageRequest) { require(request.target == null) { "request.target must be null." } @@ -23,17 +15,6 @@ internal actual fun ImageRequest.Builder.applyGlobalLifecycle() { // Do nothing. } -internal actual fun Image.toPainter( - context: PlatformContext, - filterQuality: FilterQuality, -): Painter = when (this) { - is BitmapImage -> BitmapPainter( - image = toBitmap().asComposeImageBitmap(), - filterQuality = filterQuality, - ) - else -> ImagePainter(this) -} - internal actual fun maybeNewCrossfadePainter( previous: AsyncImagePainter.State, current: AsyncImagePainter.State, diff --git a/coil-compose-core/src/nonAndroidMain/kotlin/coil3/compose/ImagePainter.nonAndroid.kt b/coil-compose-core/src/nonAndroidMain/kotlin/coil3/compose/ImagePainter.nonAndroid.kt index d9bfaa6e11..ae0dd89053 100644 --- a/coil-compose-core/src/nonAndroidMain/kotlin/coil3/compose/ImagePainter.nonAndroid.kt +++ b/coil-compose-core/src/nonAndroidMain/kotlin/coil3/compose/ImagePainter.nonAndroid.kt @@ -1,6 +1,27 @@ package coil3.compose import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.FilterQuality +import androidx.compose.ui.graphics.asComposeImageBitmap import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.Painter +import coil3.BitmapImage +import coil3.Image +import coil3.PlatformContext +import coil3.annotation.ExperimentalCoilApi +import coil3.toBitmap + +@ExperimentalCoilApi +actual fun Image.asPainter( + context: PlatformContext, + filterQuality: FilterQuality, +): Painter = when (this) { + is BitmapImage -> BitmapPainter( + image = toBitmap().asComposeImageBitmap(), + filterQuality = filterQuality, + ) + else -> ImagePainter(this) +} internal actual val Canvas.nativeCanvas get() = nativeCanvas diff --git a/internal/test-compose-screenshot/src/screenshotTest/java/coil3/composescreenshot/PreviewScreenshots.kt b/internal/test-compose-screenshot/src/screenshotTest/java/coil3/composescreenshot/PreviewScreenshots.kt index eb19650534..950e808962 100644 --- a/internal/test-compose-screenshot/src/screenshotTest/java/coil3/composescreenshot/PreviewScreenshots.kt +++ b/internal/test-compose-screenshot/src/screenshotTest/java/coil3/composescreenshot/PreviewScreenshots.kt @@ -5,6 +5,7 @@ import android.graphics.drawable.ColorDrawable import androidx.compose.foundation.Image import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale @@ -12,12 +13,21 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import coil3.ImageLoader +import coil3.annotation.ExperimentalCoilApi +import coil3.asImage import coil3.compose.AsyncImage +import coil3.compose.AsyncImagePreviewHandler +import coil3.compose.LocalAsyncImagePreviewHandler import coil3.compose.rememberAsyncImagePainter -import coil3.request.ImageRequest -import coil3.request.placeholder +@OptIn(ExperimentalCoilApi::class) class PreviewScreenshots { + private val previewHandler = AsyncImagePreviewHandler { + object : ColorDrawable(Color.RED) { + override fun getIntrinsicWidth() = 100 + override fun getIntrinsicHeight() = 100 + }.asImage() + } @Preview( device = Devices.PIXEL, @@ -28,22 +38,15 @@ class PreviewScreenshots { val context = LocalContext.current val imageLoader = remember { ImageLoader(context) } - AsyncImage( - // AsyncImagePainter's default preview behaviour displays the request's placeholder. - model = ImageRequest.Builder(context) - .data(Unit) - .placeholder( - object : ColorDrawable(Color.RED) { - override fun getIntrinsicWidth() = 100 - override fun getIntrinsicHeight() = 100 - }, - ) - .build(), - contentDescription = null, - imageLoader = imageLoader, - contentScale = ContentScale.None, - modifier = Modifier.fillMaxSize(), - ) + CompositionLocalProvider(LocalAsyncImagePreviewHandler provides previewHandler) { + AsyncImage( + model = Unit, + contentDescription = null, + imageLoader = imageLoader, + contentScale = ContentScale.None, + modifier = Modifier.fillMaxSize(), + ) + } } @Preview( @@ -55,23 +58,16 @@ class PreviewScreenshots { val context = LocalContext.current val imageLoader = remember { ImageLoader(context) } - Image( - painter = rememberAsyncImagePainter( - // AsyncImagePainter's default preview behaviour displays the request's placeholder. - model = ImageRequest.Builder(context) - .data(Unit) - .placeholder( - object : ColorDrawable(Color.RED) { - override fun getIntrinsicWidth() = 100 - override fun getIntrinsicHeight() = 100 - }, - ) - .build(), - imageLoader = imageLoader, - ), - contentDescription = null, - contentScale = ContentScale.None, - modifier = Modifier.fillMaxSize(), - ) + CompositionLocalProvider(LocalAsyncImagePreviewHandler provides previewHandler) { + Image( + painter = rememberAsyncImagePainter( + model = Unit, + imageLoader = imageLoader, + ), + contentDescription = null, + contentScale = ContentScale.None, + modifier = Modifier.fillMaxSize(), + ) + } } }