androidkotlinandroid-jetpack-compose

How to center text views in a custom progress bar in jetpack compose


I'm trying to create a custom progress bar in Compose, where each step is represented by a dot, and beneath each dot, there's a text label. However, I'm having trouble aligning the text labels so that they are properly centerred under each dot.

This is what i'm trying to achieve:

step progress bar view

Here is my current implementation and how it looks:

how my current implementation looks

data class Step(val title: String = "") {}

StepsProgressView(
     steps = listOf(Step("text"),Step("something"),Step("some"),Step("DD"),Step("Ssss cccc")),
     completedIndex = 2
)

@Composable
fun StepsProgressView(
    steps: List<Step>,
    modifier: Modifier = Modifier,
    lineWidth: Dp = 1.dp,
    completedIndex: Int = 2
) {
    Column(modifier = modifier) {
        Box(contentAlignment = Alignment.Center) {
            Canvas(modifier = Modifier.fillMaxWidth()) {
                val width = drawContext.size.width
                val height = drawContext.size.height

                val yOffset = height / 2
                val itemWidth = width / steps.size

                var startOffset = itemWidth / 2
                var endOffset = startOffset

                val barWidth = lineWidth.toPx()
                repeat(steps.size - 1) { index ->
                    endOffset += itemWidth

                    drawLine(
                        brush = SolidColor(if (index < completedIndex) Color.Blue else Color.Gray),
                        start = Offset(startOffset, yOffset),
                        end = Offset(endOffset, yOffset),
                        strokeWidth = barWidth
                    )
                    startOffset = endOffset
                }
            }

            Row(
                Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.SpaceAround,
                verticalAlignment = Alignment.CenterVertically,
            ) {
                repeat(steps.size) { index ->
                    Box(contentAlignment = Alignment.Center) {
                        // last index image
                        if (index == steps.lastIndex)
                            Image(
                                painter = painterResource(id = androidx.biometric.R.drawable.fingerprint_dialog_fp_icon),
                                contentDescription = null,
                                modifier = Modifier.size(24.dp),
                                contentScale = ContentScale.Crop
                            )
                        // circles box
                        else
                            Box(
                                modifier = Modifier
                                    .size(12.dp)
                                    .drawBehind {
                                        drawCircle(
                                            color =
                                              if (index <= completedIndex) Color.Blue
                                              else Color.Gray
                                        )
                                    },
                                contentAlignment = Alignment.Center,
                                content = {})
                    }
                }
            }
        }
        Spacer(Modifier.height(8.dp))

        Row(
            Modifier.fillMaxWidth(),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.SpaceAround
        ) {
            steps.forEachIndexed { index, step ->
                Text(
                    text = step.title,
                    style = TextStyle(
                          color = 
                             if (steps.indexOf(step) <= completedIndex) Color.Blue 
                             else Color.Gray,
                          fontSize = 13.sp,
                          textAlign = TextAlign.Center
                    ),
                    modifier = Modifier
                        .wrapContentWidth()
                        .weight(1f)
                )
            }
        }
    }
}

The issue I'm facing is that the text labels beneath the dots are not perfectly aligned in the center. I want each text label to be directly centered below its corresponding dot.

Any suggestions would be highly appreciated. Thank you!


Solution

  • The problem is that your last index image is larger than the other circles. That messes with the arrangement in your Row that depends on weight(1f) to distribute the space evenly.

    One way to solve that is to make the last index image measure with the same size as the other circles and then paint the image with the desired size nonetheless:

    // last index image
    if (index == steps.lastIndex)
        Box(modifier = Modifier.size(12.dp)) {
            Image(
                painter = painterResource(id = R.drawable.barImage),
                contentDescription = null,
                modifier = Modifier.requiredSize(24.dp),
                contentScale = ContentScale.Crop
            )
        }
    // circles box
    else
        ...
    

    You might need to add an additional padding to the Row so that this larger-than-expected image still fits.