diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6eff0c15..f36a7f90 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -46,7 +46,7 @@ dependencies { implementation(project(":landscapist-palette")) implementation(project(":glide")) - implementation(project(":coil")) + implementation(project(":coil3")) implementation(project(":fresco")) implementation(project(":fresco-websupport")) diff --git a/app/src/main/kotlin/com/github/skydoves/landscapistdemo/App.kt b/app/src/main/kotlin/com/github/skydoves/landscapistdemo/App.kt index be66ec2c..2a29a942 100644 --- a/app/src/main/kotlin/com/github/skydoves/landscapistdemo/App.kt +++ b/app/src/main/kotlin/com/github/skydoves/landscapistdemo/App.kt @@ -17,14 +17,18 @@ package com.github.skydoves.landscapistdemo +import android.content.Context import androidx.multidex.MultiDexApplication +import coil3.ImageLoader +import coil3.SingletonImageLoader +import coil3.fetch.NetworkFetcher import com.facebook.drawee.backends.pipeline.Fresco import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory import dagger.hilt.android.HiltAndroidApp import okhttp3.OkHttpClient @HiltAndroidApp -class App : MultiDexApplication() { +class App : MultiDexApplication(), SingletonImageLoader.Factory { override fun onCreate() { super.onCreate() @@ -40,4 +44,12 @@ class App : MultiDexApplication() { Fresco.initialize(this, pipelineConfig) } + + override fun newImageLoader(context: Context): ImageLoader { + return ImageLoader.Builder(context) + .components { + add(NetworkFetcher.Factory()) + } + .build() + } } diff --git a/app/src/main/kotlin/com/github/skydoves/landscapistdemo/ui/MainPosters.kt b/app/src/main/kotlin/com/github/skydoves/landscapistdemo/ui/MainPosters.kt index d8304fe7..645fa096 100644 --- a/app/src/main/kotlin/com/github/skydoves/landscapistdemo/ui/MainPosters.kt +++ b/app/src/main/kotlin/com/github/skydoves/landscapistdemo/ui/MainPosters.kt @@ -47,6 +47,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -59,6 +60,7 @@ import com.github.skydoves.landscapistdemo.theme.background import com.skydoves.landscapist.ImageOptions import com.skydoves.landscapist.animation.circular.CircularRevealPlugin import com.skydoves.landscapist.animation.crossfade.CrossfadePlugin +import com.skydoves.landscapist.coil3.CoilImage import com.skydoves.landscapist.components.rememberImageComponent import com.skydoves.landscapist.fresco.FrescoImage import com.skydoves.landscapist.glide.GlideImage @@ -132,7 +134,7 @@ private fun SelectedPoster( ) { var palette by rememberPaletteState(null) - GlideImage( + CoilImage( imageModel = { poster.image }, modifier = Modifier.aspectRatio(0.8f), component = rememberImageComponent { @@ -149,7 +151,7 @@ private fun SelectedPoster( +CircularRevealPlugin() +PalettePlugin { palette = it } }, - previewPlaceholder = R.drawable.poster, + previewPlaceholder = painterResource(id = R.drawable.poster), ) ColorPalettes(palette) diff --git a/benchmark-landscapist-app/build.gradle.kts b/benchmark-landscapist-app/build.gradle.kts index 3dd61d6b..5e4c11f1 100644 --- a/benchmark-landscapist-app/build.gradle.kts +++ b/benchmark-landscapist-app/build.gradle.kts @@ -51,6 +51,7 @@ dependencies { implementation(project(":glide")) implementation(project(":coil")) + implementation(project(":coil3")) implementation(project(":fresco")) implementation(project(":fresco-websupport")) diff --git a/benchmark-landscapist-app/src/main/kotlin/com/skydoves/benchmark/landscapist/app/App.kt b/benchmark-landscapist-app/src/main/kotlin/com/skydoves/benchmark/landscapist/app/App.kt index 80e80d2b..0a5d3a08 100644 --- a/benchmark-landscapist-app/src/main/kotlin/com/skydoves/benchmark/landscapist/app/App.kt +++ b/benchmark-landscapist-app/src/main/kotlin/com/skydoves/benchmark/landscapist/app/App.kt @@ -18,11 +18,15 @@ package com.skydoves.benchmark.landscapist.app import android.app.Application +import android.content.Context +import coil3.ImageLoader +import coil3.SingletonImageLoader +import coil3.fetch.NetworkFetcher import com.facebook.drawee.backends.pipeline.Fresco import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory import okhttp3.OkHttpClient -class App : Application() { +class App : Application(), SingletonImageLoader.Factory { override fun onCreate() { super.onCreate() @@ -38,4 +42,12 @@ class App : Application() { Fresco.initialize(this, pipelineConfig) } + + override fun newImageLoader(context: Context): ImageLoader { + return ImageLoader.Builder(context) + .components { + add(NetworkFetcher.Factory()) + } + .build() + } } diff --git a/benchmark-landscapist-app/src/main/kotlin/com/skydoves/benchmark/landscapist/app/CoilImage3Profiles.kt b/benchmark-landscapist-app/src/main/kotlin/com/skydoves/benchmark/landscapist/app/CoilImage3Profiles.kt new file mode 100644 index 00000000..5d536ac6 --- /dev/null +++ b/benchmark-landscapist-app/src/main/kotlin/com/skydoves/benchmark/landscapist/app/CoilImage3Profiles.kt @@ -0,0 +1,39 @@ +/* + * Designed and developed by 2020-2023 skydoves (Jaewoong Eum) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.skydoves.benchmark.landscapist.app + +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.skydoves.landscapist.ImageOptions +import com.skydoves.landscapist.coil3.CoilImage +import com.skydoves.landscapist.components.LocalImageComponent + +@Composable +fun Coil3ImageProfiles() { + CoilImage( + modifier = Modifier.size(120.dp), + imageModel = { + "https://user-images.githubusercontent.com/" + + "24237865/75087936-5c1d9f80-553e-11ea-81d3-a912634dd8f7.jpg" + }, + previewPlaceholder = painterResource(id = R.drawable.poster), + component = LocalImageComponent.current, + imageOptions = ImageOptions(tag = "CoilImage"), + ) +} diff --git a/benchmark-landscapist-app/src/main/kotlin/com/skydoves/benchmark/landscapist/app/CoilImageProfiles.kt b/benchmark-landscapist-app/src/main/kotlin/com/skydoves/benchmark/landscapist/app/CoilImageProfiles.kt index b454691f..4f19c701 100644 --- a/benchmark-landscapist-app/src/main/kotlin/com/skydoves/benchmark/landscapist/app/CoilImageProfiles.kt +++ b/benchmark-landscapist-app/src/main/kotlin/com/skydoves/benchmark/landscapist/app/CoilImageProfiles.kt @@ -33,6 +33,6 @@ fun CoilImageProfiles() { }, previewPlaceholder = R.drawable.poster, component = LocalImageComponent.current, - imageOptions = ImageOptions(testTag = "CoilImage"), + imageOptions = ImageOptions(tag = "CoilImage"), ) } diff --git a/benchmark-landscapist-app/src/main/kotlin/com/skydoves/benchmark/landscapist/app/FrescoImageProfiles.kt b/benchmark-landscapist-app/src/main/kotlin/com/skydoves/benchmark/landscapist/app/FrescoImageProfiles.kt index 2665ce24..e2de6c36 100644 --- a/benchmark-landscapist-app/src/main/kotlin/com/skydoves/benchmark/landscapist/app/FrescoImageProfiles.kt +++ b/benchmark-landscapist-app/src/main/kotlin/com/skydoves/benchmark/landscapist/app/FrescoImageProfiles.kt @@ -31,6 +31,6 @@ fun FrescoImageProfiles() { "24237865/75087936-5c1d9f80-553e-11ea-81d3-a912634dd8f7.jpg", previewPlaceholder = R.drawable.poster, component = LocalImageComponent.current, - imageOptions = ImageOptions(testTag = "FrescoImage"), + imageOptions = ImageOptions(tag = "FrescoImage"), ) } diff --git a/benchmark-landscapist-app/src/main/kotlin/com/skydoves/benchmark/landscapist/app/GlideImageProfiles.kt b/benchmark-landscapist-app/src/main/kotlin/com/skydoves/benchmark/landscapist/app/GlideImageProfiles.kt index 0290d625..f726b8e6 100644 --- a/benchmark-landscapist-app/src/main/kotlin/com/skydoves/benchmark/landscapist/app/GlideImageProfiles.kt +++ b/benchmark-landscapist-app/src/main/kotlin/com/skydoves/benchmark/landscapist/app/GlideImageProfiles.kt @@ -33,6 +33,6 @@ fun GlideImageProfiles() { }, previewPlaceholder = R.drawable.poster, component = LocalImageComponent.current, - imageOptions = ImageOptions(testTag = "GlideImage"), + imageOptions = ImageOptions(tag = "GlideImage"), ) } diff --git a/benchmark-landscapist-app/src/main/kotlin/com/skydoves/benchmark/landscapist/app/MainActivity.kt b/benchmark-landscapist-app/src/main/kotlin/com/skydoves/benchmark/landscapist/app/MainActivity.kt index 7acc06af..09cb9a0a 100644 --- a/benchmark-landscapist-app/src/main/kotlin/com/skydoves/benchmark/landscapist/app/MainActivity.kt +++ b/benchmark-landscapist-app/src/main/kotlin/com/skydoves/benchmark/landscapist/app/MainActivity.kt @@ -58,6 +58,7 @@ class MainActivity : ComponentActivity() { }, ) { CoilImageProfiles() + Coil3ImageProfiles() GlideImageProfiles() FrescoImageProfiles() FrescoWebSupportProfiles() diff --git a/coil/src/main/kotlin/com/skydoves/landscapist/coil/CoilImage.kt b/coil/src/main/kotlin/com/skydoves/landscapist/coil/CoilImage.kt index cea0607e..b51480a7 100644 --- a/coil/src/main/kotlin/com/skydoves/landscapist/coil/CoilImage.kt +++ b/coil/src/main/kotlin/com/skydoves/landscapist/coil/CoilImage.kt @@ -268,7 +268,7 @@ public fun CoilImage( imageOptions.LandscapistImage( modifier = Modifier .constraint(this) - .testTag(imageOptions.testTag), + .testTag(imageOptions.tag), painter = painter, ) } diff --git a/coil3/.gitignore b/coil3/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/coil3/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/coil3/api/android/coil3.api b/coil3/api/android/coil3.api new file mode 100644 index 00000000..beb94b08 --- /dev/null +++ b/coil3/api/android/coil3.api @@ -0,0 +1,75 @@ +public final class com/skydoves/landscapist/coil3/CoilImageKt { + public static final fun CoilImage (Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lcom/skydoves/landscapist/components/ImageComponent;Lcom/skydoves/landscapist/ImageOptions;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/graphics/painter/Painter;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V + public static final fun CoilImage (Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lcom/skydoves/landscapist/components/ImageComponent;Lkotlin/jvm/functions/Function0;Lcom/skydoves/landscapist/ImageOptions;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/graphics/painter/Painter;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;III)V +} + +public abstract class com/skydoves/landscapist/coil3/CoilImageState : com/skydoves/landscapist/ImageState { + public static final field $stable I +} + +public final class com/skydoves/landscapist/coil3/CoilImageState$Failure : com/skydoves/landscapist/coil3/CoilImageState { + public static final field $stable I + public fun (Lcoil3/Image;Ljava/lang/Throwable;)V + public final fun component1 ()Lcoil3/Image; + public final fun component2 ()Ljava/lang/Throwable; + public final fun copy (Lcoil3/Image;Ljava/lang/Throwable;)Lcom/skydoves/landscapist/coil3/CoilImageState$Failure; + public static synthetic fun copy$default (Lcom/skydoves/landscapist/coil3/CoilImageState$Failure;Lcoil3/Image;Ljava/lang/Throwable;ILjava/lang/Object;)Lcom/skydoves/landscapist/coil3/CoilImageState$Failure; + public fun equals (Ljava/lang/Object;)Z + public final fun getErrorImage ()Lcoil3/Image; + public final fun getReason ()Ljava/lang/Throwable; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/skydoves/landscapist/coil3/CoilImageState$Loading : com/skydoves/landscapist/coil3/CoilImageState { + public static final field $stable I + public static final field INSTANCE Lcom/skydoves/landscapist/coil3/CoilImageState$Loading; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/skydoves/landscapist/coil3/CoilImageState$None : com/skydoves/landscapist/coil3/CoilImageState { + public static final field $stable I + public static final field INSTANCE Lcom/skydoves/landscapist/coil3/CoilImageState$None; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/skydoves/landscapist/coil3/CoilImageState$Success : com/skydoves/landscapist/coil3/CoilImageState { + public static final field $stable I + public fun (Lcoil3/Image;Lcom/skydoves/landscapist/DataSource;)V + public final fun component1 ()Lcoil3/Image; + public final fun component2 ()Lcom/skydoves/landscapist/DataSource; + public final fun copy (Lcoil3/Image;Lcom/skydoves/landscapist/DataSource;)Lcom/skydoves/landscapist/coil3/CoilImageState$Success; + public static synthetic fun copy$default (Lcom/skydoves/landscapist/coil3/CoilImageState$Success;Lcoil3/Image;Lcom/skydoves/landscapist/DataSource;ILjava/lang/Object;)Lcom/skydoves/landscapist/coil3/CoilImageState$Success; + public fun equals (Ljava/lang/Object;)Z + public final fun getDataSource ()Lcom/skydoves/landscapist/DataSource; + public final fun getImage ()Lcoil3/Image; + public final fun getImageBitmap (Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/graphics/ImageBitmap; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/skydoves/landscapist/coil3/CoilImageStateKt { + public static final fun toCoilImageState (Lcom/skydoves/landscapist/ImageLoadState;)Lcom/skydoves/landscapist/coil3/CoilImageState; +} + +public final class com/skydoves/landscapist/coil3/ComposableSingletons$CoilImageKt { + public static final field INSTANCE Lcom/skydoves/landscapist/coil3/ComposableSingletons$CoilImageKt; + public static field lambda-1 Lkotlin/jvm/functions/Function3; + public static field lambda-2 Lkotlin/jvm/functions/Function3; + public fun ()V + public final fun getLambda-1$coil3_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-2$coil3_release ()Lkotlin/jvm/functions/Function3; +} + +public final class com/skydoves/landscapist/coil3/LocalCoilProviderKt { + public static final fun getLocalCoilImageLoader ()Landroidx/compose/runtime/ProvidableCompositionLocal; +} + +public final class com/skydoves/landscapist/coil3/RememberCoilImageStateKt { + public static final fun rememberCoilImageState (Lcom/skydoves/landscapist/coil3/CoilImageState;Ljava/lang/Object;Landroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/MutableState; +} + diff --git a/coil3/api/desktop/coil3.api b/coil3/api/desktop/coil3.api new file mode 100644 index 00000000..1ad2276f --- /dev/null +++ b/coil3/api/desktop/coil3.api @@ -0,0 +1,75 @@ +public final class com/skydoves/landscapist/coil3/CoilImageKt { + public static final fun CoilImage (Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lcom/skydoves/landscapist/components/ImageComponent;Lcom/skydoves/landscapist/ImageOptions;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/graphics/painter/Painter;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V + public static final fun CoilImage (Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lcom/skydoves/landscapist/components/ImageComponent;Lkotlin/jvm/functions/Function0;Lcom/skydoves/landscapist/ImageOptions;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/graphics/painter/Painter;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;III)V +} + +public abstract class com/skydoves/landscapist/coil3/CoilImageState : com/skydoves/landscapist/ImageState { + public static final field $stable I +} + +public final class com/skydoves/landscapist/coil3/CoilImageState$Failure : com/skydoves/landscapist/coil3/CoilImageState { + public static final field $stable I + public fun (Lcoil3/Image;Ljava/lang/Throwable;)V + public final fun component1 ()Lcoil3/Image; + public final fun component2 ()Ljava/lang/Throwable; + public final fun copy (Lcoil3/Image;Ljava/lang/Throwable;)Lcom/skydoves/landscapist/coil3/CoilImageState$Failure; + public static synthetic fun copy$default (Lcom/skydoves/landscapist/coil3/CoilImageState$Failure;Lcoil3/Image;Ljava/lang/Throwable;ILjava/lang/Object;)Lcom/skydoves/landscapist/coil3/CoilImageState$Failure; + public fun equals (Ljava/lang/Object;)Z + public final fun getErrorImage ()Lcoil3/Image; + public final fun getReason ()Ljava/lang/Throwable; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/skydoves/landscapist/coil3/CoilImageState$Loading : com/skydoves/landscapist/coil3/CoilImageState { + public static final field $stable I + public static final field INSTANCE Lcom/skydoves/landscapist/coil3/CoilImageState$Loading; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/skydoves/landscapist/coil3/CoilImageState$None : com/skydoves/landscapist/coil3/CoilImageState { + public static final field $stable I + public static final field INSTANCE Lcom/skydoves/landscapist/coil3/CoilImageState$None; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/skydoves/landscapist/coil3/CoilImageState$Success : com/skydoves/landscapist/coil3/CoilImageState { + public static final field $stable I + public fun (Lcoil3/Image;Lcom/skydoves/landscapist/DataSource;)V + public final fun component1 ()Lcoil3/Image; + public final fun component2 ()Lcom/skydoves/landscapist/DataSource; + public final fun copy (Lcoil3/Image;Lcom/skydoves/landscapist/DataSource;)Lcom/skydoves/landscapist/coil3/CoilImageState$Success; + public static synthetic fun copy$default (Lcom/skydoves/landscapist/coil3/CoilImageState$Success;Lcoil3/Image;Lcom/skydoves/landscapist/DataSource;ILjava/lang/Object;)Lcom/skydoves/landscapist/coil3/CoilImageState$Success; + public fun equals (Ljava/lang/Object;)Z + public final fun getDataSource ()Lcom/skydoves/landscapist/DataSource; + public final fun getImage ()Lcoil3/Image; + public final fun getImageBitmap (Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/graphics/ImageBitmap; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/skydoves/landscapist/coil3/CoilImageStateKt { + public static final fun toCoilImageState (Lcom/skydoves/landscapist/ImageLoadState;)Lcom/skydoves/landscapist/coil3/CoilImageState; +} + +public final class com/skydoves/landscapist/coil3/ComposableSingletons$CoilImageKt { + public static final field INSTANCE Lcom/skydoves/landscapist/coil3/ComposableSingletons$CoilImageKt; + public static field lambda-1 Lkotlin/jvm/functions/Function3; + public static field lambda-2 Lkotlin/jvm/functions/Function3; + public fun ()V + public final fun getLambda-1$coil3 ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-2$coil3 ()Lkotlin/jvm/functions/Function3; +} + +public final class com/skydoves/landscapist/coil3/LocalCoilProviderKt { + public static final fun getLocalCoilImageLoader ()Landroidx/compose/runtime/ProvidableCompositionLocal; +} + +public final class com/skydoves/landscapist/coil3/RememberCoilImageStateKt { + public static final fun rememberCoilImageState (Lcom/skydoves/landscapist/coil3/CoilImageState;Ljava/lang/Object;Landroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/MutableState; +} + diff --git a/coil3/build.gradle.kts b/coil3/build.gradle.kts new file mode 100644 index 00000000..762151ed --- /dev/null +++ b/coil3/build.gradle.kts @@ -0,0 +1,119 @@ +/* + * Designed and developed by 2020-2023 skydoves (Jaewoong Eum) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import com.github.skydoves.landscapist.Configuration + +plugins { + id("landscapist.library.compose.multiplatform") + id("landscapist.spotless") +} + +apply(from = "${rootDir}/scripts/publish-module.gradle.kts") + +mavenPublishing { + val artifactId = "landscapist-coil3" + coordinates( + Configuration.artifactGroup, + artifactId, + rootProject.extra.get("libVersion").toString() + ) + + pom { + name.set(artifactId) + } +} + +kotlin { + sourceSets { + all { + languageSettings.optIn("kotlin.RequiresOptIn") + languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi") + languageSettings.optIn("com.skydoves.landscapist.InternalLandscapistApi") + languageSettings.optIn("coil3.annotation.ExperimentalCoilApi") + } + val commonMain by getting { + dependencies { + api(project(":landscapist")) + api(libs.coil3) + api(libs.coil3.network) + api(libs.ktor.core) + + implementation(compose.ui) + implementation(compose.runtime) + implementation(compose.foundation) + @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) + implementation(compose.components.resources) + } + } + + val androidMain by getting { + dependencies { + implementation(libs.androidx.core.ktx) + } + } + + val jvmMain by getting { + dependencies { + api(libs.ktor.okhttp) + } + } + + val iosMain by getting { + dependencies { + api(libs.ktor.engine.darwin) + } + } + } + + targets.configureEach { + compilations.configureEach { + compilerOptions.configure { + // https://youtrack.jetbrains.com/issue/KT-61573 + freeCompilerArgs.add("-Xexpect-actual-classes") + } + } + } +} + +android { + namespace = "com.skydoves.landscapist.coil3" + compileSdk = Configuration.compileSdk + + defaultConfig { + minSdk = Configuration.minSdk + consumerProguardFiles("consumer-rules.pro") + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } +} + +baselineProfile { + baselineProfileOutputDir = "." + filter { + include("com.skydoves.landscapist.coil3.**") + } +} + +tasks.withType { + kotlinOptions { + jvmTarget = "1.8" + freeCompilerArgs += listOf( + "-Xexplicit-api=strict", + "-opt-in=kotlin.RequiresOptIn", + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", + "-opt-in=com.skydoves.landscapist.InternalLandscapistApi", + "-opt-in=coil3.annotation.ExperimentalCoilApi", + ) + } +} diff --git a/coil3/consumer-rules.pro b/coil3/consumer-rules.pro new file mode 100644 index 00000000..835d0ba6 --- /dev/null +++ b/coil3/consumer-rules.pro @@ -0,0 +1,2 @@ +# R8 full mode strips signatures from non-kept items. +-dontwarn org.slf4j.impl.StaticLoggerBinder diff --git a/coil3/src/androidMain/AndroidManifest.xml b/coil3/src/androidMain/AndroidManifest.xml new file mode 100644 index 00000000..31ad153b --- /dev/null +++ b/coil3/src/androidMain/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/coil3/src/androidMain/kotlin/com/skydoves/landscapist/coil3/Platforms.kt b/coil3/src/androidMain/kotlin/com/skydoves/landscapist/coil3/Platforms.kt new file mode 100644 index 00000000..bdf12ca0 --- /dev/null +++ b/coil3/src/androidMain/kotlin/com/skydoves/landscapist/coil3/Platforms.kt @@ -0,0 +1,68 @@ +/* + * Designed and developed by 2020-2023 skydoves (Jaewoong Eum) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:OptIn(ExperimentalCoilApi::class) + +package com.skydoves.landscapist.coil3 + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.core.graphics.drawable.toBitmap +import coil3.Image +import coil3.ImageLoader +import coil3.PlatformContext +import coil3.annotation.ExperimentalCoilApi +import coil3.imageLoader +import coil3.request.ImageRequest +import coil3.request.lifecycle +import com.skydoves.landscapist.plugins.ImagePlugin +import com.skydoves.landscapist.rememberDrawablePainter + +internal actual fun getPlatform(): Platform = Platform.Android + +@Composable +internal actual fun Image.toImageBitmap(): ImageBitmap { + val context = LocalContext.current + return asDrawable(context.resources).toBitmap().asImageBitmap() +} + +internal actual val platformContext: PlatformContext + @Composable get() = LocalContext.current + +internal actual val platformImageLoader: ImageLoader + @Composable get() = platformContext.imageLoader + +@Composable +internal actual fun buildImageRequest( + data: Any?, + requestListener: ImageRequest.Listener?, +): ImageRequest { + val lifecycleOwner = LocalLifecycleOwner.current + return ImageRequest.Builder(platformContext) + .data(data) + .listener(requestListener) + .lifecycle(lifecycleOwner) + .build() +} + +@Composable +internal actual fun rememberImagePainter(image: Image, imagePlugins: List): Painter { + val resource = platformContext.resources + return rememberDrawablePainter(drawable = image.asDrawable(resource), imagePlugins = imagePlugins) +} diff --git a/coil3/src/commonMain/AndroidManifest.xml b/coil3/src/commonMain/AndroidManifest.xml new file mode 100644 index 00000000..126ab54c --- /dev/null +++ b/coil3/src/commonMain/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/coil3/src/commonMain/kotlin/com/skydoves/landscapist/coil3/CoilImage.kt b/coil3/src/commonMain/kotlin/com/skydoves/landscapist/coil3/CoilImage.kt new file mode 100644 index 00000000..56352d56 --- /dev/null +++ b/coil3/src/commonMain/kotlin/com/skydoves/landscapist/coil3/CoilImage.kt @@ -0,0 +1,386 @@ +/* + * Designed and developed by 2020-2023 skydoves (Jaewoong Eum) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:Suppress("unused") + +package com.skydoves.landscapist.coil3 + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.BoxWithConstraintsScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.IntSize +import coil3.ImageLoader +import coil3.request.ImageRequest +import coil3.request.ImageResult +import coil3.size.SizeResolver +import com.skydoves.landscapist.DataSource +import com.skydoves.landscapist.ImageLoad +import com.skydoves.landscapist.ImageLoadState +import com.skydoves.landscapist.ImageOptions +import com.skydoves.landscapist.LandscapistImage +import com.skydoves.landscapist.StableHolder +import com.skydoves.landscapist.components.ComposeFailureStatePlugins +import com.skydoves.landscapist.components.ComposeLoadingStatePlugins +import com.skydoves.landscapist.components.ComposeSuccessStatePlugins +import com.skydoves.landscapist.components.ImageComponent +import com.skydoves.landscapist.components.imagePlugins +import com.skydoves.landscapist.components.rememberImageComponent +import com.skydoves.landscapist.constraints.Constrainable +import com.skydoves.landscapist.constraints.constraint +import com.skydoves.landscapist.plugins.ImagePlugin +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.flow.channelFlow + +/** + * Load and render an image with the given [imageModel] from the network or local storage. + * + * Supported types for the [imageModel] are the below: + * + * ``` + * CoilImage( + * imageModel = { imageModel }, + * modifier = modifier, + * imageOptions = ImageOptions(contentScale = ContentScale.Crop), + * loading = { + * Box(modifier = Modifier.matchParentSize()) { + * CircularProgressIndicator( + * modifier = Modifier.align(Alignment.Center) + * ) + * } + * }, + * failure = { + * Text(text = "image request failed.") + * }) + * ``` + * + * @param imageModel The data model to request image. See [ImageRequest.Builder.data] for types allowed. + * @param modifier [Modifier] used to adjust the layout or drawing content. + * @param imageLoader The [ImageLoader] to use when requesting the image. + * @param component An image component that conjuncts pluggable [ImagePlugin]s. + * @param requestListener A class for monitoring the status of a request while images load. + * @param imageOptions Represents parameters to load generic [Image] Composable. + * @param onImageStateChanged An image state change listener will be triggered whenever the image state is changed. + * @param previewPlaceholder Drawable resource ID which will be displayed when this function is ran in preview mode. + * @param loading Content to be displayed when the request is in progress. + * @param success Content to be displayed when the request is succeeded. + * @param failure Content to be displayed when the request is failed. + */ +@Composable +public fun CoilImage( + imageModel: () -> Any?, + modifier: Modifier = Modifier, + imageLoader: @Composable () -> ImageLoader = { LocalCoilProvider.getCoilImageLoader() }, + component: ImageComponent = rememberImageComponent {}, + requestListener: (() -> ImageRequest.Listener)? = null, + imageOptions: ImageOptions = ImageOptions(), + onImageStateChanged: (CoilImageState) -> Unit = {}, + previewPlaceholder: Painter? = null, + loading: @Composable (BoxScope.(imageState: CoilImageState.Loading) -> Unit)? = null, + success: @Composable ( + BoxScope.( + imageState: CoilImageState.Success, + painter: Painter, + ) -> Unit + )? = null, + failure: @Composable (BoxScope.(imageState: CoilImageState.Failure) -> Unit)? = null, +) { + val imageRequest = + buildImageRequest(data = imageModel.invoke(), requestListener = requestListener?.invoke()) + CoilImage( + imageRequest = { imageRequest }, + imageLoader = imageLoader, + component = component, + modifier = modifier, + imageOptions = imageOptions, + onImageStateChanged = onImageStateChanged, + previewPlaceholder = previewPlaceholder, + loading = loading, + success = success, + failure = failure, + ) +} + +/** + * Load and render an image with the given [imageRequest] from the network or local storage. + * + * ``` + * CoilImage( + * imageRequest = { + * ImageRequest.Builder(context) + * .data(imageModel) + * .lifecycle(lifecycleOwner) + * .build() + * }, + * modifier = modifier, + * loading = { + * Box(modifier = Modifier.matchParentSize()) { + * CircularProgressIndicator( + * modifier = Modifier.align(Alignment.Center) + * ) + * } + * }, + * failure = { + * Text(text = "image request failed.") + * }) + * ``` + * + * @param imageRequest The request to execute. + * @param modifier [Modifier] used to adjust the layout or drawing content. + * @param imageLoader The [ImageLoader] to use when requesting the image. + * @param component An image component that conjuncts pluggable [ImagePlugin]s. + * @param imageOptions Represents parameters to load generic [Image] Composable. + * @param onImageStateChanged An image state change listener will be triggered whenever the image state is changed. + * @param previewPlaceholder Drawable resource ID which will be displayed when this function is ran in preview mode. + * @param loading Content to be displayed when the request is in progress. + * @param success Content to be displayed when the request is succeeded. + * @param failure Content to be displayed when the request is failed. + */ +@Composable +public fun CoilImage( + imageRequest: () -> ImageRequest, + modifier: Modifier = Modifier, + imageLoader: @Composable () -> ImageLoader = { LocalCoilProvider.getCoilImageLoader() }, + component: ImageComponent = rememberImageComponent {}, + imageOptions: ImageOptions = ImageOptions(), + onImageStateChanged: (CoilImageState) -> Unit = {}, + previewPlaceholder: Painter? = null, + loading: @Composable (BoxScope.(imageState: CoilImageState.Loading) -> Unit)? = null, + success: @Composable ( + BoxScope.( + imageState: CoilImageState.Success, + painter: Painter, + ) -> Unit + )? = null, + failure: @Composable (BoxScope.(imageState: CoilImageState.Failure) -> Unit)? = null, +) { + if (LocalInspectionMode.current && previewPlaceholder != null) { + with(imageOptions) { + Image( + modifier = modifier, + painter = previewPlaceholder, + alignment = alignment, + contentScale = contentScale, + alpha = alpha, + colorFilter = colorFilter, + contentDescription = contentDescription, + ) + return + } + } + + CoilImage( + recomposeKey = StableHolder(imageRequest.invoke()), + imageLoader = StableHolder(imageLoader.invoke()), + imageOptions = imageOptions, + modifier = modifier, + ) ImageRequest@{ imageState -> + when ( + val coilImageState = imageState.toCoilImageState().apply { + onImageStateChanged.invoke(this) + } + ) { + is CoilImageState.None -> Unit + + is CoilImageState.Loading -> { + component.ComposeLoadingStatePlugins( + modifier = Modifier.constraint(this), + imageOptions = imageOptions, + executor = { size -> + CoilThumbnail( + requestSize = size, + recomposeKey = StableHolder(imageRequest.invoke()), + imageLoader = StableHolder(imageLoader.invoke()), + imageOptions = imageOptions, + ) + }, + ) + loading?.invoke(this, coilImageState) + } + + is CoilImageState.Failure -> { + component.ComposeFailureStatePlugins( + modifier = Modifier.constraint(this), + imageOptions = imageOptions, + reason = coilImageState.reason, + ) + failure?.invoke(this, coilImageState) + } + + is CoilImageState.Success -> { + component.ComposeSuccessStatePlugins( + modifier = Modifier.constraint(this), + imageModel = imageRequest.invoke().data, + imageOptions = imageOptions, + imageBitmap = coilImageState.imageBitmap, + ) + + val image = coilImageState.image ?: return@ImageRequest + val painter = rememberImagePainter(image = image, imagePlugins = component.imagePlugins) + + if (success != null) { + success.invoke(this, coilImageState, painter) + } else { + imageOptions.LandscapistImage( + modifier = Modifier + .constraint(this) + .testTag(imageOptions.tag), + painter = painter, + ) + } + } + } + } +} + +/** + * Requests loading an image and create a composable that provides the current state [ImageLoadState] of the content. + * + * ``` + * CoilImage( + * recomposeKey = ImageRequest.Builder(context) + * .data(imageModel) + * .lifecycle(lifecycleOwner) + * .build(), + * modifier = modifier, + * ) { imageState -> + * when (val coilImageState = imageState.toCoilImageState()) { + * is CoilImageState.None -> // do something + * is CoilImageState.Loading -> // do something + * is CoilImageState.Failure -> // do something + * is CoilImageState.Success -> // do something + * } + * } + * ``` + * + * @param recomposeKey The request to execute. + * @param modifier [Modifier] used to adjust the layout or drawing content. + * @param imageOptions Represents parameters to load generic [Image] Composable. + * @param imageLoader The [ImageLoader] to use when requesting the image. + * @param content Content to be displayed for the given state. + */ +@Composable +private fun CoilImage( + recomposeKey: StableHolder, + modifier: Modifier = Modifier, + imageOptions: ImageOptions, + imageLoader: StableHolder = StableHolder(LocalCoilProvider.getCoilImageLoader()), + content: @Composable BoxWithConstraintsScope.(imageState: ImageLoadState) -> Unit, +) { + val context = platformContext + val request = rememberRequestWithConstraints( + request = recomposeKey.value, + imageOptions = imageOptions, + ) + val constrainable: Constrainable? = + remember(recomposeKey, imageOptions) { (request.sizeResolver) as? Constrainable } + + ImageLoad( + recomposeKey = recomposeKey.value, + constrainable = constrainable, + executeImageRequest = { + channelFlow { + val newBuilder = request.newBuilder(context).target( + onStart = { trySendBlocking(ImageLoadState.Loading) }, + ).build() + + val result = imageLoader.value.execute(newBuilder).toResult() + send(result) + } + }, + imageOptions = imageOptions, + modifier = modifier, + content = content, + ) +} + +@Composable +private fun CoilThumbnail( + requestSize: IntSize, + recomposeKey: StableHolder, + imageOptions: ImageOptions, + imageLoader: StableHolder = StableHolder(LocalCoilProvider.getCoilImageLoader()), +) { + CoilImage( + recomposeKey = recomposeKey, + imageLoader = imageLoader, + imageOptions = imageOptions.copy(requestSize = requestSize), + ) ImageRequest@{ imageState -> + val coilImageState = imageState.toCoilImageState() + if (coilImageState is CoilImageState.Success) { + val image = coilImageState.image ?: return@ImageRequest + val painter = rememberImagePainter(image = image, imagePlugins = emptyList()) + imageOptions.LandscapistImage( + modifier = Modifier, + painter = painter, + ) + } + } +} + +private fun ImageResult.toResult(): ImageLoadState = when (this) { + is coil3.request.SuccessResult -> { + ImageLoadState.Success( + data = image, + dataSource = dataSource.toDataSource(), + ) + } + + is coil3.request.ErrorResult -> { + ImageLoadState.Failure( + data = image, + reason = throwable, + ) + } +} + +private fun coil3.decode.DataSource.toDataSource(): DataSource = when (this) { + coil3.decode.DataSource.NETWORK -> DataSource.NETWORK + coil3.decode.DataSource.MEMORY -> DataSource.MEMORY + coil3.decode.DataSource.MEMORY_CACHE -> DataSource.MEMORY + coil3.decode.DataSource.DISK -> DataSource.DISK +} + +@Composable +internal fun rememberRequestWithConstraints( + request: ImageRequest, + imageOptions: ImageOptions, +): ImageRequest { + return remember(request, imageOptions) { + if (request.defined.sizeResolver == null) { + val sizeResolver = if (imageOptions.isValidSize) { + SizeResolver( + coil3.size.Size( + width = imageOptions.requestSize.width, + height = imageOptions.requestSize.height, + ), + ) + } else if (imageOptions.contentScale == ContentScale.None) { + SizeResolver(coil3.size.Size.ORIGINAL) + } else { + ConstraintsSizeResolver() + } + request.newBuilder().size(sizeResolver).build() + } else { + request + } + } +} diff --git a/coil3/src/commonMain/kotlin/com/skydoves/landscapist/coil3/CoilImageState.kt b/coil3/src/commonMain/kotlin/com/skydoves/landscapist/coil3/CoilImageState.kt new file mode 100644 index 00000000..82620533 --- /dev/null +++ b/coil3/src/commonMain/kotlin/com/skydoves/landscapist/coil3/CoilImageState.kt @@ -0,0 +1,68 @@ +/* + * Designed and developed by 2020-2023 skydoves (Jaewoong Eum) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.skydoves.landscapist.coil3 + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.ImageBitmap +import coil3.Image +import com.skydoves.landscapist.DataSource +import com.skydoves.landscapist.ImageLoadState +import com.skydoves.landscapist.ImageState + +/** GlideImageState represents the image loading states for Coil. */ +@Immutable +public sealed class CoilImageState : ImageState { + + /** Request not started. */ + @Immutable + public data object None : CoilImageState() + + /** Request is currently in progress. */ + @Immutable + public data object Loading : CoilImageState() + + /** Request is completed successfully and ready to use an [ImageBitmap]. */ + @Immutable + public data class Success(val image: Image?, val dataSource: DataSource) : + CoilImageState() { + + /** Get [ImageBitmap] from the succeed image [drawable]. */ + public val imageBitmap: ImageBitmap? + @Composable get() = image?.toImageBitmap() + } + + /** Request failed. */ + @Immutable + public data class Failure(val errorImage: Image?, val reason: Throwable?) : CoilImageState() +} + +/** casts an [ImageLoadState] type to a [CoilImageState]. */ +public fun ImageLoadState.toCoilImageState(): CoilImageState { + return when (this) { + is ImageLoadState.None -> CoilImageState.None + is ImageLoadState.Loading -> CoilImageState.Loading + is ImageLoadState.Success -> CoilImageState.Success( + image = data as? Image, + dataSource = dataSource, + ) + + is ImageLoadState.Failure -> CoilImageState.Failure( + errorImage = data as? Image, + reason = reason, + ) + } +} diff --git a/coil3/src/commonMain/kotlin/com/skydoves/landscapist/coil3/ConstraintsSizeResolver.kt b/coil3/src/commonMain/kotlin/com/skydoves/landscapist/coil3/ConstraintsSizeResolver.kt new file mode 100644 index 00000000..45075556 --- /dev/null +++ b/coil3/src/commonMain/kotlin/com/skydoves/landscapist/coil3/ConstraintsSizeResolver.kt @@ -0,0 +1,44 @@ +/* + * Designed and developed by 2020-2023 skydoves (Jaewoong Eum) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.skydoves.landscapist.coil3 + +import androidx.compose.ui.unit.Constraints +import coil3.size.Dimension +import coil3.size.Size +import com.skydoves.landscapist.ZeroConstraints +import com.skydoves.landscapist.constraints.Constrainable +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.mapNotNull + +internal class ConstraintsSizeResolver : Constrainable, coil3.size.SizeResolver { + + private val _constraints = MutableStateFlow(ZeroConstraints) + + override suspend fun size() = _constraints.mapNotNull(Constraints::inferredCoilSize).first() + + override fun setConstraints(constraints: Constraints) { + _constraints.value = constraints + } +} + +private fun Constraints.inferredCoilSize(): Size? = when { + isZero -> null + else -> Size( + width = if (hasBoundedWidth) Dimension(maxWidth) else Dimension.Undefined, + height = if (hasBoundedHeight) Dimension(maxHeight) else Dimension.Undefined, + ) +} diff --git a/coil3/src/commonMain/kotlin/com/skydoves/landscapist/coil3/LocalCoilProvider.kt b/coil3/src/commonMain/kotlin/com/skydoves/landscapist/coil3/LocalCoilProvider.kt new file mode 100644 index 00000000..6a0b04c5 --- /dev/null +++ b/coil3/src/commonMain/kotlin/com/skydoves/landscapist/coil3/LocalCoilProvider.kt @@ -0,0 +1,51 @@ +/* + * Designed and developed by 2020-2023 skydoves (Jaewoong Eum) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.skydoves.landscapist.coil3 + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.staticCompositionLocalOf +import coil3.ImageLoader +import coil3.SingletonImageLoader +import coil3.fetch.NetworkFetcher + +/** + * Local containing the preferred [ImageLoader] for providing the same instance + * in our composable hierarchy. + */ +public val LocalCoilImageLoader: ProvidableCompositionLocal = + staticCompositionLocalOf { null } + +/** A provider for taking the local instances related to the `CoilImage`. */ +internal object LocalCoilProvider { + + /** Returns the current or default [ImageLoader] for the `ColiImage` parameter. */ + @Composable + fun getCoilImageLoader(): ImageLoader { + return LocalCoilImageLoader.current ?: let { + if (getPlatform() == Platform.NonAndroid) { + val defaultImageLoader = ImageLoader.Builder(platformContext) + .components { + add(NetworkFetcher.Factory()) + } + .build() + SingletonImageLoader.setSafe { defaultImageLoader } + } + + platformImageLoader + } + } +} diff --git a/coil3/src/commonMain/kotlin/com/skydoves/landscapist/coil3/Platforms.kt b/coil3/src/commonMain/kotlin/com/skydoves/landscapist/coil3/Platforms.kt new file mode 100644 index 00000000..e43d3a61 --- /dev/null +++ b/coil3/src/commonMain/kotlin/com/skydoves/landscapist/coil3/Platforms.kt @@ -0,0 +1,51 @@ +/* + * Designed and developed by 2020-2023 skydoves (Jaewoong Eum) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.skydoves.landscapist.coil3 + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.painter.Painter +import coil3.Image +import coil3.ImageLoader +import coil3.PlatformContext +import coil3.request.ImageRequest +import com.skydoves.landscapist.plugins.ImagePlugin + +internal enum class Platform { + Android, NonAndroid +} + +internal expect fun getPlatform(): Platform + +/** Transforms coil's [Image] to Compose [ImageBitmap]. */ +@Composable +internal expect fun Image.toImageBitmap(): ImageBitmap + +/** Returns coil's platform context. */ +internal expect val platformContext: PlatformContext + +/** Returns [ImageLoader] from the platform context. */ +internal expect val platformImageLoader: ImageLoader + +/** Builds an [ImageRequest] depending on its target platform. */ +@Composable +internal expect fun buildImageRequest( + data: Any?, + requestListener: ImageRequest.Listener?, +): ImageRequest + +@Composable +internal expect fun rememberImagePainter(image: Image, imagePlugins: List): Painter diff --git a/coil3/src/commonMain/kotlin/com/skydoves/landscapist/coil3/RememberCoilImageState.kt b/coil3/src/commonMain/kotlin/com/skydoves/landscapist/coil3/RememberCoilImageState.kt new file mode 100644 index 00000000..7b0507b1 --- /dev/null +++ b/coil3/src/commonMain/kotlin/com/skydoves/landscapist/coil3/RememberCoilImageState.kt @@ -0,0 +1,33 @@ +/* + * Designed and developed by 2020-2023 skydoves (Jaewoong Eum) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.skydoves.landscapist.coil3 + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember + +/** + * Create and remember [CoilImageState]. + * + * @param initialState The initial state of [CoilImageState]. + * @param key The key that may trigger recomposition. + */ +@Composable +public fun rememberCoilImageState( + initialState: CoilImageState = CoilImageState.None, + key: Any? = null, +): MutableState = remember(key1 = key) { mutableStateOf(initialState) } diff --git a/coil3/src/skiaMain/kotlin/com/skydoves/landscapist/coil3/Platforms.kt b/coil3/src/skiaMain/kotlin/com/skydoves/landscapist/coil3/Platforms.kt new file mode 100644 index 00000000..b2ccd4f0 --- /dev/null +++ b/coil3/src/skiaMain/kotlin/com/skydoves/landscapist/coil3/Platforms.kt @@ -0,0 +1,73 @@ +/* + * Designed and developed by 2020-2023 skydoves (Jaewoong Eum) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:OptIn(ExperimentalCoilApi::class) + +package com.skydoves.landscapist.coil3 + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asComposeImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.Painter +import coil3.Image +import coil3.ImageLoader +import coil3.PlatformContext +import coil3.SingletonImageLoader +import coil3.annotation.ExperimentalCoilApi +import coil3.request.ImageRequest +import com.skydoves.landscapist.plugins.ImagePlugin +import com.skydoves.landscapist.plugins.composePainterPlugins + +internal actual fun getPlatform(): Platform = Platform.NonAndroid + +@Composable +internal actual fun Image.toImageBitmap(): ImageBitmap { + return asBitmap().asComposeImageBitmap() +} + +internal actual val platformContext: PlatformContext + @Composable get() = PlatformContext.INSTANCE + +internal actual val platformImageLoader: ImageLoader + @Composable get() = SingletonImageLoader.get(platformContext) + +@Composable +internal actual fun buildImageRequest( + data: Any?, + requestListener: ImageRequest.Listener?, +): ImageRequest { + return ImageRequest.Builder(platformContext) + .data(data) + .listener(requestListener) + .build() +} + +@Composable +internal actual fun rememberImagePainter(image: Image, imagePlugins: List): Painter { + val bitmapPainter = bitmapPainter(image = image) + return remember(image, imagePlugins) { + bitmapPainter + }.composePainterPlugins( + imagePlugins = imagePlugins, + imageBitmap = image.toImageBitmap(), + ) +} + +@Composable +internal fun bitmapPainter(image: Image): Painter { + return BitmapPainter(image = image.asBitmap().asComposeImageBitmap()) +} diff --git a/fresco/src/main/kotlin/com/skydoves/landscapist/fresco/FrescoImage.kt b/fresco/src/main/kotlin/com/skydoves/landscapist/fresco/FrescoImage.kt index 22b4585e..bf96ff22 100644 --- a/fresco/src/main/kotlin/com/skydoves/landscapist/fresco/FrescoImage.kt +++ b/fresco/src/main/kotlin/com/skydoves/landscapist/fresco/FrescoImage.kt @@ -175,7 +175,7 @@ public fun FrescoImage( imageOptions.LandscapistImage( modifier = Modifier .constraint(this) - .testTag(imageOptions.testTag), + .testTag(imageOptions.tag), painter = painter, ) } diff --git a/glide/src/main/kotlin/com/skydoves/landscapist/glide/GlideImage.kt b/glide/src/main/kotlin/com/skydoves/landscapist/glide/GlideImage.kt index 6d1a85f8..3226ad86 100644 --- a/glide/src/main/kotlin/com/skydoves/landscapist/glide/GlideImage.kt +++ b/glide/src/main/kotlin/com/skydoves/landscapist/glide/GlideImage.kt @@ -214,7 +214,7 @@ public fun GlideImage( imageOptions.LandscapistImage( modifier = Modifier .constraint(this) - .testTag(imageOptions.testTag), + .testTag(imageOptions.tag), painter = painter, ) } diff --git a/gradle.properties b/gradle.properties index 16a2cf00..08c1d257 100644 --- a/gradle.properties +++ b/gradle.properties @@ -45,6 +45,7 @@ enableComposeCompilerReports=true kotlin.mpp.enableCInteropCommonization=true kotlin.mpp.stability.nowarn=true kotlin.mpp.androidSourceSetLayoutVersion=2 +kotlin.native.ignoreIncorrectDependencies=true kotlin.native.binary.memoryModel=experimental kotlin.native.cacheKind=none diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index be4263f2..c2543625 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,6 +22,8 @@ jetbrains-compose = "1.5.11" glide = "4.16.0" fresco = "3.1.3" coil = "2.5.0" +coil3 = "3.0.0-alpha01" +ktor = "3.0.0-wasm2" palette = "2.2.0" hilt = "2.50" spotless = "6.21.0" @@ -63,6 +65,12 @@ fresco-drawable = { group = "com.facebook.fresco", name = "animated-drawable", v fresco-websupport = { group = "com.facebook.fresco", name = "webpsupport", version.ref = "fresco" } coil = { group = "io.coil-kt", name = "coil", version.ref = "coil" } coil-gif = { group = "io.coil-kt", name = "coil-gif", version.ref = "coil" } +coil3 = { group = "io.coil-kt.coil3", name = "coil", version.ref = "coil3" } +coil3-network = { group = "io.coil-kt.coil3", name = "coil-network", version.ref = "coil3" } +ktor-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" } +ktor-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" } +ktor-engine-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } +ktor-engine-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" } # Dependencies of the included build-logic android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } diff --git a/landscapist-animation/build.gradle.kts b/landscapist-animation/build.gradle.kts index c02277fe..2547c852 100644 --- a/landscapist-animation/build.gradle.kts +++ b/landscapist-animation/build.gradle.kts @@ -58,6 +58,15 @@ kotlin { } } } + + targets.configureEach { + compilations.configureEach { + compilerOptions.configure { + // https://youtrack.jetbrains.com/issue/KT-61573 + freeCompilerArgs.add("-Xexpect-actual-classes") + } + } + } } android { diff --git a/landscapist-animation/src/skiaMain/kotlin/com/skydoves/landscapist/animation/circular/CircularRevealPainter.kt b/landscapist-animation/src/skiaMain/kotlin/com/skydoves/landscapist/animation/circular/CircularRevealPainter.kt index b511e3e2..8b180b87 100644 --- a/landscapist-animation/src/skiaMain/kotlin/com/skydoves/landscapist/animation/circular/CircularRevealPainter.kt +++ b/landscapist-animation/src/skiaMain/kotlin/com/skydoves/landscapist/animation/circular/CircularRevealPainter.kt @@ -73,7 +73,6 @@ internal actual class CircularRevealPainter actual constructor( scale = mDrawableRect.size.width / bitmapWidth.toFloat() dy = (mDrawableRect.size.height - bitmapHeight * scale) * 0.5f } - // resize the matrix to scale by sx and sy. val m1 = Matrix33.makeScale(scale, scale) val m2 = Matrix33.makeTranslate( diff --git a/landscapist-palette/build.gradle.kts b/landscapist-palette/build.gradle.kts index 6be7ccc8..a8c3a1b9 100644 --- a/landscapist-palette/build.gradle.kts +++ b/landscapist-palette/build.gradle.kts @@ -53,6 +53,15 @@ kotlin { } } } + + targets.configureEach { + compilations.configureEach { + compilerOptions.configure { + // https://youtrack.jetbrains.com/issue/KT-61573 + freeCompilerArgs.add("-Xexpect-actual-classes") + } + } + } } android { diff --git a/landscapist-placeholder/build.gradle.kts b/landscapist-placeholder/build.gradle.kts index 91226168..7d255547 100644 --- a/landscapist-placeholder/build.gradle.kts +++ b/landscapist-placeholder/build.gradle.kts @@ -58,6 +58,15 @@ kotlin { } } } + + targets.configureEach { + compilations.configureEach { + compilerOptions.configure { + // https://youtrack.jetbrains.com/issue/KT-61573 + freeCompilerArgs.add("-Xexpect-actual-classes") + } + } + } } android { diff --git a/landscapist/api/android/landscapist.api b/landscapist/api/android/landscapist.api index 07a2018d..1b2fff6c 100644 --- a/landscapist/api/android/landscapist.api +++ b/landscapist/api/android/landscapist.api @@ -90,7 +90,7 @@ public final class com/skydoves/landscapist/ImageOptions { public final fun getContentDescription ()Ljava/lang/String; public final fun getContentScale ()Landroidx/compose/ui/layout/ContentScale; public final fun getRequestSize-YbymL2g ()J - public final fun getTestTag ()Ljava/lang/String; + public final fun getTag ()Ljava/lang/String; public fun hashCode ()I public final fun isValidSize ()Z public fun toString ()Ljava/lang/String; @@ -189,3 +189,7 @@ public abstract interface class com/skydoves/landscapist/plugins/ImagePlugin$Suc public abstract fun compose (Landroidx/compose/ui/Modifier;Ljava/lang/Object;Lcom/skydoves/landscapist/ImageOptions;Landroidx/compose/ui/graphics/ImageBitmap;Landroidx/compose/runtime/Composer;I)Lcom/skydoves/landscapist/plugins/ImagePlugin; } +public final class com/skydoves/landscapist/plugins/ImagePluginKt { + public static final fun composePainterPlugins (Landroidx/compose/ui/graphics/painter/Painter;Ljava/util/List;Landroidx/compose/ui/graphics/ImageBitmap;Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/graphics/painter/Painter; +} + diff --git a/landscapist/api/desktop/landscapist.api b/landscapist/api/desktop/landscapist.api index c5ea6526..0577c8a9 100644 --- a/landscapist/api/desktop/landscapist.api +++ b/landscapist/api/desktop/landscapist.api @@ -86,7 +86,7 @@ public final class com/skydoves/landscapist/ImageOptions { public final fun getContentDescription ()Ljava/lang/String; public final fun getContentScale ()Landroidx/compose/ui/layout/ContentScale; public final fun getRequestSize-YbymL2g ()J - public final fun getTestTag ()Ljava/lang/String; + public final fun getTag ()Ljava/lang/String; public fun hashCode ()I public final fun isValidSize ()Z public fun toString ()Ljava/lang/String; @@ -181,3 +181,7 @@ public abstract interface class com/skydoves/landscapist/plugins/ImagePlugin$Suc public abstract fun compose (Landroidx/compose/ui/Modifier;Ljava/lang/Object;Lcom/skydoves/landscapist/ImageOptions;Landroidx/compose/ui/graphics/ImageBitmap;Landroidx/compose/runtime/Composer;I)Lcom/skydoves/landscapist/plugins/ImagePlugin; } +public final class com/skydoves/landscapist/plugins/ImagePluginKt { + public static final fun composePainterPlugins (Landroidx/compose/ui/graphics/painter/Painter;Ljava/util/List;Landroidx/compose/ui/graphics/ImageBitmap;Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/graphics/painter/Painter; +} + diff --git a/landscapist/src/commonMain/kotlin/com/skydoves/landscapist/ImageOptions.kt b/landscapist/src/commonMain/kotlin/com/skydoves/landscapist/ImageOptions.kt index 02d75102..589051e8 100644 --- a/landscapist/src/commonMain/kotlin/com/skydoves/landscapist/ImageOptions.kt +++ b/landscapist/src/commonMain/kotlin/com/skydoves/landscapist/ImageOptions.kt @@ -34,7 +34,7 @@ import androidx.compose.ui.unit.IntSize * @property colorFilter The colorFilter parameter used to apply for the image when it is rendered onscreen. * @property alpha The alpha parameter used to apply for the image when it is rendered onscreen. * @property requestSize The [IntSize] that will be used to request remote images. - * @property testTag A test tag option that will be applied to a success image's modifier. + * @property tag An optional tag feature can be utilized either to trigger recomposition or to attach a test tag to your image. */ @Immutable public data class ImageOptions( @@ -44,7 +44,7 @@ public data class ImageOptions( public val colorFilter: ColorFilter? = null, public val alpha: Float = DefaultAlpha, public val requestSize: IntSize = IntSize(DEFAULT_IMAGE_SIZE, DEFAULT_IMAGE_SIZE), - public val testTag: String = "", + public val tag: String = "", ) { /** Returns true if the [requestSize] is valid. */ public val isValidSize: Boolean diff --git a/landscapist/src/commonMain/kotlin/com/skydoves/landscapist/plugins/ImagePlugin.kt b/landscapist/src/commonMain/kotlin/com/skydoves/landscapist/plugins/ImagePlugin.kt index 40d1b779..15c40e78 100644 --- a/landscapist/src/commonMain/kotlin/com/skydoves/landscapist/plugins/ImagePlugin.kt +++ b/landscapist/src/commonMain/kotlin/com/skydoves/landscapist/plugins/ImagePlugin.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.unit.IntSize import com.skydoves.landscapist.ImageLoadState import com.skydoves.landscapist.ImageOptions +import com.skydoves.landscapist.InternalLandscapistApi /** * A pluggable compose interface that will be executed for loading images. @@ -87,7 +88,8 @@ public sealed interface ImagePlugin { * @param imageBitmap A target [imageBitmap] to be composed of the given [Painter] */ @Composable -internal fun Painter.composePainterPlugins( +@InternalLandscapistApi +public fun Painter.composePainterPlugins( imagePlugins: List, imageBitmap: ImageBitmap, ): Painter { diff --git a/settings.gradle.kts b/settings.gradle.kts index 0915f791..21de46d7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,4 @@ @file:Suppress("UnstableApiUsage") - pluginManagement { includeBuild("build-logic") repositories { @@ -8,6 +7,7 @@ pluginManagement { mavenCentral() maven(url = "https://plugins.gradle.org/m2/") maven(url = "https://maven.pkg.jetbrains.space/public/p/compose/dev") + maven("https://maven.pkg.jetbrains.space/kotlin/p/wasm/experimental") } } dependencyResolutionManagement { @@ -17,6 +17,7 @@ dependencyResolutionManagement { mavenCentral() maven(url = "https://plugins.gradle.org/m2/") maven(url = "https://maven.pkg.jetbrains.space/public/p/compose/dev") + maven("https://maven.pkg.jetbrains.space/kotlin/p/wasm/experimental") } } @@ -29,6 +30,7 @@ include(":landscapist-palette") include(":landscapist-placeholder") include(":landscapist-transformation") include(":coil") +include(":coil3") include(":glide") include(":fresco") include(":fresco-websupport")