How to make Rainbow border animation for box in compose?

1.4k Views Asked by At

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

1

There are 1 best solutions below

6
Thracian On

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

enter image description here

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)
        }
    }
}

Edit

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