From de4c265d55e2d62d3841814ab3b3faa465f1f979 Mon Sep 17 00:00:00 2001 From: skydoves Date: Thu, 4 Jul 2024 22:13:35 +0900 Subject: [PATCH] Improve bitmap managing by using graphicsLayer APIs --- cloudy/api/cloudy.api | 6 + .../main/kotlin/com/skydoves/cloudy/Cloudy.kt | 146 +++++------------- .../kotlin/com/skydoves/cloudy/CloudyState.kt | 4 +- gradle/libs.versions.toml | 2 +- 4 files changed, 47 insertions(+), 111 deletions(-) diff --git a/cloudy/api/cloudy.api b/cloudy/api/cloudy.api index e6403dd..07959fb 100644 --- a/cloudy/api/cloudy.api +++ b/cloudy/api/cloudy.api @@ -20,11 +20,17 @@ public final class com/skydoves/cloudy/CloudyState$Error : com/skydoves/cloudy/C public final class com/skydoves/cloudy/CloudyState$Loading : com/skydoves/cloudy/CloudyState { public static final field $stable I public static final field INSTANCE Lcom/skydoves/cloudy/CloudyState$Loading; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class com/skydoves/cloudy/CloudyState$Nothing : com/skydoves/cloudy/CloudyState { public static final field $stable I public static final field INSTANCE Lcom/skydoves/cloudy/CloudyState$Nothing; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class com/skydoves/cloudy/CloudyState$Success : com/skydoves/cloudy/CloudyState { diff --git a/cloudy/src/main/kotlin/com/skydoves/cloudy/Cloudy.kt b/cloudy/src/main/kotlin/com/skydoves/cloudy/Cloudy.kt index 9851b8c..9814096 100644 --- a/cloudy/src/main/kotlin/com/skydoves/cloudy/Cloudy.kt +++ b/cloudy/src/main/kotlin/com/skydoves/cloudy/Cloudy.kt @@ -16,15 +16,7 @@ package com.skydoves.cloudy import android.graphics.Bitmap -import android.graphics.Rect -import android.os.Build -import android.os.Handler -import android.os.Looper import android.renderscript.RenderScript -import android.view.PixelCopy -import android.view.View -import android.view.Window -import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.runtime.Composable @@ -35,25 +27,24 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.draw.blur +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.graphics.layer.GraphicsLayer +import androidx.compose.ui.graphics.layer.drawLayer +import androidx.compose.ui.graphics.rememberGraphicsLayer import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.debugInspectorInfo import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.view.doOnLayout -import androidx.core.view.drawToBitmap import com.skydoves.cloudy.internals.CloudyModifier import com.skydoves.cloudy.internals.InternalLaunchedEffect import com.skydoves.cloudy.internals.LayoutInfo -import com.skydoves.cloudy.internals.getActivity import com.skydoves.cloudy.internals.render.RenderScriptToolkit import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine /** * Cloudy is a replacement of the [blur] modifier (under Android 12), @@ -80,26 +71,23 @@ public fun Cloudy( ) { val context = LocalContext.current var initialBitmap by remember { mutableStateOf(null) } - AndroidView( - factory = { ComposeView(context) }, - update = { - it.composeCloudy( - modifier = modifier, - key1 = radius, - key2 = key1, - key3 = key2, - radius = radius, - initialBitmap = initialBitmap, - onStateChanged = { state -> - onStateChanged.invoke(state) - if (allowAccumulate.invoke(state) && state is CloudyState.Success) { - initialBitmap = state.bitmap - } - }, - content = content - ) - } - ) + AndroidView(factory = { ComposeView(context) }, update = { + it.composeCloudy( + modifier = modifier, + key1 = radius, + key2 = key1, + key3 = key2, + radius = radius, + initialBitmap = initialBitmap, + onStateChanged = { state -> + onStateChanged.invoke(state) + if (allowAccumulate.invoke(state) && state is CloudyState.Success) { + initialBitmap = state.bitmap + } + }, + content = content + ) + }) } /** @@ -130,6 +118,9 @@ private fun ComposeView.composeCloudy( key2 = key2, key3 = key3 ) { mutableStateOf(LayoutInfo()) } + + val graphicsLayer = rememberGraphicsLayer() + Box( modifier = modifier .onGloballyPositioned { @@ -140,14 +131,23 @@ private fun ComposeView.composeCloudy( height = it.size.height ) } + .drawWithContent { + // call record to capture the content in the graphics layer + graphicsLayer.record { + // draw the contents of the composable into the graphics layer + this@drawWithContent.drawContent() + } + // draw the graphics layer on the visible canvas + drawLayer(graphicsLayer) + } .cloudy( - view = this, key1 = key1, key2 = key2, key3 = key3, radius = radius, layoutInfo = layoutInfo, initialBitmap = initialBitmap, + graphicsLayer = graphicsLayer, onStateChanged = onStateChanged ), content = content @@ -159,7 +159,6 @@ private fun ComposeView.composeCloudy( * Composition of a [Modifier] to apply a blur effect to the given [view] and lunch blur rendering * process on the [Dispatchers.IO]. * - * @param view Target view that will be rendered with blur effects. * @param radius Radius of the blur along both the x and y axis. It must be in 0 to 25. * @param key1 Key value for trigger recomposition. * @param key2 Key value for trigger recomposition. @@ -168,11 +167,11 @@ private fun ComposeView.composeCloudy( * @param onStateChanged Lambda function that will be invoked when the blur process has been updated. */ private fun Modifier.cloudy( - view: View, key1: Any? = null, key2: Any? = null, key3: Any? = null, initialBitmap: Bitmap? = null, + graphicsLayer: GraphicsLayer, @androidx.annotation.IntRange(from = 0, to = 25) radius: Int, layoutInfo: LayoutInfo, onStateChanged: (CloudyState) -> Unit @@ -182,7 +181,6 @@ private fun Modifier.cloudy( properties["cloudy"] = radius }, factory = { - val window = LocalContext.current.getActivity()!!.window var blurredBitmap: Bitmap? by remember(key1 = key1, key2 = key2, key3 = key3) { mutableStateOf(initialBitmap) } @@ -192,10 +190,8 @@ private fun Modifier.cloudy( launch { onStateChanged.invoke(CloudyState.Loading) withContext(Dispatchers.IO) { - val targetBitmap = blurredBitmap ?: view.drawToBitmapPostLaidOut( - layoutInfo = layoutInfo, - window = window - ) + val targetBitmap = blurredBitmap ?: graphicsLayer.toImageBitmap().asAndroidBitmap() + .copy(Bitmap.Config.ARGB_8888, true) blurredBitmap = RenderScriptToolkit.blur( inputBitmap = targetBitmap, @@ -219,69 +215,3 @@ private fun Modifier.cloudy( } } ) - -/** - * Return [Bitmap] from the given [window] based on [layoutInfo] information. - * - * @param layoutInfo The [LayoutInfo] contains global layout information to decide the rendering position and scale. - * @param window The given window will be used to extract bitmap information. - */ -private suspend fun View.drawToBitmapPostLaidOut( - window: Window, - layoutInfo: LayoutInfo -): Bitmap? { - return suspendCoroutine { continuation -> - doOnLayout { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - drawBitmapWithPixelCopy( - window = window, - layoutInfo = layoutInfo, - onSuccess = { bitmap -> continuation.resume(bitmap) }, - onError = { error -> continuation.resumeWithException(error) } - ) - } else { - continuation.resume(this.drawToBitmap()) - } - } - } -} - -/** - * Extract [Bitmap] as pixels from the given [window] based on [layoutInfo] information. - * - * @param window The given window will be used to extract bitmap information. - * @param layoutInfo The [LayoutInfo] contains global layout information to decide the rendering position and scale. - * @param onSuccess This lambda will be called when extracting process has been succeed. - * @param onError This lambda will be called when extracting process has been failed. - */ -@RequiresApi(Build.VERSION_CODES.O) -private fun drawBitmapWithPixelCopy( - window: Window, - layoutInfo: LayoutInfo, - onSuccess: (Bitmap) -> Unit, - onError: (Throwable) -> Unit -) { - val rect = Rect( - layoutInfo.xOffset, - layoutInfo.yOffset, - layoutInfo.xOffset + layoutInfo.width, - layoutInfo.yOffset + layoutInfo.height - ) - - val bitmap = Bitmap.createBitmap(layoutInfo.width, layoutInfo.height, Bitmap.Config.ARGB_8888) - PixelCopy.request( - window, - rect, - bitmap, - { copyResult -> - if (copyResult == PixelCopy.SUCCESS) { - onSuccess.invoke(bitmap) - } else { - onError.invoke( - RuntimeException("Failed to copy pixels of the given bitmap!") - ) - } - }, - Handler(Looper.getMainLooper()) - ) -} diff --git a/cloudy/src/main/kotlin/com/skydoves/cloudy/CloudyState.kt b/cloudy/src/main/kotlin/com/skydoves/cloudy/CloudyState.kt index a57d59c..bad39f7 100644 --- a/cloudy/src/main/kotlin/com/skydoves/cloudy/CloudyState.kt +++ b/cloudy/src/main/kotlin/com/skydoves/cloudy/CloudyState.kt @@ -21,10 +21,10 @@ import android.graphics.Bitmap public sealed interface CloudyState { /** Represents the state of [Cloudy] process doesn't started. */ - public object Nothing : CloudyState + public data object Nothing : CloudyState /** Represents the state of [Cloudy] process is ongoing. */ - public object Loading : CloudyState + public data object Loading : CloudyState /** Represents the state of [Cloudy] process is successful. */ public data class Success(public val bitmap: Bitmap?) : CloudyState diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0c3c20e..0d965fc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,7 +32,7 @@ kotlinBinaryCompatibilityValidator = { id = "org.jetbrains.kotlinx.binary-compat [libraries] material = { module = "com.google.android.material:material", version.ref = "material" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidxComposeBom" } -androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-compose-ui = { group = "androidx.compose.ui", name = "ui", version = "1.7.0-beta04" } androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-compose-material = { group = "androidx.compose.material", name = "material" }