Search code examples
androidandroid-jetpack-composeandroid-animationjetpack-compose-animationandroid-jetpack-compose-animation

Animate Linear Gradient (Brush) infinitely and reversely in Compose


Initial State looks like this (animatedOffset = 0f)

enter image description here

at 1f I want to get the reversed gradient:

![enter image description here

Current code:

val transition = rememberInfiniteTransition(label = "NavIconGradientAnim")

val animatedOffset by transition.animateFloat(
    initialValue = 0f, targetValue = 1f,
    label = "NavIconGradientAnimOffset",
    animationSpec = infiniteRepeatable(
        animation = tween(1500, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    )
)
Image(
    painterResource(id = item.icon),
    contentDescription = null,
    modifier = Modifier.drawWithCache {
        onDrawWithContent {
            with(drawContext.canvas.nativeCanvas) {
                val angle = 45f
                val endX = drawContext.size.width
                val endY = (endX * kotlin.math.tan(Math.toRadians(angle.toDouble()))).toFloat()

                val checkPoint = saveLayer(null, null)
                drawContent()
                val gradient = Brush.linearGradient(
                    colors = listOf(Color.Blue, Color.Yellow),
                    start = Offset(animatedOffset * endX, animatedOffset * endY),
                    end = Offset(endX, endX)
                )
                drawRect(
                    brush = gradient,
                    blendMode = BlendMode.SrcIn
                )
                restoreToCount(checkPoint)
            }
        }
    }
)

I tried to set

end = Offset(endX - animatedOffset * endX, endY - animatedOffset * endY),

but it doesn't look good (GIF preview):

enter image description here

it changes color abruptly


Solution

  • There are several ways to do it and you don't have to calculate tangent.

    One way of doing it is

        val limit = 1.5f
    
        val transition = rememberInfiniteTransition(label = "shimmer")
        val progressAnimated by transition.animateFloat(
            initialValue = -limit,
            targetValue = limit,
            animationSpec = infiniteRepeatable(
                animation = tween(1500, easing = LinearEasing),
                repeatMode = RepeatMode.Reverse
            ), label = "shimmer"
        )
    

    And set brush with

    val width = size.width val height = size.height

    val offset = width * progress
    val gradientWidth = width
    
    val brush = Brush.linearGradient(
        colors = gradientColors,
        start = Offset(offset, 0f),
        end = Offset(offset + gradientWidth, height)
    )
    

    Result

    enter image description here

    Demo

    @Preview
    @Composable
    private fun SingleGradientTest() {
    
        Column(
            Modifier.padding(20.dp)
        ) {
            val painterStar = painterResource(id = R.drawable.star_foreground)
    
            val limit = 1.5f
            var progress by remember {
                mutableStateOf(-limit)
            }
    
            val transition = rememberInfiniteTransition(label = "shimmer")
            val progressAnimated by transition.animateFloat(
                initialValue = -limit,
                targetValue = limit,
                animationSpec = infiniteRepeatable(
                    animation = tween(1500, easing = LinearEasing),
                    repeatMode = RepeatMode.Reverse
                ), label = "shimmer"
            )
    
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .aspectRatio(1f)
                    .drawWithCache {
                        val width = size.width
                        val height = size.height
    
                        val offset = width * progress
                        val gradientWidth = width
    
                        val brush = Brush.linearGradient(
                            colors = gradientColors,
                            start = Offset(offset, 0f),
                            end = Offset(offset + gradientWidth, height)
                        )
    
                        onDrawBehind {
                            drawRect(
                                brush = brush,
                                blendMode = BlendMode.SrcIn
                            )
                        }
                    }
            )
    
            Box(
                modifier = Modifier
                    .size(100.dp)
                    .graphicsLayer {
                        compositingStrategy = CompositingStrategy.Offscreen
                    }
                    .drawWithCache {
                        val width = size.width
                        val height = size.height
    
                        val offset = width * progress
                        val gradientWidth = width
    
    
                        val brush = Brush.linearGradient(
                            colors = gradientColors,
                            start = Offset(offset, 0f),
                            end = Offset(offset + gradientWidth, height)
    
                        )
    
                        onDrawBehind {
                            // Destination
                            with(painterStar) {
                                draw(
                                    size = Size(width, width)
                                )
                            }
    
                            // Source
                            drawRect(
                                brush = brush,
                                blendMode = BlendMode.SrcIn
                            )
                        }
                    }
            )
    
            Text("Progress: $progress")
            Slider(
                value = progress,
                onValueChange = { progress = it },
                valueRange = -limit..limit
            )
    
            Text(text = "Animated progress: $progressAnimated")
            Box(
                modifier = Modifier
                    .size(100.dp)
                    .graphicsLayer {
                        compositingStrategy = CompositingStrategy.Offscreen
                    }
                    .drawWithCache {
                        val width = size.width
                        val height = size.height
    
                        val offset = width * progressAnimated
                        val gradientWidth = width
    
                        val brush = Brush.linearGradient(
                            colors = gradientColors,
                            start = Offset(offset, 0f),
                            end = Offset(offset + gradientWidth, height)
    
                        )
    
                        onDrawBehind {
                            // Destination
                            with(painterStar) {
                                draw(
                                    size = Size(width, width)
                                )
                            }
    
                            // Source
                            drawRect(
                                brush = brush,
                                blendMode = BlendMode.SrcIn
                            )
                        }
                    }
            )
        }
    }