Skip to content

Commit

Permalink
Merge pull request #27 from skydoves/improve/graphicsLayer
Browse files Browse the repository at this point in the history
Improve bitmap managing by using graphicsLayer APIs
  • Loading branch information
skydoves authored Jul 4, 2024
2 parents 5a495a0 + de4c265 commit e4fa994
Show file tree
Hide file tree
Showing 4 changed files with 47 additions and 111 deletions.
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
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
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

0 comments on commit e4fa994

Please sign in to comment.