How to make Rainbow border animation for box in compose, all the examples that I saw are for Circle and they use rotate with drawbehind, but what I really need is to make the same for a Box in compose.
Thanks
With rotate but it didn´t work
How to make Rainbow border animation for box in compose, all the examples that I saw are for Circle and they use rotate with drawbehind, but what I really need is to make the same for a Box in compose.
Thanks
With rotate but it didn´t work
This can be accomplished using BlendModes. If you are not familiar with BlendModes you can check out answers below.
Jetpack Compose Applying PorterDuffMode to Image
How to clip or cut a Composable?
Result
You need to create a Brush.sweepGradient with rainbow colors first
val gradientColors = listOf(
Color.Red,
Color.Magenta,
Color.Blue,
Color.Cyan,
Color.Green,
Color.Yellow,
Color.Red
)
Then need draw this sweep gradient as circle that overflows from our composable then we will draw a rectangle for borders with some color and apply BlendMode.SrcIn on circle to get rectangle shape with circle brush we rotate with infinite animation
fun Modifier.drawRainbowBorder(
strokeWidth: Dp,
durationMillis: Int
) = composed {
val infiniteTransition = rememberInfiniteTransition(label = "rotation")
val angle by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis, easing = LinearEasing),
repeatMode = RepeatMode.Restart
), label = "rotation"
)
val brush = Brush.sweepGradient(gradientColors)
Modifier.drawWithContent {
val strokeWidthPx = strokeWidth.toPx()
val width = size.width
val height = size.height
drawContent()
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
// Destination
drawRect(
color = Color.Gray,
topLeft = Offset(strokeWidthPx / 2, strokeWidthPx / 2),
size = Size(width - strokeWidthPx, height - strokeWidthPx),
style = Stroke(strokeWidthPx)
)
// Source
rotate(angle) {
drawCircle(
brush = brush,
radius = size.width,
blendMode = BlendMode.SrcIn,
)
}
restoreToCount(checkPoint)
}
}
}
If you wish to draw with Shape
you can use function below. This function needs to use Modifier.clip because drawing a Outline
that is smaller than original shape size with
val outline = shape.createOutline(
size = Size(
size.width - strokeWidthPx,
size.height - strokeWidthPx
),
layoutDirection = layoutDirection,
density = density
)
creates very small but noticeable blank space near corners with RoundedCornerShape and probably with other custom shapes depending how they are aligned. I checked out how Modifier.border prevents this, it checks for 3 shape types
when (val outline = shape.createOutline(size, layoutDirection, this)) {
is Outline.Generic ->
drawGenericBorder(
borderCacheRef,
brush,
outline,
fillArea,
strokeWidthPx
)
is Outline.Rounded ->
drawRoundRectBorder(
borderCacheRef,
brush,
outline,
topLeft,
borderSize,
fillArea,
strokeWidthPx
)
is Outline.Rectangle ->
drawRectBorder(
brush,
topLeft,
borderSize,
fillArea,
strokeWidthPx
)
}
When it's generic type it creates mask Path
which i don't intend to do for this example but if you don't want to clip content you can implement similar approach.
fun Modifier.drawAnimatedBorder(
strokeWidth: Dp,
shape: Shape,
brush: (Size) -> Brush = {
Brush.sweepGradient(gradientColors)
},
durationMillis: Int
) = composed {
val infiniteTransition = rememberInfiniteTransition(label = "rotation")
val angle by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis, easing = LinearEasing),
repeatMode = RepeatMode.Restart
), label = "rotation"
)
Modifier
.clip(shape)
.drawWithCache {
val strokeWidthPx = strokeWidth.toPx()
val outline: Outline = shape.createOutline(size, layoutDirection, this)
val pathBounds = outline.bounds
onDrawWithContent {
// This is actual content of the Composable that this modifier is assigned to
drawContent()
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
// Destination
// We draw 2 times of the stroke with since we want actual size to be inside
// bounds while the outer stroke with is clipped with Modifier.clip
// 🔥 Using a maskPath with op(this, outline.path, PathOperation.Difference)
// And GenericShape can be used as Modifier.border does instead of clip
drawOutline(
outline = outline,
color = Color.Gray,
style = Stroke(strokeWidthPx * 2)
)
// Source
rotate(angle) {
drawCircle(
brush = brush(size),
radius = size.width,
blendMode = BlendMode.SrcIn,
)
}
restoreToCount(checkPoint)
}
}
}
}
Usage
@Preview
@Composable
private fun AnimatedRainbowBorderSample() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.size(140.dp, 100.dp)
.drawRainbowBorder(
strokeWidth = 4.dp,
durationMillis = 3000
),
contentAlignment = Alignment.Center
) {
Text(text = "Hello World", fontSize = 20.sp)
}
Spacer(modifier = Modifier.height(10.dp))
Box(
modifier = Modifier
.drawAnimatedBorder(
strokeWidth = 4.dp,
durationMillis = 2000,
shape = RoundedCornerShape(10.dp)
)
.padding(12.dp),
contentAlignment = Alignment.Center
) {
Text(text = "Hello World", fontSize = 20.sp)
}
Spacer(modifier = Modifier.height(10.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
Box(
modifier = Modifier
.size(120.dp)
// .border(2.dp, Color.Black, RoundedCornerShape(20.dp))
.drawAnimatedBorder(
strokeWidth = 6.dp,
durationMillis = 3000,
shape = RoundedCornerShape(20.dp)
),
contentAlignment = Alignment.Center
) {
Image(
modifier = Modifier
.matchParentSize(),
painter = painterResource(id = R.drawable.avatar_1_raster),
contentDescription = null,
contentScale = ContentScale.FillBounds
)
}
Box(
modifier = Modifier
.size(120.dp)
.drawAnimatedBorder(
strokeWidth = 6.dp,
durationMillis = 3000,
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Image(
modifier = Modifier
.matchParentSize(),
painter = painterResource(id = R.drawable.avatar_2_raster),
contentDescription = null,
contentScale = ContentScale.FillBounds
)
}
}
Spacer(modifier = Modifier.height(10.dp))
Box(
modifier = Modifier
.size(80.dp)
.drawAnimatedBorder(
strokeWidth = 4.dp,
durationMillis = 2000,
shape = CircleShape
)
)
Spacer(modifier = Modifier.height(10.dp))
Box(
modifier = Modifier
.drawAnimatedBorder(
strokeWidth = 4.dp,
durationMillis = 2000,
shape = CutCornerShape(8.dp)
)
.padding(12.dp),
contentAlignment = Alignment.Center
) {
Text(text = "Hello World", fontSize = 20.sp)
}
Spacer(modifier = Modifier.height(10.dp))
Box(
modifier = Modifier
.drawAnimatedBorder(
strokeWidth = 4.dp,
durationMillis = 2000,
shape = createBubbleShape(
arrowWidth = 20f,
arrowHeight = 20f,
arrowOffset = 20f,
arrowDirection = ArrowDirection.Left
)
)
.padding(12.dp),
contentAlignment = Alignment.Center
) {
Text(text = "Hello World", fontSize = 20.sp)
}
Spacer(modifier = Modifier.height(10.dp))
Box(
modifier = Modifier
.drawAnimatedBorder(
brush = {
Brush.sweepGradient(
colors = listOf(
Color.Gray,
Color.White,
Color.Gray,
Color.White,
Color.Gray
)
)
},
strokeWidth = 4.dp,
durationMillis = 2000,
shape = RoundedCornerShape(10.dp)
)
.padding(12.dp),
contentAlignment = Alignment.Center
) {
Text(text = "Hello World", fontSize = 20.sp)
}
}
}
Extra bubble shape
fun createBubbleShape(
arrowWidth: Float,
arrowHeight: Float,
arrowOffset: Float,
arrowDirection: ArrowDirection
): GenericShape {
return GenericShape { size: Size, layoutDirection: LayoutDirection ->
val width = size.width
val height = size.height
when (arrowDirection) {
ArrowDirection.Left -> {
moveTo(arrowWidth, arrowOffset)
lineTo(0f, arrowOffset)
lineTo(arrowWidth, arrowHeight + arrowOffset)
addRoundRect(
RoundRect(
rect = Rect(left = arrowWidth, top = 0f, right = width, bottom = height),
cornerRadius = CornerRadius(x = 20f, y = 20f)
)
)
}
ArrowDirection.Right -> {
moveTo(width - arrowWidth, arrowOffset)
lineTo(width, arrowOffset)
lineTo(width - arrowWidth, arrowHeight + arrowOffset)
addRoundRect(
RoundRect(
rect = Rect(
left = 0f,
top = 0f,
right = width - arrowWidth,
bottom = height
),
cornerRadius = CornerRadius(x = 20f, y = 20f)
)
)
}
ArrowDirection.Top -> {
moveTo(arrowOffset, arrowHeight)
lineTo(arrowOffset + arrowWidth / 2, 0f)
lineTo(arrowOffset + arrowWidth, arrowHeight)
addRoundRect(
RoundRect(
rect = Rect(
left = 0f,
top = arrowHeight,
right = width,
bottom = height
),
cornerRadius = CornerRadius(x = 20f, y = 20f)
)
)
}
else -> {
moveTo(arrowOffset, height - arrowHeight)
lineTo(arrowOffset + arrowWidth / 2, height)
lineTo(arrowOffset + arrowWidth, height - arrowHeight)
addRoundRect(
RoundRect(
rect = Rect(
left = 0f,
top = 0f,
right = width,
bottom = height - arrowHeight
),
cornerRadius = CornerRadius(x = 20f, y = 20f)
)
)
}
}
}
}
enum class ArrowDirection {
Left, Right, Top, Bottom
}
Full sample available in this tutorial with resources and everything else
drawContent()
first for border to be drawn on top of content. drawContent()
this is the actual content, Text in the examples above. Modifier.drawWithContent unlike Modifier.drawBehind lets you choose order of drawing and actual content –
Gentianella © 2022 - 2024 — McMap. All rights reserved.