I want to place some composables at specific positions on top of an image. However, the image will be responsive (it will resize), so I can't use Modifier.offset(some_fixed_offset.dp)
.
Modifier.offset(10.percent)
would be ideal, but that doesn't work (or does it?).
How can I implement this?
These are the possible paths that I examined briefly:
BoxWithConstraints
has min/max size, but I would need the exact size to calculate the relative positions myself.Before I dive into the custom solution, is there an easier way, similar to .offset(10.percent)
?
Here's some test code that I started with:
@Composable
fun GeneralPlaceHolder(modifier: Modifier = Modifier) {
Box (
modifier
.background(Color.Red)
.size(300.dp) //<-- if this size changes,
// the yellow dot should hold it's relative position
.padding(10.dp)
) {
Box(modifier = Modifier
.background(Color.Blue)
.fillMaxSize()) {
DaThing()
}
}
}
@Composable
fun DaThing() {
Box {
Image(painter = painterResource(id = R.drawable.testgrid10x10),
contentDescription = "",
modifier = Modifier.fillMaxSize())
Box(modifier = Modifier
.size(5.dp)
.offset(x = 10.dp, y=20.dp) //<-- percentage no can do?
.background(Color.Yellow)
)
}
}
And a screenshot with a grid as the "image". It's a 10x10 grid, so offset(10.percent, 10.percent)
would place the yellow dot at the first grid point.
Modifier.offset() changes placeable placement based on fixed values via parameters as
private class OffsetNode(
var x: Dp,
var y: Dp,
var rtlAware: Boolean
) : LayoutModifierNode, Modifier.Node() {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val placeable = measurable.measure(constraints)
return layout(placeable.width, placeable.height) {
if (rtlAware) {
placeable.placeRelative(x.roundToPx(), y.roundToPx())
} else {
placeable.place(x.roundToPx(), y.roundToPx())
}
}
}
}
By changing placeable.placeRelative(x.roundToPx(), y.roundToPx())
to percentage of parent Constraints
with Modifier.layout{}
you can place your Composable to value in percentage based on parent.
However the thing you need to pay attention is it should be the Constraints
coming from parent which means where you put Modifier.layout{}
matters. If you set it after any size, offset or padding Modifier
, Constraints
might change since size and padding Modifiers do exact same thing based on their inner logic. If you wish to learn about how Constraints, layout and size modifiers work you can refer this answer.
@Preview
@Composable
fun GeneralPlaceHolderTest() {
GeneralPlaceHolder()
}
@Composable
fun GeneralPlaceHolder(modifier: Modifier = Modifier) {
Column {
var percentage by remember {
mutableStateOf(0f)
}
Slider(
value = percentage,
onValueChange = {
percentage = it
}
)
Text("Percentage: $percentage")
Box(
modifier
.background(Color.Red)
.size(300.dp) //<-- if this size changes,
// the yellow dot should hold it's relative position
.padding(10.dp)
) {
Box(
modifier = Modifier
.background(Color.Blue)
.fillMaxSize()
) {
DaThing(percentage)
}
}
}
}
@Composable
fun DaThing(percentage: Float) {
Box {
Box(
modifier = Modifier.fillMaxSize().drawBehind {
drawRect(color = Color.Blue)
}
)
Box(
modifier = Modifier
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
placeable.placeRelative(
x = (constraints.maxWidth * percentage).toInt(),
y = (constraints.maxHeight * percentage).toInt()
)
}
}
.size(5.dp)
.background(Color.Yellow)
)
}
}
This layout also can be refactored as Modifier
fun Modifier.offsetByPercent(percentage: Float) = this.then(
Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
placeable.placeRelative(
x = (constraints.maxWidth * percentage).toInt(),
y = (constraints.maxHeight * percentage).toInt()
)
}
}
)
And can be used as
Box(
modifier = Modifier
.offsetByPercent(percentage)
.size(5.dp)
.background(Color.Yellow)
)