androidbuttonandroid-jetpack-composespinner

How to vertically center a CircularProgressIndicator in a Button?


I want to create a Button that while disabled, shows a CircularProgressIndicator (use case: user clicks the button, a network request is made and during this the button is disabled with a nice spinner showing). The issue I have is that the CircularProgressIndicator is not vertically centered. This is my current code:

@Preview
@Composable
private fun ButtonSandbox() {
    Button(
        enabled = false,
        modifier = Modifier.fillMaxWidth()
            .height(48.dp),
        onClick = {},
        colors = ButtonDefaults.buttonColors(
            disabledContainerColor = MaterialTheme.colorScheme.primary,
            disabledContentColor = MaterialTheme.colorScheme.onPrimary
        )
    ) {
        CircularProgressIndicator(
            color = MaterialTheme.colorScheme.onPrimary,
            trackColor = MaterialTheme.colorScheme.onPrimary, // just to make it completely visible in preview
            modifier = Modifier.align(Alignment.CenterVertically) // my (failed) attempt to fix it
        )
    }
}

And this is how it looks like, note that the CircularProgressIndicator is not vertically centered: enter image description here

How can I fix that?


Solution

  • Button uses a Surface and a Row where the content is displayed. Row content is already aligned center vertically. If you look at the min height by default is 40.dp. Even if you don't provide the height modifier it should work

    @Preview
    @Composable
    fun ButtonSandbox() {
        Button(
            enabled = false,
            modifier = Modifier.fillMaxWidth(),
            onClick = {},
            colors = ButtonDefaults.buttonColors(
                disabledContainerColor = MaterialTheme.colorScheme.primary,
                disabledContentColor = MaterialTheme.colorScheme.onPrimary
            )
        ) {
            CircularProgressIndicator(
                color = MaterialTheme.colorScheme.onPrimary,
                trackColor = MaterialTheme.colorScheme.onPrimary,
            ) 
     }
    }
    

    If you want to customize the button height i suggest you use a Surface and a row and provide the height as required.

    Something like the below code will help. Customize this to your needs.

    val ButtonShape = RoundedCornerShape(250.dp)
    
    @Composable
    fun CustomRoundedButton(
        modifier: Modifier = Modifier,
        borderColor: Color? = null,
        backgroundColor: Color = MaterialTheme.colorScheme.primary,
        elevation: Dp = 0.dp,
        content: @Composable () -> Unit,
        onClick: () -> Unit
    ) {
        val newModifier = if (borderColor != null) {
            modifier.border(1.dp, borderColor, shape = ButtonShape)
        } else {
            modifier
        }
    
        val finalModifier = newModifier.height(48.dp)
    
        Surface(
            modifier = finalModifier
                .semantics { role = Role.Button }
                .clip(ButtonShape)
                .clickable(
                    indication = null,
                    interactionSource = remember { MutableInteractionSource() }) {
                    onClick()
                },
            color = backgroundColor,
            shadowElevation = elevation
        ) {
            Row(
                Modifier,
                horizontalArrangement = Arrangement.Center,
                verticalAlignment = Alignment.CenterVertically,
            ) {
                content()
            }
        }
    }
    

    Usage:

    CustomRoundedButton(modifier = Modifier.width(200.dp), content = {
                        CircularProgressIndicator(
                            modifier = Modifier.wrapContentHeight(),
                            color = MaterialTheme.colorScheme.onPrimary,
                            trackColor = MaterialTheme.colorScheme.onPrimary, // just to make it completely visible in preview
                        )
                    }) {
    
                    }
    

    Output :

    enter image description here

    Update : If you look at the docs you can see

    /** * The default min height applied for all buttons. Note that you can override it by applying * Modifier.heightIn directly on the button composable. */ val MinHeight = 40.dp

    So you can provide heightIn as well instead of custom button. Below is the code using heightIn. Its the same as yours there is not need to provide this Modifier.align(Alignment.CenterVertically for CircularProgressIndicator

    @Preview
    @Composable
    fun ButtonSandbox() {
        Button(
            enabled = false,
            modifier = Modifier.fillMaxWidth()
                .heightIn(48.dp),
            onClick = {},
            colors = ButtonDefaults.buttonColors(
                disabledContainerColor = MaterialTheme.colorScheme.primary,
                disabledContentColor = MaterialTheme.colorScheme.onPrimary
            )
        ) {
            CircularProgressIndicator(
                color = MaterialTheme.colorScheme.onPrimary,
                trackColor = MaterialTheme.colorScheme.onPrimary,
            )
        }
    }