From f08019154416df4991f641d733af707caac2af44 Mon Sep 17 00:00:00 2001 From: skydoves Date: Fri, 5 Jul 2024 22:08:17 +0900 Subject: [PATCH] Introduce cloudy Modifier --- .../kotlin/com/skydoves/cloudydemo/Main.kt | 68 ++++----- cloudy/api/cloudy.api | 4 + .../main/kotlin/com/skydoves/cloudy/Cloudy.kt | 5 +- .../cloudy/internals/CloudyModifierNode.kt | 131 ++++++++++++++++++ 4 files changed, 166 insertions(+), 42 deletions(-) create mode 100644 cloudy/src/main/kotlin/com/skydoves/cloudy/internals/CloudyModifierNode.kt diff --git a/app/src/main/kotlin/com/skydoves/cloudydemo/Main.kt b/app/src/main/kotlin/com/skydoves/cloudydemo/Main.kt index 6bbe017..4f429df 100644 --- a/app/src/main/kotlin/com/skydoves/cloudydemo/Main.kt +++ b/app/src/main/kotlin/com/skydoves/cloudydemo/Main.kt @@ -39,12 +39,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.skydoves.cloudy.Cloudy -import com.skydoves.cloudy.CloudyState +import com.skydoves.cloudy.internals.cloudy import com.skydoves.cloudydemo.model.MockUtil import com.skydoves.landscapist.glide.GlideImage -import com.skydoves.landscapist.glide.GlideImageState -import com.skydoves.landscapist.glide.rememberGlideImageState @Composable fun Main() { @@ -62,7 +59,8 @@ fun Main() { durationMillis = 1000, delayMillis = 500, easing = FastOutLinearInEasing - ) + ), + label = "Blur Animation" ) LaunchedEffect(Unit) { @@ -70,43 +68,33 @@ fun Main() { } val poster = remember { MockUtil.getMockPoster() } - var glideState by rememberGlideImageState() - Cloudy( - radius = radius, - key1 = glideState, - allowAccumulate = { it is CloudyState.Success && glideState is GlideImageState.Success } - ) { - GlideImage( - modifier = Modifier.size(400.dp), - imageModel = { poster.image }, - onImageStateChanged = { glideState = it } - ) - } - Cloudy( - radius = radius, - allowAccumulate = { true } - ) { - Column { - Text( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - text = poster.name, - fontSize = 40.sp, - color = MaterialTheme.colors.onBackground, - textAlign = TextAlign.Center - ) + GlideImage( + modifier = Modifier + .size(400.dp) + .cloudy(radius = radius), + imageModel = { poster.image } + ) - Text( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - text = poster.description, - color = MaterialTheme.colors.onBackground, - textAlign = TextAlign.Center - ) - } + Column(modifier = Modifier.cloudy(radius = radius)) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + text = poster.name, + fontSize = 40.sp, + color = MaterialTheme.colors.onBackground, + textAlign = TextAlign.Center + ) + + Text( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + text = poster.description, + color = MaterialTheme.colors.onBackground, + textAlign = TextAlign.Center + ) } } } diff --git a/cloudy/api/cloudy.api b/cloudy/api/cloudy.api index 07959fb..f8e1fa6 100644 --- a/cloudy/api/cloudy.api +++ b/cloudy/api/cloudy.api @@ -49,6 +49,10 @@ public final class com/skydoves/cloudy/RememberCloudyStateKt { public static final fun rememberCloudyState (Lcom/skydoves/cloudy/CloudyState;Ljava/lang/Object;Landroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/MutableState; } +public final class com/skydoves/cloudy/internals/CloudyModifierNodeKt { + public static final fun cloudy (Landroidx/compose/ui/Modifier;ILkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)Landroidx/compose/ui/Modifier; +} + public final class com/skydoves/cloudy/internals/render/Range2d { public static final field $stable I public fun (IIII)V diff --git a/cloudy/src/main/kotlin/com/skydoves/cloudy/Cloudy.kt b/cloudy/src/main/kotlin/com/skydoves/cloudy/Cloudy.kt index 9814096..a80650f 100644 --- a/cloudy/src/main/kotlin/com/skydoves/cloudy/Cloudy.kt +++ b/cloudy/src/main/kotlin/com/skydoves/cloudy/Cloudy.kt @@ -23,6 +23,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.composed @@ -70,7 +71,7 @@ public fun Cloudy( content: @Composable BoxScope.() -> Unit ) { val context = LocalContext.current - var initialBitmap by remember { mutableStateOf(null) } + var initialBitmap by rememberSaveable(key1, key2) { mutableStateOf(null) } AndroidView(factory = { ComposeView(context) }, update = { it.composeCloudy( modifier = modifier, @@ -181,7 +182,7 @@ private fun Modifier.cloudy( properties["cloudy"] = radius }, factory = { - var blurredBitmap: Bitmap? by remember(key1 = key1, key2 = key2, key3 = key3) { + var blurredBitmap: Bitmap? by rememberSaveable(key1, key2, key3) { mutableStateOf(initialBitmap) } diff --git a/cloudy/src/main/kotlin/com/skydoves/cloudy/internals/CloudyModifierNode.kt b/cloudy/src/main/kotlin/com/skydoves/cloudy/internals/CloudyModifierNode.kt new file mode 100644 index 0000000..e1c3d87 --- /dev/null +++ b/cloudy/src/main/kotlin/com/skydoves/cloudy/internals/CloudyModifierNode.kt @@ -0,0 +1,131 @@ +/* + * Designed and developed by 2022 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.cloudy.internals + +import android.graphics.Bitmap +import androidx.annotation.IntRange +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.layer.GraphicsLayer +import androidx.compose.ui.graphics.rememberGraphicsLayer +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.node.DrawModifierNode +import androidx.compose.ui.node.GlobalPositionAwareModifierNode +import androidx.compose.ui.node.LayoutModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.offset +import com.skydoves.cloudy.CloudyState +import com.skydoves.cloudy.internals.render.RenderScriptToolkit +import kotlinx.coroutines.runBlocking + +@Composable +public fun Modifier.cloudy( + @IntRange(from = 0, to = 25) radius: Int = 10, + onStateChanged: (CloudyState) -> Unit = {} +): Modifier { + return this then CloudyModifierNodeElement( + graphicsLayer = rememberGraphicsLayer(), + radius = radius, + onStateChanged = onStateChanged + ) +} + +private data class CloudyModifierNodeElement( + private val graphicsLayer: GraphicsLayer, + @IntRange(from = 0, to = 25) val radius: Int = 10, + val onStateChanged: (CloudyState) -> Unit = {} +) : ModifierNodeElement() { + + override fun InspectorInfo.inspectableProperties() { + name = "cloudy" + properties["cloudy"] = radius + } + + override fun create(): CloudyModifierNode = CloudyModifierNode( + graphicsLayer = graphicsLayer, + radius = radius, + onStateChanged = onStateChanged + ) + + override fun update(node: CloudyModifierNode) { + node.radius = radius + } +} + +private class CloudyModifierNode( + val graphicsLayer: GraphicsLayer, + @IntRange(from = 0, to = 25) var radius: Int = 10, + private val onStateChanged: (CloudyState) -> Unit = {} +) : LayoutModifierNode, GlobalPositionAwareModifierNode, DrawModifierNode, Modifier.Node() { + + private var layoutInfo = LayoutInfo() + + override fun onGloballyPositioned(coordinates: LayoutCoordinates) { + layoutInfo = LayoutInfo( + xOffset = coordinates.positionInWindow().x.toInt(), + yOffset = coordinates.positionInWindow().y.toInt(), + width = coordinates.size.width, + height = coordinates.size.height + ) + } + + override fun MeasureScope.measure( + measurable: Measurable, + constraints: Constraints + ): MeasureResult { + val placeable = measurable.measure(constraints.offset()) + return layout(placeable.width, placeable.height) { + placeable.placeRelative(0, 0) + } + } + + override fun ContentDrawScope.draw() { + // call record to capture the content in the graphics layer + graphicsLayer.record { + // draw the contents of the composable into the graphics layer + this@draw.drawContent() + } + + onStateChanged.invoke(CloudyState.Loading) + + try { + val targetBitmap: Bitmap? = runBlocking { + graphicsLayer.toImageBitmap().asAndroidBitmap() + .copy(Bitmap.Config.ARGB_8888, true) + } + + val blurredBitmap = RenderScriptToolkit.blur( + inputBitmap = targetBitmap, + radius = radius + )?.apply { + drawImage(this.asImageBitmap()) + } + + onStateChanged.invoke(CloudyState.Success(blurredBitmap)) + } catch (e: Exception) { + onStateChanged.invoke(CloudyState.Error(e)) + } + } +}