I want to set androidx.compose.foundation.Canvas
size dynamically after measuring measurables and placing placeables with layout function but Canvas requires a Modifier with specific size how can achieve this with Layout and Canvas and in efficient way.
What i want to achieve is as in the image to set content and also add some space and draw an arrow in red area to create speech bubble as can be seen here.
First Container in image is the correct one with Modifier.layout and Modifier.drawbackground as
@Composable
private fun MyComposable(modifier: Modifier = Modifier) {
Row(
modifier = Modifier
) {
var totalSize = Size.Zero
val layoutModifier = modifier
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
totalSize = Size(placeable.width + 50f, placeable.height.toFloat())
println("MyComposable() LAYOUT placeable: $placeable")
layout(totalSize.width.roundToInt(), placeable.height) {
placeable.place(0, 0)
}
}
.drawBehind {
println("MyComposable() drawBehind size: $size")
drawRect(Color.Red, size = totalSize, style = Stroke(2f))
drawRect(Color.Blue, size = size, style = Stroke(2f))
}
Column(modifier = layoutModifier) {
Text("First Text")
Text("Second Text")
}
}
}
And logs
I: MyComposable() LAYOUT placeable width: 250, height: 124
I: MyComposable() drawBehind size: Size(250.0, 124.0)
With Layout and Canvas
@Composable
private fun CanvasLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
var widthInDp by remember { mutableStateOf(0.dp) }
var heightInDp by remember { mutableStateOf(0.dp) }
Layout(content = content, modifier = modifier) { measurables, constraints ->
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
val maxWidth = placeables.maxOf { it.width } + 50
val height = placeables.sumOf { it.height }
widthInDp = (maxWidth / density).toDp()
heightInDp = (height / density).toDp()
println("🚗 CanvasLayout() LAYOUT maxWidth: $maxWidth, height: $height placeables: ${placeables.size}")
var yPos = 0
layout(maxWidth, height) {
placeables.forEach { placeable: Placeable ->
placeable.placeRelative(0, yPos)
yPos += placeable.height
}
}
}
androidx.compose.foundation.Canvas(
modifier = Modifier.size(widthInDp, heightInDp),
onDraw = {
println("🚙 CanvasLayout() CANVAS size: $size")
drawRect(Color.Blue, size = size, style = Stroke(2f))
}
)
}
As can be seen in image bottom rectangle is drawn below Text composables instead of around them and calls layout
twice
I: 🚗 CanvasLayout() LAYOUT maxWidth: 300, height: 124 placeables: 2
I: 🚙 CanvasLayout() CANVAS size: Size(0.0, 0.0)
I: 🚗 CanvasLayout() LAYOUT maxWidth: 300, height: 124 placeables: 2
I: 🚙 CanvasLayout() CANVAS size: Size(114.0, 47.0)
What's the correct and efficient way to use Layout
and Canvas
together?
Sometimes it requires a dynamic size which requires you to modify Canvas size either as in this question.
Since Canvas
is a Spacer
@Composable
fun Canvas(modifier: Modifier, onDraw: DrawScope.() -> Unit) =
Spacer(modifier.drawBehind(onDraw))
draw behind works fine.
But sometimes you design your layout with Canvas in a way canvas happens to be in a statless Composable in bottom layer such as in default Slider Source code.
fun Slider() {
// States rememberables
SliderImpl()
}
fun SliderImpl() {
Canvas()
// Other Composables like thumb
}
When you need to pass dynamic size to Canvas there are few options
1- Using Modifier.onSizeChanged{} on your layout you want to get your dimension but this is not advised, and it might cause an infinite loop you set size and read again
2- Using a BoxWithConstraints
which returns min/maxWidth, min/maxHeight but max constraints doesn't always correspond to bounds of your layout. It works when your Composable fills whole layout when Modifier.fillMax used or you have a fixed size.
3- Other option is to use a SubcomposeLayout
where you create main(you want to measure) and dependent(you want to pass measurement to) Composables. Main component gets subcomposed and passes dimension to dependent one and you set it.
This logic is used by Scaffold
, TabRow
to measure and space Composables based on other. You can check this link about SubcomposeLayout for more details. BoxWithConstraints
, and LazyList
are also SubcomposeLayouts
4- LookAheadLayout
, this might work either, i haven't tried yet, when i do i will edit this answer with a sample