Reputation: 1263
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:
How would I go about doing this? Thank you :)
Upvotes: 26
Views: 26925
Reputation: 712
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!
Upvotes: 57
Reputation: 161
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)
)
Upvotes: 2
Reputation: 66
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:
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)
)
}
}
Upvotes: 0
Reputation: 363975
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)
)
Upvotes: 30
Reputation: 262
@Composable
fun Avatar(color: Color) {
Box(
modifier = Modifier
.size(size.Dp)
.clip(CircleShape)
.background(color = color),
contentAlignment = Alignment.Center
) {
Text(text = "Hello World")
}
}
Upvotes: 10
Reputation: 93614
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.
Upvotes: 1