diff --git a/landscapist-placeholder/api/android/landscapist-placeholder.api b/landscapist-placeholder/api/android/landscapist-placeholder.api new file mode 100644 index 00000000..cd10590f --- /dev/null +++ b/landscapist-placeholder/api/android/landscapist-placeholder.api @@ -0,0 +1,71 @@ +public abstract class com/skydoves/landscapist/placeholder/placeholder/PlaceholderPlugin { + public static final field $stable I +} + +public final class com/skydoves/landscapist/placeholder/placeholder/PlaceholderPlugin$Failure : com/skydoves/landscapist/placeholder/placeholder/PlaceholderPlugin, com/skydoves/landscapist/plugins/ImagePlugin$FailureStatePlugin { + public static final field $stable I + public fun (Ljava/lang/Object;)V + public final fun component1 ()Ljava/lang/Object; + public fun compose (Landroidx/compose/ui/Modifier;Lcom/skydoves/landscapist/ImageOptions;Ljava/lang/Throwable;Landroidx/compose/runtime/Composer;I)Lcom/skydoves/landscapist/plugins/ImagePlugin; + public final fun copy (Ljava/lang/Object;)Lcom/skydoves/landscapist/placeholder/placeholder/PlaceholderPlugin$Failure; + public static synthetic fun copy$default (Lcom/skydoves/landscapist/placeholder/placeholder/PlaceholderPlugin$Failure;Ljava/lang/Object;ILjava/lang/Object;)Lcom/skydoves/landscapist/placeholder/placeholder/PlaceholderPlugin$Failure; + public fun equals (Ljava/lang/Object;)Z + public final fun getSource ()Ljava/lang/Object; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/skydoves/landscapist/placeholder/placeholder/PlaceholderPlugin$Loading : com/skydoves/landscapist/placeholder/placeholder/PlaceholderPlugin, com/skydoves/landscapist/plugins/ImagePlugin$LoadingStatePlugin { + public static final field $stable I + public fun (Ljava/lang/Object;)V + public final fun component1 ()Ljava/lang/Object; + public fun compose (Landroidx/compose/ui/Modifier;Lcom/skydoves/landscapist/ImageOptions;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;I)Lcom/skydoves/landscapist/plugins/ImagePlugin; + public final fun copy (Ljava/lang/Object;)Lcom/skydoves/landscapist/placeholder/placeholder/PlaceholderPlugin$Loading; + public static synthetic fun copy$default (Lcom/skydoves/landscapist/placeholder/placeholder/PlaceholderPlugin$Loading;Ljava/lang/Object;ILjava/lang/Object;)Lcom/skydoves/landscapist/placeholder/placeholder/PlaceholderPlugin$Loading; + public fun equals (Ljava/lang/Object;)Z + public final fun getSource ()Ljava/lang/Object; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/skydoves/landscapist/placeholder/shimmer/LocalShimmerKt { + public static final fun getLocalShimmerPlugin ()Landroidx/compose/runtime/ProvidableCompositionLocal; +} + +public final class com/skydoves/landscapist/placeholder/shimmer/ShimmerKt { + public static final fun Shimmer-eopBjH0 (Landroidx/compose/ui/Modifier;JJLcom/skydoves/landscapist/placeholder/shimmer/ShimmerType;Landroidx/compose/runtime/Composer;II)V +} + +public final class com/skydoves/landscapist/placeholder/shimmer/ShimmerPlugin : com/skydoves/landscapist/plugins/ImagePlugin$LoadingStatePlugin { + public static final field $stable I + public synthetic fun (JJLcom/skydoves/landscapist/placeholder/shimmer/ShimmerType;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JJLcom/skydoves/landscapist/placeholder/shimmer/ShimmerType;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1-0d7_KjU ()J + public final fun component2-0d7_KjU ()J + public final fun component3 ()Lcom/skydoves/landscapist/placeholder/shimmer/ShimmerType; + public fun compose (Landroidx/compose/ui/Modifier;Lcom/skydoves/landscapist/ImageOptions;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;I)Lcom/skydoves/landscapist/plugins/ImagePlugin; + public final fun copy-jxsXWHM (JJLcom/skydoves/landscapist/placeholder/shimmer/ShimmerType;)Lcom/skydoves/landscapist/placeholder/shimmer/ShimmerPlugin; + public static synthetic fun copy-jxsXWHM$default (Lcom/skydoves/landscapist/placeholder/shimmer/ShimmerPlugin;JJLcom/skydoves/landscapist/placeholder/shimmer/ShimmerType;ILjava/lang/Object;)Lcom/skydoves/landscapist/placeholder/shimmer/ShimmerPlugin; + public fun equals (Ljava/lang/Object;)Z + public final fun getBaseColor-0d7_KjU ()J + public final fun getHighlightColor-0d7_KjU ()J + public final fun getShimmerType ()Lcom/skydoves/landscapist/placeholder/shimmer/ShimmerType; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/skydoves/landscapist/placeholder/shimmer/ShimmerType : java/lang/Enum { + public static final field FADE Lcom/skydoves/landscapist/placeholder/shimmer/ShimmerType; + public static final field RESONATE Lcom/skydoves/landscapist/placeholder/shimmer/ShimmerType; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/skydoves/landscapist/placeholder/shimmer/ShimmerType; + public static fun values ()[Lcom/skydoves/landscapist/placeholder/shimmer/ShimmerType; +} + +public final class com/skydoves/landscapist/placeholder/thumbnail/ThumbnailPlugin : com/skydoves/landscapist/plugins/ImagePlugin$LoadingStatePlugin { + public static final field $stable I + public synthetic fun (JILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun compose (Landroidx/compose/ui/Modifier;Lcom/skydoves/landscapist/ImageOptions;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;I)Lcom/skydoves/landscapist/plugins/ImagePlugin; +} + diff --git a/landscapist-placeholder/api/desktop/landscapist-placeholder.api b/landscapist-placeholder/api/desktop/landscapist-placeholder.api new file mode 100644 index 00000000..cd10590f --- /dev/null +++ b/landscapist-placeholder/api/desktop/landscapist-placeholder.api @@ -0,0 +1,71 @@ +public abstract class com/skydoves/landscapist/placeholder/placeholder/PlaceholderPlugin { + public static final field $stable I +} + +public final class com/skydoves/landscapist/placeholder/placeholder/PlaceholderPlugin$Failure : com/skydoves/landscapist/placeholder/placeholder/PlaceholderPlugin, com/skydoves/landscapist/plugins/ImagePlugin$FailureStatePlugin { + public static final field $stable I + public fun (Ljava/lang/Object;)V + public final fun component1 ()Ljava/lang/Object; + public fun compose (Landroidx/compose/ui/Modifier;Lcom/skydoves/landscapist/ImageOptions;Ljava/lang/Throwable;Landroidx/compose/runtime/Composer;I)Lcom/skydoves/landscapist/plugins/ImagePlugin; + public final fun copy (Ljava/lang/Object;)Lcom/skydoves/landscapist/placeholder/placeholder/PlaceholderPlugin$Failure; + public static synthetic fun copy$default (Lcom/skydoves/landscapist/placeholder/placeholder/PlaceholderPlugin$Failure;Ljava/lang/Object;ILjava/lang/Object;)Lcom/skydoves/landscapist/placeholder/placeholder/PlaceholderPlugin$Failure; + public fun equals (Ljava/lang/Object;)Z + public final fun getSource ()Ljava/lang/Object; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/skydoves/landscapist/placeholder/placeholder/PlaceholderPlugin$Loading : com/skydoves/landscapist/placeholder/placeholder/PlaceholderPlugin, com/skydoves/landscapist/plugins/ImagePlugin$LoadingStatePlugin { + public static final field $stable I + public fun (Ljava/lang/Object;)V + public final fun component1 ()Ljava/lang/Object; + public fun compose (Landroidx/compose/ui/Modifier;Lcom/skydoves/landscapist/ImageOptions;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;I)Lcom/skydoves/landscapist/plugins/ImagePlugin; + public final fun copy (Ljava/lang/Object;)Lcom/skydoves/landscapist/placeholder/placeholder/PlaceholderPlugin$Loading; + public static synthetic fun copy$default (Lcom/skydoves/landscapist/placeholder/placeholder/PlaceholderPlugin$Loading;Ljava/lang/Object;ILjava/lang/Object;)Lcom/skydoves/landscapist/placeholder/placeholder/PlaceholderPlugin$Loading; + public fun equals (Ljava/lang/Object;)Z + public final fun getSource ()Ljava/lang/Object; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/skydoves/landscapist/placeholder/shimmer/LocalShimmerKt { + public static final fun getLocalShimmerPlugin ()Landroidx/compose/runtime/ProvidableCompositionLocal; +} + +public final class com/skydoves/landscapist/placeholder/shimmer/ShimmerKt { + public static final fun Shimmer-eopBjH0 (Landroidx/compose/ui/Modifier;JJLcom/skydoves/landscapist/placeholder/shimmer/ShimmerType;Landroidx/compose/runtime/Composer;II)V +} + +public final class com/skydoves/landscapist/placeholder/shimmer/ShimmerPlugin : com/skydoves/landscapist/plugins/ImagePlugin$LoadingStatePlugin { + public static final field $stable I + public synthetic fun (JJLcom/skydoves/landscapist/placeholder/shimmer/ShimmerType;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JJLcom/skydoves/landscapist/placeholder/shimmer/ShimmerType;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1-0d7_KjU ()J + public final fun component2-0d7_KjU ()J + public final fun component3 ()Lcom/skydoves/landscapist/placeholder/shimmer/ShimmerType; + public fun compose (Landroidx/compose/ui/Modifier;Lcom/skydoves/landscapist/ImageOptions;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;I)Lcom/skydoves/landscapist/plugins/ImagePlugin; + public final fun copy-jxsXWHM (JJLcom/skydoves/landscapist/placeholder/shimmer/ShimmerType;)Lcom/skydoves/landscapist/placeholder/shimmer/ShimmerPlugin; + public static synthetic fun copy-jxsXWHM$default (Lcom/skydoves/landscapist/placeholder/shimmer/ShimmerPlugin;JJLcom/skydoves/landscapist/placeholder/shimmer/ShimmerType;ILjava/lang/Object;)Lcom/skydoves/landscapist/placeholder/shimmer/ShimmerPlugin; + public fun equals (Ljava/lang/Object;)Z + public final fun getBaseColor-0d7_KjU ()J + public final fun getHighlightColor-0d7_KjU ()J + public final fun getShimmerType ()Lcom/skydoves/landscapist/placeholder/shimmer/ShimmerType; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/skydoves/landscapist/placeholder/shimmer/ShimmerType : java/lang/Enum { + public static final field FADE Lcom/skydoves/landscapist/placeholder/shimmer/ShimmerType; + public static final field RESONATE Lcom/skydoves/landscapist/placeholder/shimmer/ShimmerType; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/skydoves/landscapist/placeholder/shimmer/ShimmerType; + public static fun values ()[Lcom/skydoves/landscapist/placeholder/shimmer/ShimmerType; +} + +public final class com/skydoves/landscapist/placeholder/thumbnail/ThumbnailPlugin : com/skydoves/landscapist/plugins/ImagePlugin$LoadingStatePlugin { + public static final field $stable I + public synthetic fun (JILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun compose (Landroidx/compose/ui/Modifier;Lcom/skydoves/landscapist/ImageOptions;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;I)Lcom/skydoves/landscapist/plugins/ImagePlugin; +} + diff --git a/landscapist-placeholder/build.gradle.kts b/landscapist-placeholder/build.gradle.kts index a0db59c6..91226168 100644 --- a/landscapist-placeholder/build.gradle.kts +++ b/landscapist-placeholder/build.gradle.kts @@ -16,7 +16,7 @@ import com.github.skydoves.landscapist.Configuration plugins { - id("landscapist.library.compose") + id("landscapist.library.compose.multiplatform") id("landscapist.spotless") } @@ -35,6 +35,31 @@ mavenPublishing { } } +kotlin { + sourceSets { + all { + languageSettings.optIn("kotlin.RequiresOptIn") + languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi") + languageSettings.optIn("com.skydoves.landscapist.InternalLandscapistApi") + } + val commonMain by getting { + dependencies { + api(project(":landscapist")) + + implementation(compose.ui) + implementation(compose.runtime) + implementation(compose.foundation) + } + } + + val androidMain by getting { + dependencies { + implementation(libs.androidx.core.ktx) + } + } + } +} + android { namespace = "com.skydoves.landscapist.placeholder" compileSdk = Configuration.compileSdk @@ -52,17 +77,14 @@ baselineProfile { } } -dependencies { - api(project(":landscapist")) - - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.compose.ui) - implementation(libs.androidx.compose.runtime) - implementation(libs.androidx.compose.foundation) - - androidTestImplementation(libs.androidx.test.rules) - androidTestImplementation(libs.androidx.test.runner) - androidTestImplementation(libs.androidx.test.junit) - androidTestImplementation(libs.androidx.compose.ui) - androidTestImplementation(libs.androidx.compose.ui.test) -} \ No newline at end of file +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", + ) + } +} diff --git a/landscapist-placeholder/src/main/AndroidManifest.xml b/landscapist-placeholder/src/androidMain/AndroidManifest.xml similarity index 100% rename from landscapist-placeholder/src/main/AndroidManifest.xml rename to landscapist-placeholder/src/androidMain/AndroidManifest.xml diff --git a/landscapist-placeholder/src/main/baseline-prof.txt b/landscapist-placeholder/src/androidMain/baseline-prof.txt similarity index 100% rename from landscapist-placeholder/src/main/baseline-prof.txt rename to landscapist-placeholder/src/androidMain/baseline-prof.txt diff --git a/landscapist-placeholder/src/main/kotlin/com/skydoves/landscapist/placeholder/placeholder/PlaceholderPlugin.kt b/landscapist-placeholder/src/commonMain/kotlin/com/skydoves/landscapist/placeholder/placeholder/PlaceholderPlugin.kt similarity index 100% rename from landscapist-placeholder/src/main/kotlin/com/skydoves/landscapist/placeholder/placeholder/PlaceholderPlugin.kt rename to landscapist-placeholder/src/commonMain/kotlin/com/skydoves/landscapist/placeholder/placeholder/PlaceholderPlugin.kt diff --git a/landscapist-placeholder/src/main/kotlin/com/skydoves/landscapist/placeholder/shimmer/LocalShimmer.kt b/landscapist-placeholder/src/commonMain/kotlin/com/skydoves/landscapist/placeholder/shimmer/LocalShimmer.kt similarity index 96% rename from landscapist-placeholder/src/main/kotlin/com/skydoves/landscapist/placeholder/shimmer/LocalShimmer.kt rename to landscapist-placeholder/src/commonMain/kotlin/com/skydoves/landscapist/placeholder/shimmer/LocalShimmer.kt index 0e79e508..81e89e29 100644 --- a/landscapist-placeholder/src/main/kotlin/com/skydoves/landscapist/placeholder/shimmer/LocalShimmer.kt +++ b/landscapist-placeholder/src/commonMain/kotlin/com/skydoves/landscapist/placeholder/shimmer/LocalShimmer.kt @@ -13,8 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@file:JvmName("LocalShimmerParams") -@file:JvmMultifileClass @file:Suppress("unused") package com.skydoves.landscapist.placeholder.shimmer diff --git a/landscapist-placeholder/src/commonMain/kotlin/com/skydoves/landscapist/placeholder/shimmer/Placeholder.kt b/landscapist-placeholder/src/commonMain/kotlin/com/skydoves/landscapist/placeholder/shimmer/Placeholder.kt new file mode 100644 index 00000000..f4ae5bd7 --- /dev/null +++ b/landscapist-placeholder/src/commonMain/kotlin/com/skydoves/landscapist/placeholder/shimmer/Placeholder.kt @@ -0,0 +1,267 @@ +/* + * 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.placeholder.shimmer + +import androidx.compose.animation.core.FiniteAnimationSpec +import androidx.compose.animation.core.InfiniteRepeatableSpec +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.Transition +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.animation.core.updateTransition +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.geometry.toRect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.drawOutline +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.node.Ref +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.unit.LayoutDirection + +/** + * Originated from https://github.com/google/accompanist/blob/main/placeholder/src/main/java/com/google/accompanist/placeholder/Placeholder.kt + * + * All rights reserved to Google LLC. + * + * Contains default values used by [Modifier.placeholder] and [PlaceholderHighlight]. + */ +internal object PlaceholderDefaults { + /** + * The default [InfiniteRepeatableSpec] to use for [fade]. + */ + val fadeAnimationSpec: InfiniteRepeatableSpec by lazy { + infiniteRepeatable( + animation = tween(delayMillis = 0, durationMillis = 600), + repeatMode = RepeatMode.Reverse, + ) + } + + /** + * The default [InfiniteRepeatableSpec] to use for [shimmer]. + */ + val shimmerAnimationSpec: InfiniteRepeatableSpec by lazy { + infiniteRepeatable( + animation = tween(delayMillis = 0, durationMillis = 1700), + repeatMode = RepeatMode.Restart, + ) + } +} + +/** + * Draws some skeleton UI which is typically used whilst content is 'loading'. + * + * A version of this modifier which uses appropriate values for Material themed apps is available + * in the 'Placeholder Material' library. + * + * You can provide a [PlaceholderHighlight] which runs an highlight animation on the placeholder. + * The [shimmer] and [fade] implementations are provided for easy usage. + * + * A cross-fade transition will be applied to the content and placeholder UI when the [visible] + * value changes. The transition can be customized via the [contentFadeTransitionSpec] and + * [placeholderFadeTransitionSpec] parameters. + * + * You can find more information on the pattern at the Material Theming + * [Placeholder UI](https://material.io/design/communication/launch-screen.html#placeholder-ui) + * guidelines. + * + * @sample com.google.accompanist.sample.placeholder.DocSample_Foundation_Placeholder + * + * @param visible whether the placeholder should be visible or not. + * @param color the color used to draw the placeholder UI. + * @param shape desired shape of the placeholder. Defaults to [RectangleShape]. + * @param highlight optional highlight animation. + * @param placeholderFadeTransitionSpec The transition spec to use when fading the placeholder + * on/off screen. The boolean parameter defined for the transition is [visible]. + * @param contentFadeTransitionSpec The transition spec to use when fading the content + * on/off screen. The boolean parameter defined for the transition is [visible]. + */ +internal fun Modifier.placeholder( + visible: Boolean, + color: Color, + shape: Shape = RectangleShape, + highlight: PlaceholderHighlight? = null, + placeholderFadeTransitionSpec: + @Composable Transition.Segment.() -> FiniteAnimationSpec = { spring() }, + contentFadeTransitionSpec: + @Composable Transition.Segment.() -> FiniteAnimationSpec = { spring() }, +): Modifier = composed( + inspectorInfo = debugInspectorInfo { + name = "placeholder" + value = visible + properties["visible"] = visible + properties["color"] = color + properties["highlight"] = highlight + properties["shape"] = shape + }, +) { + // Values used for caching purposes + val lastSize = remember { Ref() } + val lastLayoutDirection = remember { Ref() } + val lastOutline = remember { Ref() } + + // The current highlight animation progress + var highlightProgress: Float by remember { mutableStateOf(0f) } + + // This is our crossfade transition + val transitionState = remember { MutableTransitionState(visible) }.apply { + targetState = visible + } + val transition = updateTransition(transitionState, "placeholder_crossfade") + + val placeholderAlpha by transition.animateFloat( + transitionSpec = placeholderFadeTransitionSpec, + label = "placeholder_fade", + targetValueByState = { placeholderVisible -> if (placeholderVisible) 1f else 0f }, + ) + val contentAlpha by transition.animateFloat( + transitionSpec = contentFadeTransitionSpec, + label = "content_fade", + targetValueByState = { placeholderVisible -> if (placeholderVisible) 0f else 1f }, + ) + + // Run the optional animation spec and update the progress if the placeholder is visible + val animationSpec = highlight?.animationSpec + if (animationSpec != null && (visible || placeholderAlpha >= 0.01f)) { + val infiniteTransition = rememberInfiniteTransition() + highlightProgress = infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 1f, + animationSpec = animationSpec, + ).value + } + + val paint = remember { Paint() } + remember(color, shape, highlight) { + drawWithContent { + // Draw the composable content first + if (contentAlpha in 0.01f..0.99f) { + // If the content alpha is between 1% and 99%, draw it in a layer with + // the alpha applied + paint.alpha = contentAlpha + withLayer(paint) { + with(this@drawWithContent) { + drawContent() + } + } + } else if (contentAlpha >= 0.99f) { + // If the content alpha is > 99%, draw it with no alpha + drawContent() + } + + if (placeholderAlpha in 0.01f..0.99f) { + // If the placeholder alpha is between 1% and 99%, draw it in a layer with + // the alpha applied + paint.alpha = placeholderAlpha + withLayer(paint) { + lastOutline.value = drawPlaceholder( + shape = shape, + color = color, + highlight = highlight, + progress = highlightProgress, + lastOutline = lastOutline.value, + lastLayoutDirection = lastLayoutDirection.value, + lastSize = lastSize.value, + ) + } + } else if (placeholderAlpha >= 0.99f) { + // If the placeholder alpha is > 99%, draw it with no alpha + lastOutline.value = drawPlaceholder( + shape = shape, + color = color, + highlight = highlight, + progress = highlightProgress, + lastOutline = lastOutline.value, + lastLayoutDirection = lastLayoutDirection.value, + lastSize = lastSize.value, + ) + } + + // Keep track of the last size & layout direction + lastSize.value = size + lastLayoutDirection.value = layoutDirection + } + } +} + +private fun DrawScope.drawPlaceholder( + shape: Shape, + color: Color, + highlight: PlaceholderHighlight?, + progress: Float, + lastOutline: Outline?, + lastLayoutDirection: LayoutDirection?, + lastSize: Size?, +): Outline? { + // shortcut to avoid Outline calculation and allocation + if (shape === RectangleShape) { + // Draw the initial background color + drawRect(color = color) + + if (highlight != null) { + drawRect( + brush = highlight.brush(progress, size), + alpha = highlight.alpha(progress), + ) + } + // We didn't create an outline so return null + return null + } + + // Otherwise we need to create an outline from the shape + val outline = lastOutline.takeIf { + size == lastSize && layoutDirection == lastLayoutDirection + } ?: shape.createOutline(size, layoutDirection, this) + + // Draw the placeholder color + drawOutline(outline = outline, color = color) + + if (highlight != null) { + drawOutline( + outline = outline, + brush = highlight.brush(progress, size), + alpha = highlight.alpha(progress), + ) + } + + // Return the outline we used + return outline +} + +private inline fun DrawScope.withLayer( + paint: Paint, + drawBlock: DrawScope.() -> Unit, +) = drawIntoCanvas { canvas -> + canvas.saveLayer(size.toRect(), paint) + drawBlock() + canvas.restore() +} diff --git a/landscapist-placeholder/src/commonMain/kotlin/com/skydoves/landscapist/placeholder/shimmer/PlaceholderHighlight.kt b/landscapist-placeholder/src/commonMain/kotlin/com/skydoves/landscapist/placeholder/shimmer/PlaceholderHighlight.kt new file mode 100644 index 00000000..e810097e --- /dev/null +++ b/landscapist-placeholder/src/commonMain/kotlin/com/skydoves/landscapist/placeholder/shimmer/PlaceholderHighlight.kt @@ -0,0 +1,153 @@ +/* + * 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.placeholder.shimmer + +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.InfiniteRepeatableSpec +import androidx.compose.runtime.Stable +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import kotlin.math.max + +/** + * Originated from https://github.com/google/accompanist/blob/main/placeholder/src/main/java/com/google/accompanist/placeholder/PlaceholderHighlight.kt + * + * All rights reserved to Google LLC. + * + * A class which provides a brush to paint placeholder based on progress. + */ +@Stable +internal interface PlaceholderHighlight { + /** + * The optional [AnimationSpec] to use when running the animation for this highlight. + */ + val animationSpec: InfiniteRepeatableSpec? + + /** + * Return a [Brush] to draw for the given [progress] and [size]. + * + * @param progress the current animated progress in the range of 0f..1f. + * @param size The size of the current layout to draw in. + */ + fun brush( + progress: Float, + size: Size, + ): Brush + + /** + * Return the desired alpha value used for drawing the [Brush] returned from [brush]. + * + * @param progress the current animated progress in the range of 0f..1f. + */ + fun alpha(progress: Float): Float + + companion object +} + +/** + * Creates a [Fade] brush with the given initial and target colors. + * + * @sample com.google.accompanist.sample.placeholder.DocSample_Foundation_PlaceholderFade + * + * @param highlightColor the color of the highlight which is faded in/out. + * @param animationSpec the [AnimationSpec] to configure the animation. + */ +internal fun PlaceholderHighlight.Companion.fade( + highlightColor: Color, + animationSpec: InfiniteRepeatableSpec = PlaceholderDefaults.fadeAnimationSpec, +): PlaceholderHighlight = Fade( + highlightColor = highlightColor, + animationSpec = animationSpec, +) + +/** + * Creates a [PlaceholderHighlight] which 'shimmers', using the given [highlightColor]. + * + * The highlight starts at the top-start, and then grows to the bottom-end during the animation. + * During that time it is also faded in, from 0f..progressForMaxAlpha, and then faded out from + * progressForMaxAlpha..1f. + * + * @sample com.google.accompanist.sample.placeholder.DocSample_Foundation_PlaceholderShimmer + * + * @param highlightColor the color of the highlight 'shimmer'. + * @param animationSpec the [AnimationSpec] to configure the animation. + * @param progressForMaxAlpha The progress where the shimmer should be at it's peak opacity. + * Defaults to 0.6f. + */ +internal fun PlaceholderHighlight.Companion.shimmer( + highlightColor: Color, + animationSpec: InfiniteRepeatableSpec = PlaceholderDefaults.shimmerAnimationSpec, + progressForMaxAlpha: Float = 0.6f, +): PlaceholderHighlight = Shimmer( + highlightColor = highlightColor, + animationSpec = animationSpec, + progressForMaxAlpha = progressForMaxAlpha, +) + +private data class Fade( + private val highlightColor: Color, + override val animationSpec: InfiniteRepeatableSpec, +) : PlaceholderHighlight { + private val brush = SolidColor(highlightColor) + + override fun brush(progress: Float, size: Size): Brush = brush + override fun alpha(progress: Float): Float = progress +} + +private data class Shimmer( + private val highlightColor: Color, + override val animationSpec: InfiniteRepeatableSpec, + private val progressForMaxAlpha: Float = 0.6f, +) : PlaceholderHighlight { + override fun brush( + progress: Float, + size: Size, + ): Brush = Brush.radialGradient( + colors = listOf( + highlightColor.copy(alpha = 0f), + highlightColor, + highlightColor.copy(alpha = 0f), + ), + center = Offset(x = 0f, y = 0f), + radius = (max(size.width, size.height) * progress * 2).coerceAtLeast(0.01f), + ) + + override fun alpha(progress: Float): Float = when { + // From 0f...ProgressForOpaqueAlpha we animate from 0..1 + progress <= progressForMaxAlpha -> { + lerp( + start = 0f, + stop = 1f, + fraction = progress / progressForMaxAlpha, + ) + } + // From ProgressForOpaqueAlpha..1f we animate from 1..0 + else -> { + lerp( + start = 1f, + stop = 0f, + fraction = (progress - progressForMaxAlpha) / (1f - progressForMaxAlpha), + ) + } + } +} + +private fun lerp(start: Float, stop: Float, fraction: Float): Float { + return (1 - fraction) * start + fraction * stop +} diff --git a/landscapist-placeholder/src/commonMain/kotlin/com/skydoves/landscapist/placeholder/shimmer/Shimmer.kt b/landscapist-placeholder/src/commonMain/kotlin/com/skydoves/landscapist/placeholder/shimmer/Shimmer.kt new file mode 100644 index 00000000..6ed76c31 --- /dev/null +++ b/landscapist-placeholder/src/commonMain/kotlin/com/skydoves/landscapist/placeholder/shimmer/Shimmer.kt @@ -0,0 +1,48 @@ +/* + * 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.placeholder.shimmer + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +@Composable +public fun Shimmer( + modifier: Modifier, + baseColor: Color, + highlightColor: Color, + shimmerType: ShimmerType = ShimmerType.RESONATE, +) { + Box(modifier = modifier) { + Box( + modifier = Modifier.matchParentSize() + .placeholder( + visible = true, + color = baseColor, + highlight = if (shimmerType == ShimmerType.RESONATE) { + PlaceholderHighlight.shimmer( + highlightColor = highlightColor, + ) + } else { + PlaceholderHighlight.fade( + highlightColor = highlightColor, + ) + }, + ), + ) + } +} diff --git a/landscapist-placeholder/src/main/kotlin/com/skydoves/landscapist/placeholder/shimmer/ShimmerPlugin.kt b/landscapist-placeholder/src/commonMain/kotlin/com/skydoves/landscapist/placeholder/shimmer/ShimmerPlugin.kt similarity index 68% rename from landscapist-placeholder/src/main/kotlin/com/skydoves/landscapist/placeholder/shimmer/ShimmerPlugin.kt rename to landscapist-placeholder/src/commonMain/kotlin/com/skydoves/landscapist/placeholder/shimmer/ShimmerPlugin.kt index 58d54bf0..b7146cef 100644 --- a/landscapist-placeholder/src/main/kotlin/com/skydoves/landscapist/placeholder/shimmer/ShimmerPlugin.kt +++ b/landscapist-placeholder/src/commonMain/kotlin/com/skydoves/landscapist/placeholder/shimmer/ShimmerPlugin.kt @@ -19,7 +19,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntSize import com.skydoves.landscapist.ImageOptions import com.skydoves.landscapist.plugins.ImagePlugin @@ -29,20 +28,12 @@ import com.skydoves.landscapist.plugins.ImagePlugin * * @param baseColor the base color of the content. * @param highlightColor the shimmer's highlight color. - * @param intensity adjust the density of the highlight at the center. - * @param dropOff adjust the size of the fading edge of the highlight. - * @param tilt adjust an angle at which the highlight is tilted and measured in degrees. - * @param durationMillis a milli-second time to move the simmering effect from start to finish animation. */ @Immutable public data class ShimmerPlugin( val baseColor: Color, val highlightColor: Color, - val width: Dp? = null, - val intensity: Float = DefaultShimmerIntensity, - val dropOff: Float = DefaultShimmerDropOff, - val tilt: Float = DefaultShimmerTilt, - val durationMillis: Int = DefaultDurationMillis, + val shimmerType: ShimmerType = ShimmerType.RESONATE, ) : ImagePlugin.LoadingStatePlugin { @Composable @@ -55,11 +46,7 @@ public data class ShimmerPlugin( modifier = modifier, baseColor = baseColor, highlightColor = highlightColor, - shimmerWidth = width, - intensity = intensity, - dropOff = dropOff, - tilt = tilt, - durationMillis = durationMillis, + shimmerType = shimmerType, ) } } diff --git a/landscapist-placeholder/src/commonMain/kotlin/com/skydoves/landscapist/placeholder/shimmer/ShimmerType.kt b/landscapist-placeholder/src/commonMain/kotlin/com/skydoves/landscapist/placeholder/shimmer/ShimmerType.kt new file mode 100644 index 00000000..c50aba63 --- /dev/null +++ b/landscapist-placeholder/src/commonMain/kotlin/com/skydoves/landscapist/placeholder/shimmer/ShimmerType.kt @@ -0,0 +1,20 @@ +/* + * 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.placeholder.shimmer + +public enum class ShimmerType { + FADE, RESONATE +} diff --git a/landscapist-placeholder/src/main/kotlin/com/skydoves/landscapist/placeholder/thumbnail/ThumbnailPlugin.kt b/landscapist-placeholder/src/commonMain/kotlin/com/skydoves/landscapist/placeholder/thumbnail/ThumbnailPlugin.kt similarity index 100% rename from landscapist-placeholder/src/main/kotlin/com/skydoves/landscapist/placeholder/thumbnail/ThumbnailPlugin.kt rename to landscapist-placeholder/src/commonMain/kotlin/com/skydoves/landscapist/placeholder/thumbnail/ThumbnailPlugin.kt diff --git a/landscapist-placeholder/src/main/kotlin/com/skydoves/landscapist/placeholder/shimmer/Shimmer.kt b/landscapist-placeholder/src/main/kotlin/com/skydoves/landscapist/placeholder/shimmer/Shimmer.kt deleted file mode 100644 index b33f0f21..00000000 --- a/landscapist-placeholder/src/main/kotlin/com/skydoves/landscapist/placeholder/shimmer/Shimmer.kt +++ /dev/null @@ -1,156 +0,0 @@ -/* - * 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:JvmName("Shimmer") -@file:JvmMultifileClass - -package com.skydoves.landscapist.placeholder.shimmer - -import android.graphics.Matrix -import android.graphics.PorterDuff -import android.graphics.PorterDuffXfermode -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.tween -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.toRect -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.LinearGradientShader -import androidx.compose.ui.graphics.Paint -import androidx.compose.ui.graphics.Shader -import androidx.compose.ui.graphics.ShaderBrush -import androidx.compose.ui.graphics.TileMode -import androidx.compose.ui.graphics.drawscope.drawIntoCanvas -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Dp -import androidx.core.util.Pools -import kotlin.math.max -import kotlin.math.min -import kotlin.math.tan - -/** - * Create a shimmering effect composable with base and highlighting colors. - * - * @param modifier basic modifier, must be applied fillMaxSize(). - * @param baseColor base background color of this composable. - * @param highlightColor highlight shimmering effect color of this composable. - * @param intensity controls the brightness of the highlight at the center. - * @param dropOff controls the size of the fading edge of the highlight. - * @param tilt angle at which the highlight is tilted, measured in degrees. - * @param durationMillis animation duration of the shimmering start to end. - */ -@Composable -public fun Shimmer( - modifier: Modifier = Modifier, - baseColor: Color, - highlightColor: Color, - shimmerWidth: Dp? = null, - intensity: Float = DefaultShimmerIntensity, - dropOff: Float = DefaultShimmerDropOff, - tilt: Float = DefaultShimmerTilt, - durationMillis: Int = DefaultDurationMillis, -) { - val shimmerWidthPx = with(LocalDensity.current) { shimmerWidth?.toPx() } - val animatedProgress = remember { Animatable(0f) } - LaunchedEffect(key1 = baseColor) { - animatedProgress.animateTo( - targetValue = 1f, - animationSpec = infiniteRepeatable( - animation = tween(durationMillis = durationMillis, easing = LinearEasing), - ), - ) - } - - Box(modifier) { - Canvas(Modifier.fillMaxSize()) { - val paint = paintPool.acquire() ?: Paint() - val shaderMatrix = Matrix() - val tiltTan = tan(Math.toRadians(tilt.toDouble())) - val width = shimmerWidthPx ?: (size.width + tiltTan * size.height).toFloat() - - try { - val dx = offset(-width, width, animatedProgress.value) - val shader: Shader = LinearGradientShader( - from = Offset(0f, 0f), - to = Offset(size.width, 0f), - colors = listOf( - baseColor, - highlightColor, - highlightColor, - baseColor, - ), - colorStops = listOf( - max((1f - intensity - dropOff) / 2f, 0f), - max((1f - intensity - 0.001f) / 2f, 0f), - min((1f + intensity + 0.001f) / 2f, 1f), - min((1f + intensity + dropOff) / 2f, 1f), - ), - tileMode = TileMode.Clamp, - ) - val brush = ShaderBrush(shader) - paint.asFrameworkPaint().apply { - isAntiAlias = true - xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) - setShader(shader) - } - - shaderMatrix.reset() - shaderMatrix.setRotate(tilt, size.width / 2f, size.height / 2f) - shaderMatrix.postTranslate(dx, 0f) - shader.setLocalMatrix(shaderMatrix) - - drawIntoCanvas { canvas -> - canvas.saveLayer(size.toRect(), paint) - - drawRect(brush, Offset(0f, 0f), size) - - canvas.restore() - } - } finally { - // resets the paint and release to the pool. - paint.asFrameworkPaint().reset() - paintPool.release(paint) - } - } - } -} - -/** returns a shimmer matrix offset. */ -private fun offset(start: Float, end: Float, percent: Float): Float { - return start + (end - start) * percent -} - -/** A definition of the default intensity. */ -internal const val DefaultShimmerIntensity = 0f - -/** A definition of the default dropOff. */ -internal const val DefaultShimmerDropOff = 0.5f - -/** A definition of the default tilt. */ -internal const val DefaultShimmerTilt = 20f - -/** A definition of the default duration. */ -internal const val DefaultDurationMillis = 650 - -/** paint pool which caching and reusing [Paint] instances. */ -private val paintPool = Pools.SimplePool(2)