Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve bitmap managing by using graphicsLayer APIs #27

Merged
merged 1 commit into from
Jul 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cloudy/api/cloudy.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
146 changes: 38 additions & 108 deletions cloudy/src/main/kotlin/com/skydoves/cloudy/Cloudy.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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),
Expand All @@ -80,26 +71,23 @@ public fun Cloudy(
) {
val context = LocalContext.current
var initialBitmap by remember { mutableStateOf<Bitmap?>(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
)
})
}

/**
Expand Down Expand Up @@ -130,6 +118,9 @@ private fun ComposeView.composeCloudy(
key2 = key2,
key3 = key3
) { mutableStateOf(LayoutInfo()) }

val graphicsLayer = rememberGraphicsLayer()

Box(
modifier = modifier
.onGloballyPositioned {
Expand All @@ -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
[email protected]()
}
// 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
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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)
}
Expand All @@ -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,
Expand All @@ -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())
)
}
4 changes: 2 additions & 2 deletions cloudy/src/main/kotlin/com/skydoves/cloudy/CloudyState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
Loading