https://kotlinlang.org logo
Title
z

Zoltan Demant

08/22/2022, 3:53 AM
Im trying to blur some content in compose.
Modifier.blur
seems like the way to go, but Im also trying to support this effect all the way down to API 21. Has anyone had any success with alternative approaches?
Ive looked at this library. It looks like one of the better view based alternatives, but for me it just introduces a ton of sublte bugs (e.g. list scroll position is no longer restored when navigating). Many of the issues might just be me doing too much fancy custom stuff, but alas.
c

Colton Idle

08/22/2022, 5:59 AM
Theres 169 replies in this thread that gets to the bottom of why this is hard to do. 😄 https://kotlinlang.slack.com/archives/CJLTWPH7S/p1651845634528419
z

Zoltan Demant

08/22/2022, 6:13 AM
Oh, I guess I have some reading to do 😅
Am I wrong to think that I can just overlay a blurred image over my content to make this happen? 🤔
The stuff in that thread was awesome to read up on, but its way more than I need (I think). I just want to blur everything on a given screen really.
c

Colton Idle

08/22/2022, 9:52 AM
I don't think that's possible? at least not from what i understand.
z

Zoltan Demant

08/22/2022, 9:59 AM
From my limited testing.. it doesnt seem to do much 😕 In theory, if I have an image with the desired blur effect, I should be able to just overlay my content with it at 70% (or so) opacity to get a blurry vibe, but no bueno.
c

Colton Idle

08/22/2022, 10:12 AM
yeah. its also weird because the blur doesn't work like how it does in ios. like in ios itll blur everything behind it. in compose it only blurs the view you set the modifier to.
i think me or @Chris Sinco [G] opened up an issuetracker for having blur work like it does in css with backdrop filter https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter
z

Zoltan Demant

08/22/2022, 10:22 AM
Fingers crossed that they make it happen, Ive starred the issue Ill run through some more experiments in a couple of days.. if I find anything of value Ill share that here 🙂
I got an opportunity to experiment with this a bit more, and I was able to blur content all the way back to API 21! :blob-smile-happy: My use case is much simpler than those in the thread you linked though, I only want to render/blur the content once and then show the blurred piece statically. Anyhow, it works, and I thought Id update here in case anyone else is looking to make this happen!
c

Colton Idle

08/31/2022, 11:33 AM
Definitely curious to hear more!
z

Zoltan Demant

08/31/2022, 11:47 AM
I pass my @Composable content to a ComposeView, render that view to a bitmap, and blur the bitmap using RenderScript!
@Composable
fun Capture(
    key: Int = currentCompositeKeyHash,
    context: Context = LocalContext.current,
    transformation: Transformation = NoTransformation,
    content: @Composable () -> Unit,
    onBitmapCaptured: (Bitmap) -> Unit,
) {
    val cachedBitmap = BitmapCache[key]

    if (cachedBitmap != null) {
        onBitmapCaptured(cachedBitmap)
        return
    }

    val scope = rememberCoroutineScope()

    val view = remember(key) {
        ComposeView(context).apply { visibility = INVISIBLE }
    }

    LaunchedEffect(view) {
        <http://view.post|view.post> {
            scope.launch {
                val bitmap = withContext(Default) {
                    view.createTransformedBitmap(transformation)
                }

                BitmapCache[key] = bitmap
                onBitmapCaptured(bitmap)
            }
        }
    }

    AndroidView(
        factory = {
            view.apply { setContent(content) }
        },
    )
}
I use this for a couple of things other than blurring, so the transformation code is a bit generic
I found this on stackoverflow for blurring using RenderScript 👍🏽 Its been flawless thus far, tested on 4 devices in total.
var script: RenderScript? = null
var input: Allocation? = null
var output: Allocation? = null
var blur: ScriptIntrinsicBlur? = null
try {
    script = RenderScript.create(context)
    script.messageHandler = RSMessageHandler()

    input = createFromBitmap(
        script,
        bitmap,
        MIPMAP_NONE,
        USAGE_SCRIPT,
    )

    output = createTyped(
        script,
        input.type,
    )

    blur = create(
        script,
        U8_4(script),
    )

    blur.setInput(input)
    blur.setRadius(radius)
    blur.forEach(output)
    output.copyTo(bitmap)
} finally {
    script?.destroy()
    input?.destroy()
    output?.destroy()
    blur?.destroy()
}
return bitmap
I did run into issues when using the above code in a pager (width/height == 0), rewrote it to this and it works great. I think the view.post call was the culprit.
@Composable
fun Capture(
    key: Int = currentCompositeKeyHash,
    transformation: Transformation = NoTransformation,
    content: @Composable () -> Unit,
    callback: (Bitmap) -> Unit,
) {
    val cachedBitmap = BitmapCache[key]

    if (cachedBitmap != null) {
        callback(cachedBitmap)
        return
    }

    val scope = rememberCoroutineScope()

    AndroidView(
        factory = { context ->
            ComposeView(context).apply {
                visibility = INVISIBLE

                captureBitmap(
                    scope = scope,
                    transformation = transformation,
                    content = content,
                    callback = { bitmap ->
                        BitmapCache[key] = bitmap
                        callback(bitmap)
                    },
                )
            }
        },
    )
}

private inline fun ComposeView.captureBitmap(
    scope: CoroutineScope,
    transformation: Transformation,
    noinline content: @Composable () -> Unit,
    crossinline callback: (Bitmap) -> Unit,
) {
    setContent(content)

    val listener = object : OnGlobalLayoutListener {
        override fun onGlobalLayout() {
            viewTreeObserver.removeOnGlobalLayoutListener(this)

            scope.launch {
                val bitmap = withContext(Default) {
                    createTransformedBitmap(transformation)
                }

                callback(bitmap)
            }
        }
    }

    viewTreeObserver.addOnGlobalLayoutListener(listener)
}

private suspend fun View.createTransformedBitmap(
    transformation: Transformation,
): Bitmap {
    val bitmap = createBitmap(
        width,
        height,
        ARGB_8888,
    )

    val canvas = Canvas(bitmap)
    draw(canvas)

    return transformation.transform(
        context = context,
        bitmap = bitmap,
    )
}
c

Colton Idle

08/31/2022, 3:21 PM
Awesome. I have never used renderscript so that does seem a little daunting to me. but i will give it a shot next time i need it
m

mgrazianodecastro

09/08/2022, 4:11 AM
Could you share a Gist on git for the code? I'm trying to understand all the moving parts on the effect, but I'm having a hard time
z

Zoltan Demant

09/08/2022, 4:30 AM
Sure! The code has changed a bit since my last comment on here, but its still pretty compact. I use SubcomposeLayout in order to get the available size without having to show the non-blurred view to the user, you might want to do something different there depending on your use-case. Id also suggest caching the resulting bitmap if youre using any type of LazyLayout, otherwise it will get recreated as the blur content enters the composition again, etc. Good luck 🙏🏽 https://gist.github.com/zoltish/1705dc27a3a35ef2731b2ef018aac2e0
m

mgrazianodecastro

09/09/2022, 1:50 AM
Thanks man, appreciate