I have a simple Box Composable which I wanna clip to a custom shape, but whenever I try it, the shape's size remains pretty rigid and small. I've tried scaling on the path level using Matrix(), but to no avail. Here's the sample -
const val clockHand = "M0 80.52L2 86l2-5.48V2.74C4 1.83 3.6 0 2 0S0 1.83 0 2.74v77.78Z"
val clockHandShape = object : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density
): Outline {
return Outline.Generic(PathParser.createPathFromPathData(clockHand).asComposePath())
}
}
Box(
modifier = Modifier
.background(Color.Green)
.clip(clockHandShape)
.width(30.dp)
.height(50.dp)
.background(Color(0xFFFF007A))
.layoutId("hourHand")
) {}
As a control, you can replace the custom shape with a pre-defined one like RectangleShape or CircleShape etc.
Clipping doesn't make size modifiers ineffective. Shape is created using the intrinsic pixel size coming from the values that creates that drawable.
All you need to do is get intrinsic size of Path using getBounds().size and scale to Composable size when creating the shape using matrix.postScale.
#Preview
#Composable
private fun Test() {
val clockHand = "M0 80.52L2 86l2-5.48V2.74C4 1.83 3.6 0 2 0S0 1.83 0 2.74v77.78Z"
val path = PathParser.createPathFromPathData(clockHand).asComposePath()
val pathSize = path.getBounds().size
val clockHandShape = object : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density
): Outline {
val matrix = android.graphics.Matrix()
matrix.postScale(
size.width / pathSize.width, size.height / pathSize.height
)
path.asAndroidPath().transform(matrix)
return Outline.Generic(path)
}
}
Box(
modifier = Modifier
.clip(clockHandShape)
.size(90.dp, 150.dp)
.background(Color(0xFFFF007A))
.layoutId("hourHand")
) {}
}
Related
Problem description
I'm trying to create a component on android using Compose and Canvas that simulates a 7-segment display like this:
For that, I adopted a strategy of creating only half of this component and mirroring this part that I created downwards, so I would have the entire display.
This is the top part of the 7-segment display:
But the problem is when "mirror" the top to bottom. It turns out that when I add the Modifier.rotate(180f) the figure rotates around the origin of the canvas clockwise, and so it doesn't appear on the screen (it would if it were counterclockwise).
I don't want to do this solution using a font for this, I would like to solve this problem through the canvas and compose itself. If there is a smarter way to do this on canvas without necessarily needing a mirror I would like to know.
My code
Below is my code that I'm using to draw this:
DisplayComponent.kt
#Composable
fun DisplayComponent(
modifier: Modifier = Modifier,
size: Int = 1000,
color: Color = MaterialTheme.colors.primary,
) {
Column(modifier = modifier) {
HalfDisplayComponent(size, color)
HalfDisplayComponent(
modifier = Modifier.rotate(180f),
size = size,
color = color
)
}
}
#Composable
private fun HalfDisplayComponent(
size: Int,
color: Color,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier) {
LedModel.values().forEach {
LedComponent(
ledModel = it,
size = size,
color = color
)
}
}
}
LedModel.kt
enum class LedModel(val coordinates: List<Pair<Float, Float>>) {
HorizontalTop(
listOf(
Pair(0.04f, 0.03f), // Point A
Pair(0.07f, 0f), // Point B
Pair(0.37f, 0f), // Point C
Pair(0.4f, 0.03f), // Point D
Pair(0.34f, 0.08f), // Point E
Pair(0.1f, 0.08f), // Point F
)
),
VerticalRight(
listOf(
Pair(0.41f, 0.04f), // Point A
Pair(0.44f, 0.07f), // Point B
Pair(0.44f, 0.37f), // Point C
Pair(0.41f, 0.4f), // Point D
Pair(0.35f, 0.35f), // Point E
Pair(0.35f, 0.09f), // Point F
)
),
VerticalLeft(
listOf(
Pair(0.03f, 0.4f), // Point A
Pair(0f, 0.37f), // Point B
Pair(0f, 0.07f), // Point C
Pair(0.03f, 0.04f), // Point D
Pair(0.09f, 0.09f), // Point E
Pair(0.09f, 0.35f), // Point F
)
),
HorizontalBottom(
listOf(
Pair(0.1f, 0.36f), // Point A
Pair(0.34f, 0.36f), // Point B
Pair(0.39f, 0.4f), // Point C
Pair(0.05f, 0.4f), // Point D
)
),
}
LedComponent.kt
#Composable
fun LedComponent(
modifier: Modifier = Modifier,
size: Int = 30,
color: Color = MaterialTheme.colors.primary,
ledModel: LedModel = LedModel.HorizontalTop
) = getPath(ledModel.coordinates).let { path ->
Canvas(modifier = modifier.scale(size.toFloat())) {
drawPath(path, color)
}
}
private fun getPath(coordinates: List<Pair<Float, Float>>): Path = Path().apply {
coordinates.map {
transformPointCoordinate(it)
}.forEachIndexed { index, point ->
if (index == 0) moveTo(point.x, point.y) else lineTo(point.x, point.y)
}
}
private fun transformPointCoordinate(point: Pair<Float, Float>) =
Offset(point.first.dp.value, point.second.dp.value)
My failed attempt
As described earlier, I tried adding a Modifier by rotating the composable of the display but it didn't work. I did it this way:
#Composable
fun DisplayComponent(
modifier: Modifier = Modifier,
size: Int = 1000,
color: Color = MaterialTheme.colors.primary,
) {
Column(modifier = modifier) {
DisplayFABGComponent(size, color)
DisplayFABGComponent(
modifier = Modifier.rotate(180f),
size = size,
color = color
)
}
}
There are many things wrong with the code you posted above.
First of all in Jetpack Compose even if your Canvas has 0.dp size you can still draw anywhere which is the first issue in your question. Your Canvas has no size modifier, which you can verify by printing DrawScope.size as below.
fun LedComponent(
modifier: Modifier = Modifier,
size: Int = 1000,
color: Color = MaterialTheme.colorScheme.primary,
ledModel: LedModel = LedModel.HorizontalTop
) = getPath(ledModel.coordinates).let { path ->
Canvas(
modifier = modifier.scale(size.toFloat())
) {
println("CANVAS size: ${this.size}")
drawPath(path, color)
}
}
any value you enter makes no difference other than Modifier.scale(0f), also this is not how you should build or scale your drawing either.
If you set size for your Canvas such as
#Composable
fun DisplayComponent(
modifier: Modifier = Modifier,
size: Int = 1000,
color: Color = MaterialTheme.colorScheme.primary,
) {
Column(modifier = modifier) {
HalfDisplayComponent(
size,
color,
Modifier
.size(200.dp)
.border(2.dp,Color.Red)
)
HalfDisplayComponent(
modifier = Modifier
.size(200.dp)
.border(2.dp, Color.Cyan)
.rotate(180f),
size = size,
color = color
)
}
}
Rotation works but what you draw is not symmetric as in image in your question.
point.first.dp.value this snippet does nothing. What it does is adds dp to float then gets float. This is not how you do float/dp conversions and which is not necessary either.
You can achieve your goal with one Canvas or using Modifier.drawBehind{}. Create a Path using Size as reference for half component then draw again and rotate it or create a path that contains full led component. Or you can have paths for each sections if you wish show LED digits separately.
This is a simple example to build only one diamond shape, then translate and rotate it to build hourglass like shape using half component. You can use this sample as demonstration for how to create Path using Size as reference, translate and rotate.
fun getHalfPath(path: Path, size: Size) {
path.apply {
val width = size.width
val height = size.height / 2
moveTo(width * 0f, height * .5f)
lineTo(width * .3f, height * 0.3f)
lineTo(width * .7f, height * 0.3f)
lineTo(width * 1f, height * .5f)
lineTo(width * .5f, height * 1f)
lineTo(width * 0f, height * .5f)
}
}
You need to use aspect ratio of 1/2f to be able to have symmetric drawing. Green border is to show bounds of Box composable.
val path = remember {
Path()
}
Box(modifier = Modifier
.border(3.dp, Color.Green)
.fillMaxWidth(.4f)
.aspectRatio(1 / 2f)
.drawBehind {
if (path.isEmpty) {
getHalfPath(path, size)
}
drawPath(
path = path,
color = Color.Red,
style = Stroke(2.dp.toPx())
)
withTransform(
{
translate(0f, size.height / 2f)
rotate(
degrees = 180f,
pivot = Offset(center.x, center.y / 2)
)
}
) {
drawPath(
path = path,
color = Color.Black,
style = Stroke(2.dp.toPx())
)
}
}
Result
I need size of my composable to draw dynamic lines but I don't want to get size by:
var size by remember { mutableStateOf(IntSize.Zero) }
Modifier.onSizeChanged{size = it}
or
Modifier.onGloballyPositioned{size = it.size}
because I don't want to recompose.
Currently I am getting size from BoxWithConstraints and passing as parameter like this:
fun DrawLines(intSize:IntSize){
// handle lines
}
Is there any better approach or that's all I can do for now?
Thanks for help.
If you are gonna use size of your Composable for drawing lines you can change your function to extension of DrawScope which returns size of your Composable. If that's not the case check answer below.
fun DrawScope.drawLine() {
this.size
}
And call this function inside either of
Modifier.drawBehind{}, Modifier.drawWithContent{} or Modifier.drawWithCache{}. Also you can pass size inside these Modifiers if you don't want to change your function.
BoxWithConstraints and SubcomposeLayout
BoxWithConstraints is not always reliable to get exact size of your content, it, as the name suggests, is good for getting Constraints. I have a detailed answer about Constraints and what BoxConstraints return with which size modifier here. You can check out Constraints section of answer to examine outcomes with each Modifier.
For instance
BoxWithConstraints() {
Text(
modifier = Modifier
.size(200.dp)
.border(2.dp, Color.Red),
text = "Constraints: ${constraints.minWidth}, max: ${constraints.maxWidth}"
)
}
Will return minWidth = 0px, and maxWidth 1080px(width of my device in px) instead of 525px which is 200.dp in my device.
And you can't get dimensions from Layout alone without recomposing either that's why BoxWithConstraints uses SubcomposeLayout to pass Constraints to content. You can check out this question to learn about SubcomposeLayout.
BoxWithConstraints source code
#Composable
#UiComposable
fun BoxWithConstraints(
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.TopStart,
propagateMinConstraints: Boolean = false,
content:
#Composable #UiComposable BoxWithConstraintsScope.() -> Unit
) {
val measurePolicy = rememberBoxMeasurePolicy(contentAlignment, propagateMinConstraints)
SubcomposeLayout(modifier) { constraints ->
val scope = BoxWithConstraintsScopeImpl(this, constraints)
val measurables = subcompose(Unit) { scope.content() }
with(measurePolicy) { measure(measurables, constraints) }
}
}
SubcomposeLayout allows deferring the composition and measure of
content until its constraints from its parent are known and some its
content can be measured, the results from which and can be passed as a
parameter to the deferred content.
In the implementation below, it can be customized as required, i use several versions of it based on why it's needed.
You can customize how you sum or max width or height, what will be
layout width or height, how you place your items to have a behavior like Row, Column or Box depends on your needs. You can limit to one Composable or multiple ones is up to you. Only thing that is required is passing Size/IntSize/DpSize from one Composable to another.
/**
* SubcomposeLayout that [SubcomposeMeasureScope.subcompose]s [mainContent]
* and gets total size of [mainContent] and passes this size to [dependentContent].
* This layout passes exact size of content unlike
* BoxWithConstraints which returns [Constraints] that doesn't match Composable dimensions under
* some circumstances
*
* #param placeMainContent when set to true places main content. Set this flag to false
* when dimensions of content is required for inside [mainContent]. Just measure it then pass
* its dimensions to any child composable
*
* #param mainContent Composable is used for calculating size and pass it
* to Composables that depend on it
*
* #param dependentContent Composable requires dimensions of [mainContent] to set its size.
* One example for this is overlay over Composable that should match [mainContent] size.
*
*/
#Composable
fun DimensionSubcomposeLayout(
modifier: Modifier = Modifier,
placeMainContent: Boolean = true,
mainContent: #Composable () -> Unit,
dependentContent: #Composable (Size) -> Unit
) {
SubcomposeLayout(
modifier = modifier
) { constraints: Constraints ->
// Subcompose(compose only a section) main content and get Placeable
val mainPlaceables: List<Placeable> = subcompose(SlotsEnum.Main, mainContent)
.map {
it.measure(constraints.copy(minWidth = 0, minHeight = 0))
}
// Get max width and height of main component
var maxWidth = 0
var maxHeight = 0
mainPlaceables.forEach { placeable: Placeable ->
maxWidth += placeable.width
maxHeight = placeable.height
}
val dependentPlaceables: List<Placeable> = subcompose(SlotsEnum.Dependent) {
dependentContent(Size(maxWidth.toFloat(), maxHeight.toFloat()))
}
.map { measurable: Measurable ->
measurable.measure(constraints)
}
layout(maxWidth, maxHeight) {
if (placeMainContent) {
mainPlaceables.forEach { placeable: Placeable ->
placeable.placeRelative(0, 0)
}
}
dependentPlaceables.forEach { placeable: Placeable ->
placeable.placeRelative(0, 0)
}
}
}
}
enum class SlotsEnum { Main, Dependent }
Usage
val content = #Composable {
Box(
modifier = Modifier
.size(200.dp)
.background(Color.Red)
)
}
val density = LocalDensity.current
DimensionSubcomposeLayout(
mainContent = { content() },
dependentContent = { size: Size ->
content()
val dpSize = density.run {size.toDpSize() }
Box(Modifier.size(dpSize).border(3.dp, Color.Green))
},
placeMainContent = false
)
or
DimensionSubcomposeLayout(
mainContent = { content() },
dependentContent = { size: Size ->
val dpSize = density.run {size.toDpSize() }
Box(Modifier.size(dpSize).border(3.dp, Color.Green))
}
)
Result
In example below we set size of Box with green border based on Box with red background. This can be complicated for a beginner but that's how you get dimensions without recomposing a Composable. SubcomposeLayout question and answers in the link provided above might help. I posted several answers and linked other answers show how to use it.
Extra Section
Layouts, Scopes, and Constraining Siblings
You can use layout in similar way Box, Row, Column does with a scope to pass information from inside to content using an interface, implementation and changing properties of this implementation
interface DimensionScope {
var size: Size
}
class DimensionScopeImpl(override var size: Size = Size.Zero) : DimensionScope
And implementing DimensionScope and Layout.
#Composable
private fun DimensionLayout(
modifier: Modifier = Modifier,
content: #Composable DimensionScope.() -> Unit
) {
val dimensionScope = remember{DimensionScopeImpl()}
Layout(
modifier = modifier,
// đ„ since we invoke it here it will have Size.Zero
// on Composition then will have size value below
content = { dimensionScope.content() }
) { measurables: List<Measurable>, constraints: Constraints ->
val placeables = measurables.map { measurable: Measurable ->
measurable.measure(constraints)
}
val maxWidth = placeables.maxOf { it.width }
val maxHeight = placeables.maxOf { it.height }
dimensionScope.size = Size(maxWidth.toFloat(), maxHeight.toFloat())
layout(maxWidth, maxHeight) {
placeables.forEach { placeable: Placeable ->
placeable.placeRelative(0, 0)
}
}
}
}
Since we invoke before being able to measure, and with Layout we can only measure once, we won't be able to pass correct Size to DimensionScopeImpl on first composition as i mentioned above. On next recompositions since we remember DimensionScopeImpl we get the correct size and Text size is correctly set and we see Text with border.
Column(modifier = Modifier.fillMaxSize().padding(20.dp)) {
val density = LocalDensity.current
var counter by remember { mutableStateOf(0) }
DimensionLayout {
Box(
modifier = Modifier
.size(200.dp)
.background(Color.Red)
)
val dpSize = density.run { size.toDpSize() }
Text(
text = "counter: $counter", modifier = Modifier
.size(dpSize)
.border(3.dp, Color.Green)
)
}
Button(onClick = { counter++ }) {
Text("Counter")
}
}
We are not able to get correct size because we needed to invoke dimensionScope.content() before measuring but in some cases you might be able to get Constraints, size or parameters from parent or your calculation. When that's the case you can pass you Size. I made an image that passes drawing area based on ContentScale as you can see here using scope.
Selectively measuring to match one Sibling to Another
Not being able to pass using Layout doesn't mean we can't set other sibling to same size and use its dimensions if needed.
For demonstration we will change dimensions of second Composable to firs one's
#Composable
private fun MatchDimensionsLayout(
modifier: Modifier = Modifier,
content: #Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables: List<Measurable>, constraints: Constraints ->
// For demonstration we will change dimensions of second Composable to firs ones
require(measurables.size == 2)
val firstMeasurable = measurables.first()
val secondMeasurable = measurables.last()
val firsPlaceable = firstMeasurable.measure(constraints)
// Measure with first one's width and height
val secondPlaceable =
secondMeasurable.measure(Constraints.fixed(firsPlaceable.width, firsPlaceable.height))
// Set width and height of this Composable width of first one, height total of first
// and second
val containerWidth = firsPlaceable.width
val containerHeight = firsPlaceable.height + secondPlaceable.height
layout(containerWidth, containerHeight) {
firsPlaceable.placeRelative(0,0)
val y = firsPlaceable.height
secondPlaceable.placeRelative(0,y)
}
}
}
Demonstration
MatchDimensionsLayout {
BoxWithConstraints {
Text(
modifier = Modifier
.size(200.dp)
.border(2.dp, Color.Red),
text = "Constraints: ${constraints.minWidth}\n" +
"max: ${constraints.maxWidth}"
)
}
BoxWithConstraints {
Text(
modifier = Modifier
.size(400.dp)
.border(2.dp, Color.Red),
text = "Constraints: ${constraints.minWidth}\n" +
"max: ${constraints.maxWidth}"
)
}
}
Since we matched size of second one to first one using Constraints.fixed for measuring BoxWithConstraints now returns dimensions of first or main Composable even if we are not able to pass dimensions from Layout as parameters.
You can also use Modifier.layoutId() instead of first or second to select Composable that you need to use as reference for measuring others
I need to create smth like below. Image should be nested inside the circle background and one of the corners should be out of this circle "pocket". I think we need smth like a mask but don't understand what instruments I can use to achieve this effect.
You can apply a custom shape to an Image composable.
Something like:
Image(
painter = painterResource(R.drawable.xxx),
contentDescription = "xxxx",
contentScale = ContentScale.Crop,
modifier = Modifier
.size(100.dp)
.clip(
RoundedCornerShape(
topStartPercent = 50,
topEndPercent = 0, //square corner
bottomEndPercent = 50,
bottomStartPercent = 50
)
)
)
Otherwise you can define you custom path using:
class MyShape(topStart: CornerSize) : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density
): Outline {
val myPath = Path().apply {
//....
}
return Outline.Generic(path = myPath)
}
}
I want to draw on Canvas based on its layout dimension, but the dimensions received from DrawScope don't match with the Canvas
#Preview
#Composable
fun Circle() {
val modifier = Modifier
.fillMaxSize()
.border(1.dp, color = Color.Magenta)
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
val minDimension = min(placeable.width, placeable.height)
Log.d("Canvas layout", "$minDimension")
layout(minDimension, minDimension) {
placeable.placeRelative(0, 0)
}
}
Canvas(modifier) {
Log.d("Canvas drawCircle", "${size.width}, ${size.height}")
drawCircle(
color = Color.White,
center = Offset(size.minDimension/2, size.minDimension/2),
radius = size.minDimension/2
)
}
}
Logcat:
D/Canvas layout: 1080
D/Canvas drawCircle: 1080.0, 1987.0
values are the same either calling fillMaxSize() at the beginning or at the end of the modifier builder, but it is 0 when not calling fillMaxSize() .
From the screenshot you can see from the border that the width and height of Canvas match but why they don't in the DrawScope, and how can I get it to match
I want to get height of my button(or other element) in Jetpack Compose. Do you know how to get?
If you want to get the height of your button after composition, then you could use: onGloballyPositionedModifier.
It returns a LayoutCoordinates object, which contains the size of your button.
Example of using onGloballyPositioned Modifier:
#Composable
fun OnGloballyPositionedExample() {
// Get local density from composable
val localDensity = LocalDensity.current
// Create element height in pixel state
var columnHeightPx by remember {
mutableStateOf(0f)
}
// Create element height in dp state
var columnHeightDp by remember {
mutableStateOf(0.dp)
}
Column(
modifier = Modifier
.onGloballyPositioned { coordinates ->
// Set column height using the LayoutCoordinates
columnHeightPx = coordinates.size.height.toFloat()
columnHeightDp = with(localDensity) { coordinates.size.height.toDp() }
}
) {
Text(text = "Column height in pixel: $columnHeightPx")
Text(text = "Column height in dp: $columnHeightDp")
}
}
A complete solution would be as follows:
#Composable
fun GetHeightCompose() {
// get local density from composable
val localDensity = LocalDensity.current
var heightIs by remember {
mutableStateOf(0.dp)
}
Box(modifier = Modifier.fillMaxSize()) {
// Important column should not be inside a Surface in order to be measured correctly
Column(
Modifier
.onGloballyPositioned { coordinates ->
heightIs = with(localDensity) { coordinates.size.height.toDp() }
}) {
Text(text = "If you want to know the height of this column with text and button in Dp it is: $heightIs")
Button(onClick = { /*TODO*/ }) {
Text(text = "Random Button")
}
}
}
}
Using Modifier.onSizeChanged{} or Modifier.globallyPositioned{} might cause infinite recompositions if you are not careful as in OPs question when size of one Composable effects another.
https://developer.android.com/reference/kotlin/androidx/compose/ui/layout/package-summary#(androidx.compose.ui.Modifier).onSizeChanged(kotlin.Function1)
Using the onSizeChanged size value in a MutableState to update layout
causes the new size value to be read and the layout to be recomposed
in the succeeding frame, resulting in a one frame lag.
You can use onSizeChanged to affect drawing operations. Use Layout or
SubcomposeLayout to enable the size of one component to affect the
size of another.
Even though it's ok to draw if the change in frames is noticeable by user it won't look good
For instance
Column {
var sizeInDp by remember { mutableStateOf(DpSize.Zero) }
val density = LocalDensity.current
Box(modifier = Modifier
.onSizeChanged {
sizeInDp = density.run {
DpSize(
it.width.toDp(),
it.height.toDp()
)
}
}
.size(200.dp)
.background(Color.Red))
Text(
"Hello World",
modifier = Modifier
.background(Color.White)
.size(sizeInDp)
)
}
Background of Text moves from initial background that cover its bounds to 200.dp size on next recomposition. If you are doing something that changes any UI drawing from one dimension to another it might look as a flash or glitch.
First alternative for getting height of an element without recomposition is using BoxWithConstraints
BoxWithConstraints' BoxScope contains maxHeight in dp and constraints.maxHeight in Int.
However BoxWithConstraints returns constraints not exact size under some conditions like using Modifier.fillMaxHeight, not having any size modifier or parent having vertical scroll returns incorrect values
You can check this answer out about dimensions returned from BoxWithConstraints, Constraints section shows what you will get using BoxWithConstraints.
verticalScroll returns Constraints.Infinity for height.
Reliable way for getting exact size is using a SubcomposeLayout
How to get exact size without recomposition?
**
* SubcomposeLayout that [SubcomposeMeasureScope.subcompose]s [mainContent]
* and gets total size of [mainContent] and passes this size to [dependentContent].
* This layout passes exact size of content unlike
* BoxWithConstraints which returns [Constraints] that doesn't match Composable dimensions under
* some circumstances
*
* #param placeMainContent when set to true places main content. Set this flag to false
* when dimensions of content is required for inside [mainContent]. Just measure it then pass
* its dimensions to any child composable
*
* #param mainContent Composable is used for calculating size and pass it
* to Composables that depend on it
*
* #param dependentContent Composable requires dimensions of [mainContent] to set its size.
* One example for this is overlay over Composable that should match [mainContent] size.
*
*/
#Composable
fun DimensionSubcomposeLayout(
modifier: Modifier = Modifier,
placeMainContent: Boolean = true,
mainContent: #Composable () -> Unit,
dependentContent: #Composable (Size) -> Unit
) {
SubcomposeLayout(
modifier = modifier
) { constraints: Constraints ->
// Subcompose(compose only a section) main content and get Placeable
val mainPlaceables: List<Placeable> = subcompose(SlotsEnum.Main, mainContent)
.map {
it.measure(constraints.copy(minWidth = 0, minHeight = 0))
}
// Get max width and height of main component
var maxWidth = 0
var maxHeight = 0
mainPlaceables.forEach { placeable: Placeable ->
maxWidth += placeable.width
maxHeight = placeable.height
}
val dependentPlaceables: List<Placeable> = subcompose(SlotsEnum.Dependent) {
dependentContent(Size(maxWidth.toFloat(), maxHeight.toFloat()))
}
.map { measurable: Measurable ->
measurable.measure(constraints)
}
layout(maxWidth, maxHeight) {
if (placeMainContent) {
mainPlaceables.forEach { placeable: Placeable ->
placeable.placeRelative(0, 0)
}
}
dependentPlaceables.forEach { placeable: Placeable ->
placeable.placeRelative(0, 0)
}
}
}
}
enum class SlotsEnum { Main, Dependent }
Usage
val content = #Composable {
Box(
modifier = Modifier
.size(200.dp)
.background(Color.Red)
)
}
val density = LocalDensity.current
DimensionSubcomposeLayout(
mainContent = { content() },
dependentContent = { size: Size ->
content()
val dpSize = density.run {size.toDpSize() }
Box(Modifier.size(dpSize).border(3.dp, Color.Green))
},
placeMainContent = false
)
or
DimensionSubcomposeLayout(
mainContent = { content() },
dependentContent = { size: Size ->
val dpSize = density.run {size.toDpSize() }
Box(Modifier.size(dpSize).border(3.dp, Color.Green))
}
)
You could also use BoxWithConstraints as follows:
Button(onClick = {}){
BoxWithConstraints{
val height = maxHeight
}
}
But I'm not sure it fits your specific usecase.
I managed to implement a workaround to reduce (not completely removes) the number of recomposition when using Modifier.onSizeChanged by making the composable that reads from it now stateless. Not the best approach available but it gets the job done.
#Composable
fun TextToMeasure(
currentHeight: Dp,
onHeightChanged: (Dp) -> Unit,
modifier: Modifier = Modifier,
) {
val density = LocalDensity.current
Text(
text = "Hello Android",
modifier = modifier
.onSizeChanged { size ->
val newHeight = with(density) { size.height.toDp }
if (newHeight != currentHeight) {
onHeightChanged(newHeight)
}
},
)
}
#Composable
fun MainScreen() {
val height = remember { mutableStateOf(0.dp) }
TextToMeasure(
currentHeight = height.value,
onHeightChanged = { height.value = it },
)
}