Custom Drop Shadow from Figma in Jetpack Compose: For Any Shape.

Kappdev
5 min readMay 4, 2024

--

Welcome 👋

In this article, we’ll craft a custom drop shadow modifier for Jetpack Compose that reflects the properties from Figma. This lets you effortlessly translate the design into code, with complete control over the shadow’s color, offset, spread, and blur radius.

The best benefit is that this modifier works with any shape in Jetpack Compose, giving you ultimate flexibility 🤸🏻‍♂️

Defining function

First things first, let’s define the dropShadow extension function for the Modifier :

fun Modifier.dropShadow(
shape: Shape,
color: Color = Color.Black.copy(0.25f),
blur: Dp = 4.dp,
offsetY: Dp = 4.dp,
offsetX: Dp = 0.dp,
spread: Dp = 0.dp
)

⚒️ ️️Parameters breakdown

shape ➜ Defines the shape of the shadow.

color ➜ Specifies the color of the shadow.

blur ➜ Controls the blur radius of the shadow.

offsetY ➜ Shifts the shadow along the Y-axis.

offsetX ➜ Shifts the shadow along the X-axis.

spread ➜ Increases the size of the shadow.

Implementation

To draw the shadow behind the actual composable we’ll utilize the drawBehind Modifier.

fun Modifier.dropShadow(
/* Parameters */
) = this.drawBehind {
/* Implementation goes here… */
}

Next, we need to calculate the size of the shadow, which depends on the DrawScope size (composable) and the spread value.

val shadowSize = Size(size.width + spread.toPx(), size.height + spread.toPx())

With the size calculated, we can create an Outline of the defined shape:

val shadowOutline = shape.createOutline(shadowSize, layoutDirection, this)

The last missing piece to draw the shadow is a Paint object. Let’s define it:

// Create a Paint object
val paint = Paint()
// Apply specified color
paint.color = color

// Check for valid blur radius
if (blur.toPx() > 0) {
paint.asFrameworkPaint().apply {
// Apply blur to the Paint
maskFilter = BlurMaskFilter(blur.toPx(), BlurMaskFilter.Blur.NORMAL)
}
}

Finally, we can draw the shadow 👩‍🎨

drawIntoCanvas { canvas ->
// Save the canvas state
canvas.save()
// Translate to specified offsets
canvas.translate(offsetX.toPx(), offsetY.toPx())
// Draw the shadow
canvas.drawOutline(shadowOutline, paint)
// Restore the canvas state
canvas.restore()
}

📌 Note: To avoid affecting subsequent elements, we have to save the canvas state before the modifications and restore it afterward.

Congratulations 🥳! We’ve successfully built it 👏. For the complete code implementation, you can access it on GitHub Gist 🧑‍💻. Now, let’s explore how we can put it to use.

Advertisement

Are you learning a foreign language and struggling with new vocabulary? Then, I strongly recommend you check out this words-learning app, which will make your journey easy and convenient!

WordBook

Practical example 💁

As an example, we’ll create a captivating counter, which you have seen on the thumbnail.

To promote code clarity and reusability, let’s create a doubleShadowDrop utility function:

fun Modifier.doubleShadowDrop(
shape: Shape,
offset: Dp = 4.dp,
blur: Dp = 8.dp
) = this
.dropShadow(shape, Color.Black.copy(0.25f), blur, offset, offset)
.dropShadow(shape, Color.White.copy(0.25f), blur, -offset, -offset)

Leveraging the utility function, let’s create a custom ElevatedButton composable that will power our buttons:

@Composable
fun ElevatedButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
shape: Shape = RoundedCornerShape(8.dp),
content: @Composable BoxScope.() -> Unit
) {
val interSource = remember { MutableInteractionSource() }
val isPressed by interSource.collectIsPressedAsState()

// Animate shadow offset and blur radius
// To create a hide shadow animation on press
val shadowOffset by animateDpAsState(
targetValue = if (isPressed) 0.dp else 4.dp
)
val shadowBlur by animateDpAsState(
targetValue = if (isPressed) 0.dp else 8.dp
)

Box(
modifier = modifier
// Apply shadow effect
.doubleShadowDrop(shape, shadowOffset, shadowBlur)
.background(Color(0xFF010203), shape)
.clip(shape)
.clickable(
interactionSource = interSource,
indication = LocalIndication.current,
onClick = onClick
),
contentAlignment = Alignment.Center,
content = content
)
}

The last component left to be crafted is a counter panel.

To enhance the visual appeal of the panel, we’ll use the SevenSegmentView. For a detailed explanation, refer to my related article provided below 👇 or find the code on 👉 GitHub Gist

@Composable
fun CounterPanel(
count: Int,
modifier: Modifier = Modifier,
shape: Shape = RoundedCornerShape(16.dp)
) {
Box(
modifier = modifier
// Apply shadow effect
.doubleShadowDrop(shape)
.background(Color(0xFF010203), shape),
contentAlignment = Alignment.Center
) {
SevenSegmentView(
number = count,
digitsNumber = 4,
segmentsSpace = 1.dp,
segmentWidth = 8.dp,
digitsSpace = 16.dp,
activeColor = Color(0xFF41CC41),
inactiveColor = Color(0xFF41CC41).copy(0.32f),
modifier = Modifier.padding(16.dp).height(84.dp)
)
}
}

Finally, we can put it all together 🔩

var count by remember { mutableIntStateOf(0) }

Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.width(250.dp)
) {

CounterPanel(
count = count,
modifier = Modifier.fillMaxWidth()
)

Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {

ElevatedButton(
onClick = { count-- },
modifier = Modifier.size(50.dp)
) {
Icon(
painter = painterResource(R.drawable.ic_remove),
contentDescription = null,
tint = Color.White
)
}

ElevatedButton(
onClick = { count = 0 },
modifier = Modifier
.height(50.dp)
.weight(1f)
) {
Text(
text = "Reset",
color = Color.White
)
}

ElevatedButton(
onClick = { count++ },
modifier = Modifier.size(50.dp)
) {
Icon(
painter = painterResource(R.drawable.ic_add),
contentDescription = null,
tint = Color.White
)
}
}
}

Check out the result 😍

You might also like 👇

Thank you for reading this article!❤️ I hope you’ve found it enjoyable and valuable. Feel free to show your appreciation by hitting the clap👏 if you liked it or follow Kappdev for more exciting articles😊

Happy coding!

--

--

Kappdev

💡 Curious Explorer 🧭 Kotlin and Compose enthusiast 👨‍💻 Passionate about self-development and growth ❤️‍🔥 Push your boundaries 🚀