Animate borders in Jetpack Compose

Ruthwik
ProAndroidDev
Published in
5 min readSep 12, 2023

--

Animation has always been my absolute favourite. Today, we will cover how to do a nice gradient border animation in Jetpack Compose. In this article, I’ll walk you through the thinking process, which is crucial so you can do similar things independently. Simply reviewing the accompanying images will suffice to kickstart your logic. The final result will be something like this.

So, let’s begin...

Before we jump into actual code, think about how it will be done. Before animating, you need a rounder corner shape with a border. You will be thinking about the RoundedCorner() shape we can pass as a parameter, and it's done. Imagine we’re working with a limited canvas where we can only draw basic shapes. How would we draw a border around a simple rectangle? Just think for a moment...

The solution lies in using fundamentals…

Yeah, I’ll draw a rounder corner rectangle. Then..

Then, draw another rectangle with a different colour, with a slight padding. Cool, let’s draw

val colorBg = Color(0xFF2C3141)

Canvas(modifier = Modifier.fillMaxWidth().height(200.dp).background(colorBg)) {
drawRoundRect(
color = Color.White,
cornerRadius = CornerRadius(x = 20.dp.toPx(), y = 20.dp.toPx())
)

drawRoundRect(
color = colorBg,
topLeft = Offset(1.dp.toPx(), 1.dp.toPx()),
size = Size(
width = size.width - 2.dp.toPx(),
height = size.height - 2.dp.toPx()
),
cornerRadius = CornerRadius(
x = 19.dp.toPx(),
y = 19.dp.toPx()
)
)
}

This will draw two rounded rectangles on top of each other. The second one will be drawn from a slight offset, which equals border thickness, and we will get the following result.

Good, we’re doing well so far. Next, how will we manage to draw a gradient border? Hmm.. think..

Yeah, I’ll draw the outer rectangle with a gradient brush. Then inner rectangle... Easy as pie, right?

Draw the outer rectangle with gradient fill, then draw the inner rectangle with a slight padding and fill the background colour.

Yes, let’s code it.

val colorBg = Color(0xFF2C3141)
val colors =
listOf(
Color(0xFFFF595A),
Color(0xFFFFC766),
Color(0xFF35A07F),
Color(0xFF35A07F),
Color(0xFFFFC766),
Color(0xFFFF595A)
)

val brush = Brush.linearGradient(colors)

Canvas(modifier = Modifier.fillMaxWidth().height(200.dp).background(colorBg)) {
drawRoundRect(
brush = brush,
cornerRadius = CornerRadius(x = 20.dp.toPx(), y = 20.dp.toPx()
)
)

drawRoundRect(
color = colorBg,
topLeft = Offset(1.dp.toPx(), 1.dp.toPx()),
size = Size(
width = size.width - 2.dp.toPx(),
height = size.height - 2.dp.toPx()
),
cornerRadius = CornerRadius(
x = 19.dp.toPx(),
y = 19.dp.toPx()
)
)
}

To explain the animation step simply, let me take a different approach to achieve the same. Instead of drawing an outer rectangle with gradient fill, I’m drawing a circle inside the canvas, adding clipToBounds() to the canvas modifier to prevent drawing the circle outside the canvas. Instead of a linearGradient() brush, I’ll use a sweepGradient() brush.

Let’s see the code…

val brush = Brush.sweepGradient(colors)

Canvas(modifier = Modifier.fillMaxWidth().height(200.dp).background(colorBg)) {

drawCircle(
brush = brush,
radius = size.width,
blendMode = BlendMode.SrcIn,
)

drawRoundRect(
color = colorBg,
topLeft = Offset(1.dp.toPx(), 1.dp.toPx()),
size = Size(
width = size.width - 2.dp.toPx(),
height = size.height - 2.dp.toPx()
),
cornerRadius = CornerRadius(
x = 19.dp.toPx(),
y = 19.dp.toPx()
)
)
}

And the result is kind of the same… see…

Now, imagine I’m rotating the background drawn circle. What happens?

Voila! it's done. We made a cool border gradient animation.

Here in the code, I used a simple animateFloat() for changing the value from 0 to 360 and rotate() to rotate the circle by an angle in the canvas.

val colorBg = Color(0xFF2C3141)
val colors =
listOf(
Color(0xFFFF595A),
Color(0xFFFFC766),
Color(0xFF35A07F),
Color(0xFF35A07F),
Color(0xFFFFC766),
Color(0xFFFF595A)
)

val brush = Brush.sweepGradient(colors)

val infiniteTransition = rememberInfiniteTransition(label = "")
val angle by
infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec =
infiniteRepeatable(
animation = tween(1500, easing = LinearEasing),
repeatMode = RepeatMode.Restart
),
label = ""
)

Canvas(modifier = Modifier.fillMaxWidth().height(200.dp).background(colorBg)) {


rotate(degrees = angle) {
drawCircle(
brush = brush,
radius = size.width,
blendMode = BlendMode.SrcIn,
)
}

drawRoundRect(
color = colorBg,
topLeft = Offset(1.dp.toPx(), 1.dp.toPx()),
size = Size(
width = size.width - 2.dp.toPx(),
height = size.height - 2.dp.toPx()
),
cornerRadius = CornerRadius(
x = 19.dp.toPx(),
y = 19.dp.toPx()
)
)
}

The end result is like this!

I’m creating a custom card Composable using this technique to be useful in a real application. The code is self-explanatory, and you will get the end result as the first animation in this article.

@Composable
fun CardWithAnimatedBorder(
modifier: Modifier = Modifier,
onCardClick: () -> Unit = {},
borderColors: List<Color> = emptyList(),
content: @Composable () -> Unit
) {
val infiniteTransition = rememberInfiniteTransition()
val angle by
infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec =
infiniteRepeatable(
animation = tween(1500, easing = LinearEasing),
repeatMode = RepeatMode.Restart
)
)

val brush =
if (borderColors.isNotEmpty()) Brush.sweepGradient(borderColors)
else Brush.sweepGradient(listOf(Color.Gray, Color.White))

Surface(modifier = modifier.clickable { onCardClick() }, shape = RoundedCornerShape(20.dp)) {
Surface(
modifier =
Modifier.clipToBounds().fillMaxWidth().padding(1.dp).drawWithContent {
rotate(angle) {
drawCircle(
brush = brush,
radius = size.width,
blendMode = BlendMode.SrcIn,
)
}
drawContent()
},
color = HushTheme.colors.backgroundColors.backgroundGlassPrimary,
shape = RoundedCornerShape(19.dp)
) {
Box(modifier = Modifier.padding(8.dp)) { content() }
}
}
}

I appreciate your time and attention. Thanks for reading

--

--