How to Create an Inner Shadow in Jetpack Compose

Kappdev
5 min readNov 27, 2023

--

Welcome 👋

In this article, we’ll create an Inner Shadow modifier in Jetpack Compose.

Additionally, we’ll craft practical examples to showcase how it can be used to enhance the UI.

Let’s dive in 🚀

Defining the Function

Let’s begin by defining an extension function innerShadow for the Modifier , which will render the effect:

fun Modifier.innerShadow(
shape: Shape,
color: Color = Color.Black,
blur: Dp = 4.dp,
offsetY: Dp = 2.dp,
offsetX: Dp = 2.dp,
spread: Dp = 0.dp
)

Parameters

  • shape 👉 The shape of the shadow.
  • color 👉 The color of the shadow.
  • blur 👉 The blur radius of the shadow.
  • offsetY 👉 The shadow offset along the Y-axis.
  • offsetX 👉 The shadow offset along the X-axis.
  • spread 👉 The amount to expand the shadow beyond its size.

Implementation

To draw the shadow on top of the content, we utilize the drawWithContent() modifier and draw the content by calling drawContent():

fun Modifier.innerShadow(
// Parameters...
) = this.drawWithContent {
drawContent() // Drawing the content
}

Then, to draw the shadow directly onto the Canvas, we utilize drawIntoCanvas:

drawIntoCanvas { canvas ->
// Rest of the implementation...
}

Next, we need to calculate the size of the shadow based on the DrawScope size and the spread :

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)

Alright, finally we can draw the inner shadow:

// Initialize a Paint object
val paint = Paint()
paint.color = color

// Save the current layer of the canvas
canvas.saveLayer(size.toRect(), paint)
// Draw the outline of the shadow onto the canvas
canvas.drawOutline(shadowOutline, paint)

// Configure the paint to act as an eraser to clip the shadow
paint.asFrameworkPaint().apply {
xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)
if (blur.toPx() > 0) {
maskFilter = BlurMaskFilter(blur.toPx(), BlurMaskFilter.Blur.NORMAL)
}
}

// Set clipping color
paint.color = Color.Black

// Translate the canvas to the offset position
canvas.translate(offsetX.toPx(), offsetY.toPx())
// Draw the outline again to clip the shadow
canvas.drawOutline(shadowOutline, paint)
// Restore the canvas
canvas.restore()

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.

Are you learning a foreign language and struggling with new vocabulary?

Then, I strongly recommend you check out this words-learning application, which will make your journey easy and convenient!

WordBook

Practical Usage 💁‍♂️

The examples below use custom colors. Here they are:

val ShadowBlack = Color.Black.copy(0.56f)
val ShadowWhite = Color.White.copy(0.56f)
val Red = Color(0xFFE91E63)

Convex Effect

This example demonstrates how the innerShadow function can be used to create a convex effect:

Text(
text = "Follow",
color = Color.White,
modifier = Modifier
// Draw the background.
.background(Red, CircleShape)
// Shadow effect
.innerShadow(
shape = CircleShape, color = ShadowBlack,
offsetY = (-2).dp, offsetX = (-2).dp
)
// Glare effect
.innerShadow(
shape = CircleShape, color = ShadowWhite,
offsetY = 2.dp, offsetX = 2.dp
)
.padding(vertical = 8.dp, horizontal = 16.dp)
)

Output:

Concave Effect

This example demonstrates how the innerShadow function can be used to create a concave effect. It is similar to the convex effect but with switched colors:

Text(
text = "Hello World",
color = Color.Black.copy(0.5f),
modifier = Modifier
// Glare effect
.innerShadow(
shape = CircleShape, color = ShadowWhite,
offsetY = (-2).dp, offsetX = (-2).dp
)
// Shadow effect
.innerShadow(
shape = CircleShape, color = ShadowBlack,
offsetY = 2.dp, offsetX = 2.dp
)
.padding(16.dp)
)

Output:

Pumping Heart

In this advanced example, we’ll implement a heart-shaped button with a pumping animation effect triggered upon clicking.

Let’s start by defining a custom heart shape using GenericShape:

val HeartShape = GenericShape { size, _ ->
val height = size.height
val width = size.width

moveTo(width / 2, height / 4)
cubicTo(
x1 = width - (width / 4), y1 = 0f,
x2 = width, y2 = width / 3,
x3 = width / 2, y3 = height - (height / 3.5f)
)
cubicTo(
x1 = 0f, y1 = width / 3,
x2 = width / 4, y2 = 0f,
x3 = width / 2, y3 = height / 4
)
close()
}

Now, we can render the heart with this shape and a convex effect:

Box(
modifier = Modifier
.size(200.dp)
.background(Red, HeartShape)
.innerShadow(
shape = HeartShape, color = ShadowBlack,
blur = 8.dp,
offsetY = (-4).dp, offsetX = (-4).dp
)
.innerShadow(
shape = HeartShape, color = ShadowWhite,
blur = 8.dp,
offsetY = 4.dp, offsetX = 4.dp
)
)

Finally, let’s add the animation:

val interactionSource = remember { MutableInteractionSource() }
val pressed by interactionSource.collectIsPressedAsState()

val heartScale by animateFloatAsState(
targetValue = if (pressed) 1.1f else 1f,
animationSpec = spring(dampingRatio = Spring.DampingRatioHighBouncy),
label = "Heart scale"
)

Box(
modifier = Modifier
.size(200.dp)
.scale(heartScale)
.clickable(
interactionSource = interactionSource,
indication = null,
onClick = { /*...*/ }
)
// Other modifiers
)

Awesome! 😍

You might also like 👇

Thank you for reading this article! ❤️ If you found it enjoyable and valuable, show your appreciation by clapping 👏 and following Kappdev for more exciting articles 😊

Consider subscribing to my email notifications 🔔 to stay updated with my latest content.

Happy coding!

--

--

Kappdev

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