Im trying to blur some content in compose. `Modifi...
# compose
z
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
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
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
I don't think that's possible? at least not from what i understand.
z
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
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
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
Definitely curious to hear more!
z
I pass my @Composable content to a ComposeView, render that view to a bitmap, and blur the bitmap using RenderScript!
Copy code
@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.
Copy code
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.
Copy code
@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
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
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
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
Thanks man, appreciate