Creating an iPhone-Like Glassy Blur Effect with Jetpack Compose on Android
Enhancing User Interfaces with a Gentle Translucency and Harmonious Blurs
In this article, I will introduce you to a library that simplifies the process of achieving our desired iPhone like blur effect. We will explore how to seamlessly integrate our own code changes (wrapper), allowing for easy incorporation of the blur effect into our composables. By doing so, we will unlock a captivating and distinctive visual experience that is truly one of a kind.
To begin, we will add the required dependency by following the instructions provided in the repository.
allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}
implementation 'com.github.jakhongirmadaminov:glassmorphic-composables:0.0.4'
Idea
The thought behind this approach is to develop a composable that can be seamlessly applied as a wrapper around our own composables, enabling us to effortlessly achieve a blurred background for the target view (in our case the background)
@Composable
fun FullSizeBlur(
modifier: Modifier = Modifier,
alpha: Float = 1F,
scrollState: ScrollState = rememberScrollState(),
captureController: CaptureController = rememberCaptureController(),
wallpaperResource: Int = R.drawable.stock_wallapaper,
blurRadius: Int = 100,
color: Color = colorResource(id = R.color.gray),
scale: Float = 1f,
strokeWidth: Float = 1f,
content: @Composable () -> Unit
)
This composable offers the ability to effortlessly create a seamless and customizable indefinite blur background for any full-sized scroll-able view.
Through the utilization of default parameter values, developers gain the ability to alter properties such as blur radius, color, and stroke width. This grants them the freedom to tailor the blur effect precisely to their preferences, achieved through the implementation of the Draw Path method.
And, you can easily set your preferred image as the wallpaper resource, which will be elegantly blurred to serve as the background for your full-size composable.
Fast Blur
The next step is to create the blurred effect for the image resource passed in the above function.
Capturable(
controller = captureController,
onCaptured = { bitmap, _ ->
bitmap?.let {
fastblur(it.asAndroidBitmap(), scale, blurRadius)?.let {
fastBlurred ->
capturedBitmap = fastBlurred
}
}
}
) {
Image(
painter = painterResource(id = wallpaperResource),
contentDescription = "wallpaper",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
}
LaunchedEffect(key1 = true, block = {
withContext(Dispatchers.Main) {
if (capturedBitmap == null) captureController.capture()
}
})
Here, we capture the blurred effect for the image passed using the library function Fast Blur (taken from Stack Blur) which is a compromise between Gaussian Blur and Box Blur effects.
It creates much better looking blurs than Box Blur and was created back in 2004 by Mario Klingemann who wrote the Stack Blur Algorithm
The Wrapper
Originally designed to apply blur backgrounds to individual items within a list, the library serves as the foundation for our wrapper. However, we extend its functionality to cater specifically to a targeted composable that occupies the entire screen size. This refined approach allows us to achieve a the effect exclusively for the desired composable (Second image in cover photo)
The library accepts a parameter called childMeasures
which is a list of Place
(lib class) objects that have the position and offset for the items in the list that need to be applied with the desired effect.
As we intend to only have one item in our list as we can see in the second image above, we can create our child measures in the following way and populate it using the onGloballyPositioned
modifier.
val childMeasures = remember { mutableStateListOf(Place()) }
Modifier
.onGloballyPositioned {
childMeasures[0].apply {
this.size = IntSize(it.size.width, it.size.height)
this.offset = Offset(
it.positionInParent().x,
it.positionInParent().y
)
}
}
The library supports two composables GlassmorphicRow
/ GlassmorphicColumn
We can use either as we just have one item in our list.
capturedBitmap?.let { capturedImage ->
GlassmorphicColumn(
scrollState = scrollState,
childMeasures = childMeasures,
targetBitmap = capturedImage,
dividerSpace = 10,
blurRadius = blurRadius,
drawOnTop = { path ->
drawPath(
path = path,
color = color,
style = Stroke(strokeWidth),
blendMode = BlendMode.Overlay
// Can even have BlendMode.Plus,
// .Screen or .Lumniosity here
)
},
content = {
Box(
modifier = Modifier
.onGloballyPositioned {
childMeasures[0].apply {
this.size = IntSize(it.size.width, it.size.height)
this.offset = Offset(
it.positionInParent().x,
it.positionInParent().y
)
}
}
) {
content()
}
}
)
}
Capturable and Glassmorphic composables must share the same parent Composable like a Box (Note from the library author)
Sample Usage
FullSizeBlur{
Column(
modifier = Modifier
.fillMaxSize()
.height(LocalConfiguration.current.screenHeightDp.dp)
) {
// Code that needs to have the blur background goes here ...
// You can even set an alpha value on your code here so that
// the blur is visible through this composable
Card(modifier = Modifier.alpha(0.6F)) {
// Card content here ...
}
}
}
Complete Code
Here’s how the final Blur composable file should look like. Imports at the bottom. One can modify this code furthermore to support smaller size blurred composables with non-blurred backgrounds (example)
@Composable
fun FullSizeBlur(
modifier: Modifier = Modifier,
alpha: Float = 1F,
scrollState: ScrollState = rememberScrollState(),
captureController: CaptureController = rememberCaptureController(),
wallpaperResource: Int = R.drawable.stock_wallapaper,
blurRadius: Int = 100,
color: Color = colorResource(id = R.color.gray),
scale: Float = 1f,
strokeWidth: Float = 1f,
content: @Composable () -> Unit
) {
var capturedBitmap by remember { mutableStateOf<Bitmap?>(null) }
Box(
modifier = modifier
.fillMaxSize()
.alpha(alpha)
) {
Capturable(
controller = captureController,
onCaptured = { bitmap, _ ->
bitmap?.let {
fastblur(it.asAndroidBitmap(), scale, blurRadius)?.let { fastBlurred ->
capturedBitmap = fastBlurred
}
}
}
) {
Image(
painter = painterResource(id = wallpaperResource),
contentDescription = "wallpaper",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
}
LaunchedEffect(key1 = true, block = {
withContext(Dispatchers.Main) {
if (capturedBitmap == null) captureController.capture()
}
})
val childMeasures = remember { mutableStateListOf(Place()) }
capturedBitmap?.let { capturedImage ->
GlassmorphicColumn(
scrollState = scrollState,
childMeasures = childMeasures,
targetBitmap = capturedImage,
blurRadius = blurRadius,
drawOnTop = { path ->
drawPath(
path = path,
color = color,
style = Stroke(strokeWidth),
)
},
content = {
Box(
modifier = Modifier
.onGloballyPositioned {
childMeasures[0].apply {
this.size = IntSize(it.size.width, it.size.height)
this.offset = Offset(
it.positionInParent().x,
it.positionInParent().y
)
}
}
) {
content()
}
}
)
}
}
}
import android.graphics.Bitmap
import androidx.compose.foundation.Image
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.IntSize
import dev.jakhongirmadaminov.glassmorphiccomposables.GlassmorphicColumn
import dev.jakhongirmadaminov.glassmorphiccomposables.Place
import dev.jakhongirmadaminov.glassmorphiccomposables.fastblur
import dev.shreyaspatil.capturable.Capturable
import dev.shreyaspatil.capturable.controller.CaptureController
import dev.shreyaspatil.capturable.controller.rememberCaptureController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
Credits to the repository creator!
If you found this article informative and inspiring, feel free to leave your thoughts and feedback in the comments section below. Keep building and may your user interfaces always captivate with their alluring blur effect!