Search code examples
android-jetpack-composeandroid-compose-image

Showing an animated progress wheel in Jetpack Compose


In this Google Codelab (Mars Photos), while Coil is loading images, the app displays a progress wheel. The code uses this XML file as a drawable:

enter image description here

and uses this code to display it:

@Composable
fun LoadingScreen(modifier: Modifier = Modifier) {
    Box(
        contentAlignment = Alignment.Center,
        modifier = modifier.fillMaxSize()
    ) {
        Image(
            modifier = Modifier.size(200.dp),
            painter = painterResource(R.drawable.loading_img),
            contentDescription = stringResource(R.string.loading)
        )
    }
}

However, that only displays a static image. I want it to rotate (which I certainly expected as behavior). I've thought of some approaches, none of which I like much:

  • Define 12 similar XML files, one for each degree of rotation, and continually display each in sequence
  • Dynamically generate the XML and somehow convert it into a painter on the fly
  • Use Coil to load a GIF

Is there a simpler way of accomplishing this task? Is there a way to rotate the image itself programmatically such as applying a predefined animation mode of some sort (perhaps similar to basicMarquee)?


Solution

  • Here's the code for the same. This code is taken from https://github.com/SmartToolFactory/Compose-ProgressIndicator and it has many other spinning circular progress view animations.

    @Composable
    fun SpinningProgressIndicator(
        modifier: Modifier = Modifier,
        @androidx.annotation.IntRange(from = 4, to = 12) staticItemCount: Int = 12,
        dynamicItemCount: Int = staticItemCount / 2,
        staticItemColor: Color = StaticItemColor,
        dynamicItemColor: Color = DynamicItemColor,
        spinnerShape: SpinnerShape = SpinnerShape.RoundedRect,
        durationMillis: Int = 1000
    ) {
    
        // Number of rotating items
        val animatedItemCount = dynamicItemCount.coerceIn(1, staticItemCount)
    
        val coefficient = 360f / staticItemCount
    
        val infiniteTransition = rememberInfiniteTransition()
        val angle by infiniteTransition.animateFloat(
            initialValue = 0f,
            targetValue = staticItemCount.toFloat(),
            animationSpec = infiniteRepeatable(
                animation = tween(
                    durationMillis = durationMillis,
                    easing = LinearEasing
                ),
                repeatMode = RepeatMode.Restart
            )
        )
    
        Canvas(modifier = modifier
            .progressSemantics()
            .size(Size)
        ) {
    
            var canvasWidth = size.width
            var canvasHeight = size.height
    
            if (canvasHeight < canvasWidth) {
                canvasWidth = canvasHeight
            } else {
                canvasHeight = canvasWidth
            }
    
            val itemWidth = canvasWidth * .3f
            val itemHeight = canvasHeight / staticItemCount
    
            val cornerRadius = itemWidth.coerceAtMost(itemHeight) / 2
    
            val horizontalOffset = (size.width - size.height).coerceAtLeast(0f) / 2
            val verticalOffset = (size.height - size.width).coerceAtLeast(0f) / 2
    
            val topLeftOffset = Offset(
                x = horizontalOffset + canvasWidth - itemWidth,
                y = verticalOffset + (canvasHeight - itemHeight) / 2
            )
    
            val size = Size(itemWidth, itemHeight)
    
            // Stationary items
            for (i in 0..360 step 360 / staticItemCount) {
                rotate(i.toFloat()) {
                        drawRect(
                            color = staticItemColor,
                            topLeft = topLeftOffset,
                            size = size,
                        )
                }
            }
    
            // Dynamic items
            for (i in 0..animatedItemCount) {
                // angle is cast to into move in intervals of static items
                rotate((angle.toInt() + i) * coefficient) {
                        drawRect(
                            color = dynamicItemColor.copy(
                                alpha = (0.2f + 0.15f * i).coerceIn(
                                    0f, 1f
                                )
                            ),
                            topLeft = topLeftOffset,
                            size = size,
                        )
                }
            }
        }
    }
    

    val infiniteTransition = rememberInfiniteTransition() is used for infinite transition animation and RepeatMode.Restart means the animation restarts giving a feel of infinite animation.

    rotate((angle.toInt() + i) * coefficient) { where you rotate the items. All the angle caluclations are in radians.

    You can customize this to your needs. You don't need a image. Draw lines or rect and then animate few of them by rotating it.