Compose: Create Text with Circle Background
Asked Answered
P

6

22

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 :)

Prayerful answered 17/4, 2021 at 1:59 Comment(0)
D
49

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

Deflocculate answered 28/5, 2022 at 14:30 Comment(2)
Here the circle behind will be twice the size of the text and will overflow the Composable. The text maximum square size is given as the circle radius (instead of the circle diameter). If you want the circle to fit exactly the text size, then set: radius = this.size.maxDimension / 2.0f. Then you can add internal and external padding around the circle.Petulance
this is a good solution since it doesnt need padding from the component itselfChader
M
28

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

Malediction answered 17/4, 2021 at 6:54 Comment(3)
Thanks, works well for longer strings like "Hello World" but I'm not sure what's needed to make it work for "1"Placket
@Placket Just assign a minSize to the Text element. For example .defaultMinSize(24.dp)Malediction
@GabrieleMariotti for small string is not working well, even giving minSize.Severalty
O
7
@Composable
fun Avatar(color: Color) {
    Box(
        modifier = Modifier
            .size(size.Dp)
            .clip(CircleShape)
            .background(color = color),
        contentAlignment = Alignment.Center
    ) {
        Text(text = "Hello World")
    }
}
Oller answered 17/8, 2022 at 19:0 Comment(3)
hi, what is size.Dp? size isn't defined in your code.Indoxyl
This is perfect if the circle is the important object and the text ajust to it, for my case of use this is the right choise to go, the acepted answer calculate the circle in base of the text size so all circles will be diferent. In case of show them in a list, that result could be a problem.Whencesoever
hey @MartyMiller size.Dp could be 16.dp for instanceOller
Q
2

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

Quoth answered 21/8, 2023 at 19:59 Comment(0)
V
1

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.

Volteface answered 17/4, 2021 at 2:41 Comment(1)
You should have added this as comment not answer.Pemmican
E
0

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)
        )
    }
}
Equilibrium answered 18/6, 2023 at 13:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.