Compose: Create Text with Circle Background

19.8k views Asked by At

Coming from SwiftUI, I wanted to create a view of a Text where it has a background of a Circle, where the circle's width/height grow as the text inside Text gets longer.

Since there's no Circle() in Compose like there is in SwifUI, I created just a Spacer instead and clipped it. The code below is using ConstraintLayout because I don't know how I would get the width of the Text in order to set the size of my Circle composable to be the same:

@Composable
fun CircleDemo() {
    ConstraintLayout {
        val (circle, text) = createRefs()

        Spacer(
            modifier = Modifier
                .background(Color.Black)
                .constrainAs(circle) {
                    centerTo(text)
                }
        )

        Text(
            text = "Hello",
            color = Color.White,
            modifier = Modifier
                .constrainAs(text) {
                    centerTo(parent)
                }
        )
    }
}

I can use a mutableStateOf { 0 } where I update that in an onGloballyPositioned modifier attached to the Text and then set that as the requiredSize for the Spacer, but 1. that seems stupid and 2. the Spacer now draws outside the boundaries of the ConstraintLayout.

Visually I want to achieve this:

A black circle with the word Hello entered inside

How would I go about doing this? Thank you :)

6

There are 6 answers

1
Mateo Vakili On BEST ANSWER

It is also possible to use drawBehind from the modifier of the textView itself such as below:

Text(
     modifier = Modifier
         .padding(16.dp)
         .drawBehind {
               drawCircle(
                    color = red,
                    radius = this.size.maxDimension
               )
          },
     text = "Hello",
)

of course, customize the radius and other properties as you wish!

enter image description here

1
Gabe Sechan On

Use a background drawable of a black circle inside a transparent color. The background drawable will stretch to fill the view, and circles should stretch well without artifacting.

3
Gabriele Mariotti On

You have to calculate the dimension of the background circle depending on the dimension of the text.
You can use a custom modifier based on Modifier.layout:

fun Modifier.circleLayout() =
    layout { measurable, constraints ->
        // Measure the composable
        val placeable = measurable.measure(constraints)

        //get the current max dimension to assign width=height
        val currentHeight = placeable.height
        val currentWidth = placeable.width
        val newDiameter = maxOf(currentHeight, currentWidth)

        //assign the dimension and the center position
        layout(newDiameter, newDiameter) {
            // Where the composable gets placed
            placeable.placeRelative((newDiameter-currentWidth)/2, (newDiameter-currentHeight)/2)
        }
    }

Then just just apply it the Text with a background with a CircleShape:

    Text(
        text = "Hello World",
        textAlign = TextAlign.Center,
        color = Color.White,
        modifier = Modifier
            .background(Color.Black, shape = CircleShape)
            .circleLayout()
            .padding(8.dp)
    )

enter image description here

3
Caique Oliveira On
@Composable
fun Avatar(color: Color) {
    Box(
        modifier = Modifier
            .size(size.Dp)
            .clip(CircleShape)
            .background(color = color),
        contentAlignment = Alignment.Center
    ) {
        Text(text = "Hello World")
    }
}
0
Velord On

Answer marked as right is a little bit wrong. That because it calculates circle radius ... Actually it depends on many factors. You must take in mind:

  1. Who is the Text parent ?
  2. What Modifier does parent have ?
  3. What Modifier does your Text have ?

Correct answer with easy customizable Circle can be that:

@Composable
fun CircleDemo() {
    // Initialize width as it is not exist
    val textWidthState: MutableState<Dp?> = remember { mutableStateOf(null) }
    val modifierWithCalculatedSize: State<Modifier> = 
    // You must provide new Modifier whenever width of Text is changed                                                                
        remember(textWidthState.value) {
            // Modifier for parent which draw the Circle
            val mod = Modifier
                .padding(horizontal = 16.dp)
                .padding(bottom = 16.dp)
            // Provide new Modifier only when calculation produces new value
            derivedStateOf {
                val currentWidth = textWidthState.value
                if (currentWidth != null) mod.size(currentWidth) else mod
            }
        }
    // Do not use Modifier with size(especially width) for Box. 
    Box(
        modifier = modifierWithCalculatedSize.value
             .clip(CircleShape),
         // Center your text inside Circle
         contentAlignment = Alignment.Center
     ) {
         val density = LocalDensity.current
         Text(
             text = "Hello",
             color = Color.White,
             modifier = Modifier
                 // Obtain width of Text after position
                 .onGloballyPositioned {
                     textWidthState.value =  with(density) { 
                         it.size.width.toDp() 
                     }
                 }
                 // Adjust Circle size
                 .padding(8.dp)
        )
    }
}
0
BennyG On

Expanding on @GabrieleMariotti's answer, you can combine the three modifiers into one, to make it easier to use.

/**
 * Draws circle with a solid [color] behind the content.
 *
 * @param color The color of the circle.
 * @param padding The padding to be applied externally to the circular shape. It determines the spacing between
 * the edge of the circle and the content inside.
 *
 * @return Combined [Modifier] that first draws the background circle and then centers the layout.
 */
fun Modifier.circleBackground(color: Color, padding: Dp): Modifier {
    val backgroundModifier = drawBehind {
        drawCircle(color, size.width / 2f, center = Offset(size.width / 2f, size.height / 2f))
    }

    val layoutModifier = layout { measurable, constraints ->
        // Adjust the constraints by the padding amount
        val adjustedConstraints = constraints.offset(-padding.roundToPx())

        // Measure the composable with the adjusted constraints
        val placeable = measurable.measure(adjustedConstraints)

        // Get the current max dimension to assign width=height
        val currentHeight = placeable.height
        val currentWidth = placeable.width
        val newDiameter = maxOf(currentHeight, currentWidth) + padding.roundToPx() * 2

        // Assign the dimension and the center position
        layout(newDiameter, newDiameter) {
            // Place the composable at the calculated position
            placeable.placeRelative((newDiameter - currentWidth) / 2, (newDiameter - currentHeight) / 2)
        }
    }

    return this then backgroundModifier then layoutModifier
}

Use it like this:

Text(
    text = "Hello World",
    color = Color.White,
    modifier = Modifier
        .circleBackground(color = Color.DarkGray, padding = 6.dp)
)

Text with circular background