How to Create a Stunning 3D Border in Jetpack Compose

Kappdev
4 min readJun 6, 2024

Welcome 👋

In this article, we will create an amazing 3D Border Modifier for Jetpack Compose that can be applied to any view with any shape. Additionally, we’ll build a beautiful search bar using this modifier.

Let’s dive in! 🚀

Convex Border

Let’s start by defining the main convexBorder extension function for the Modifier, which ultimately draws the convex border.

ConvexStyle

Before that, to enhance clarity, we craft a ConvexStyle data class to represent the style of the convex effect applied to the border.

data class ConvexStyle(
val blur: Dp = 3.dp,
val offset: Dp = 2.dp,
val glareColor: Color = Color.White.copy(0.64f),
val shadowColor: Color = Color.Black.copy(0.64f)
)

The Function

Now, we have everything set up to define the function:

fun Modifier.convexBorder(
color: Color,
shape: Shape,
strokeWidth: Dp = 8.dp,
convexStyle: ConvexStyle = ConvexStyle()
)

⭐ color ➜ The color of the border.

⭐ shape ➜ The shape of the border.

⭐ strokeWidth ➜ The width of the border stroke.

⭐ convexStyle ➜ The style of the convex effect applied to the border.

Implementation

Alright, now we can proceed to the implementation.

Drawing Shadow and Glare

Before implementing the convexBorder function, we need to define a support function drawConvexBorderShadow, which will draw shadows to create a convex effect.

fun DrawScope.drawConvexBorderShadow(
outline: Outline,
strokeWidth: Dp,
blur: Dp,
offsetX: Dp,
offsetY: Dp,
shadowColor: Color
) = drawIntoCanvas { canvas ->
// Create and set up a Paint object
val shadowPaint = Paint().apply {
this.style = PaintingStyle.Stroke
this.color = shadowColor
this.strokeWidth = strokeWidth.toPx()
}

// Save the current layer before transformations
canvas.saveLayer(size.toRect(), shadowPaint)

val halfStrokeWidth = strokeWidth.toPx() / 2
// Translate the canvas to fit the border within its bounderies
canvas.translate(halfStrokeWidth, halfStrokeWidth)
// Draw the shadow outline
canvas.drawOutline(outline, shadowPaint)

// Apply blending mode and blur effect for the shadow
shadowPaint.asFrameworkPaint().apply {
xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)
maskFilter = BlurMaskFilter(blur.toPx(), BlurMaskFilter.Blur.NORMAL)
}
// Set the color for clipping
shadowPaint.color = Color.Black

// Translate canvas and draw the shadow clipping outline
canvas.translate(offsetX.toPx(), offsetY.toPx())
canvas.drawOutline(outline, shadowPaint)
// Restore canvas to its original state
canvas.restore()
}

To understand better how this function works, take a look at the picture below 👇

The convexBorder Implementation

Now, equipped with the drawConvexBorderShadow function, we can define the main function to draw the convex border.

fun Modifier.convexBorder(
/* Parameters... */
) = this.drawWithContent {
// Adjust the size to fit within canvas boundaries
val adjustedSize = Size(size.width - strokeWidth.toPx(), size.height - strokeWidth.toPx())
// Create an outline based on the shape and adjusted size
val outline = shape.createOutline(adjustedSize, layoutDirection, this)

// Draw the original content of the composable
drawContent()

// Translate the canvas to fit the border within its bounderies
translate(halfStrokeWidth, halfStrokeWidth) {
// Draw main border outline
drawOutline(
outline = outline,
color = color,
style = Stroke(width = strokeWidth.toPx())
)
}

with(convexStyle) {
// Draw the shadow outline
drawConvexBorderShadow(outline, strokeWidth, blur, -offset, -offset, shadowColor)
// Draw the glare outline
drawConvexBorderShadow(outline, strokeWidth, blur, offset, offset, glareColor)
}
}

Here is how it works 👇

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 create a beautiful custom search bar utilizing this function.

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 💁

Alright, let’s step into the practical side of today’s article.

To write a custom-styled TextField, we can leverage the decorationBox parameter of the BasicTextField.

// Mutable state to hold the text input
var text by remember { mutableStateOf("") }

BasicTextField(
value = text,
onValueChange = { text = it },
singleLine = true,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Sentences,
imeAction = ImeAction.Search
),
textStyle = LocalTextStyle.current.copy(
fontSize = 16.sp,
fontWeight = FontWeight.Medium
),
decorationBox = { innerTextField ->
Row(
modifier = Modifier
.size(350.dp, 60.dp)
// Set the background color and shape
.background(Color(0xFF7F2DBF), CircleShape)
// Apply the convex border with the same color and shape
.convexBorder(Color(0xFF7F2DBF), CircleShape)
.padding(horizontal = 20.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
// Add a search icon
Icon(
imageVector = Icons.Rounded.Search,
contentDescription = null
)
Box {
// Show placeholder text when the input text is empty
if (text.isEmpty()) {
Text(
text = "Search...",
style = LocalTextStyle.current.copy(color = Color(0xFF242424))
)
}
// Display the actual text field
innerTextField()
}
}
}
)

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 and follow

for more exciting articles 😊

Happy coding!

--

--

Kappdev

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