There is a regular list with some data and LazyColumn for showing it.
LazyColumn {
items (list) { item ->
ListItem(item)
}
}
Simplified ListItem looks like:
#Composable
fun ListItem(item: SomeItem) {
Row(
modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min)
) {
//Some widgets
}
}
I need to set the footer's item (last in list) height to fill screen if there is not enough items for that. It's easy to find last item and provide some flag to ListItem(item: SomeItem, isLast: Boolean), but I don't know how to set the item's height to achieve my goal. Did anyone faced this problem?
It's almost the same question as was asked here about RecyclerView. And the image from that question illustatrates it.
If your items are all same height and you have have info of how tall they are via static height or using Modifier.onSizeChanged{} you can do this by getting height of the LazyColumn using BoxWithConstraints maxHeight or rememberLazyListState().layoutInfo.viewportSize
#Composable
private fun ListComposable() {
val myItems = mutableListOf<String>()
repeat(4) {
myItems.add("Item $it")
}
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.border(2.dp, Color.Cyan)
) {
itemsIndexed(myItems) { index: Int, item: String ->
if (index == myItems.size - 1) {
Text(
text = item,
modifier = Modifier
.fillMaxWidth()
.height((maxHeight - 50.dp * (myItems.size - 1)).coerceAtLeast(50.dp))
.background(Color.Green)
)
} else {
Text(
text = item,
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(Color.Red)
)
}
}
}
}
}
If you don't know total height until last item you will need to use SubcomposeLayout with list that doesn't contain last item and pass LazyColumn height- (items.size-1) total height as your last item's height.
#Composable
fun DimensionMeasureSubcomposeLayout(
modifier: Modifier = Modifier,
mainContent: #Composable () -> Unit,
dependentContent: #Composable (IntSize, Constraints) -> Unit
) {
SubcomposeLayout(modifier = modifier) { constraints ->
// Subcompose(compose only a section) main content and get Placeable
val mainPlaceables: List<Placeable> = subcompose(SlotsEnum.Main, mainContent)
.map {
it.measure(constraints)
}
// 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 maxSize = IntSize(maxWidth, maxHeight)
val dependentPlaceables = subcompose(SlotsEnum.Dependent) {
dependentContent(maxSize, constraints)
}.map {
it.measure(constraints)
}
layout(constraints.maxWidth, constraints.maxHeight) {
dependentPlaceables.forEach { placeable: Placeable ->
placeable.placeRelative(0, 0)
}
}
}
}
enum class SlotsEnum { Main, Dependent }
And use it as
#Composable
private fun SubcomposeExample() {
val myItems = mutableListOf<String>()
repeat(15) {
myItems.add("Item $it")
}
val subList = myItems.subList(0, myItems.size - 1)
val lasItem = myItems.last()
val density = LocalDensity.current
DimensionMeasureSubcomposeLayout(
modifier = Modifier,
mainContent = {
LazyColumn(
modifier = Modifier.fillMaxWidth()
) {
items(subList) { item: String ->
Text(
text = item,
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(Color.Red)
)
}
}
},
dependentContent = { intSize, constraints ->
val lastItemHeight = with(density) {
(constraints.maxHeight - intSize.height).toDp().coerceAtLeast(50.dp)
}
LazyColumn(modifier = Modifier.fillMaxWidth()) {
if (myItems.size > 1) {
items(subList) { item: String ->
Text(
text = item,
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(Color.Red)
)
}
}
item {
Text(
text = lasItem,
modifier = Modifier
.fillMaxWidth()
.height(lastItemHeight)
.background(Color.Green)
)
}
}
}
)
}
Related
I have a CustomGrid that is inside a Column. That column is inside a HorizontalPager and the HorizontalPager is inside another column where there are other elements. I want the Grid to grow in height as much as necessary, but whenever I add fillMaxHeight() or wrapContentSize() or another equivalent method, the application crashes with the error you can see in the title. Is there anything I can do to fix this error and have the Grid take up as much space as I need?
I leave you the prints of my Grid, which is custom, and the respective components.
Custom Grid code:
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.ParentDataModifier
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import kotlin.math.max
interface GridScope {
#Stable
fun Modifier.span(columns: Int = 1, rows: Int = 1) = this.then(
GridData(columns, rows)
)
companion object : GridScope
}
private class GridData(
val columnSpan: Int,
val rowSpan: Int,
) : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any = this#GridData
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as GridData
if (columnSpan != other.columnSpan) return false
if (rowSpan != other.rowSpan) return false
return true
}
override fun hashCode(): Int {
var result = columnSpan
result = 31 * result + rowSpan
return result
}
}
private val Measurable.gridData: GridData?
get() = parentData as? GridData
private val Measurable.columnSpan: Int
get() = gridData?.columnSpan ?: 1
private val Measurable.rowSpan: Int
get() = gridData?.rowSpan ?: 1
data class GridInfo(
val numChildren: Int,
val columnSpan: Int,
val rowSpan: Int,
)
#Composable
fun Grid(
columns: Int,
modifier: Modifier = Modifier,
content: #Composable GridScope.() -> Unit,
) {
check(columns > 0) { "Columns must be greater than 0" }
Layout(
content = { GridScope.content() },
modifier = modifier,
) { measurables, constraints ->
// calculate how many rows we need
val standardGrid = GridData(1, 1)
val spans = measurables.map { measurable -> measurable.gridData ?: standardGrid }
val gridInfo = calculateGridInfo(spans, columns)
val rows = gridInfo.sumOf { it.rowSpan }
// build constraints
val baseConstraints = Constraints.fixed(
width = constraints.maxWidth / columns,
height = constraints.maxHeight / rows,
)
val cellConstraints = measurables.map { measurable ->
val columnSpan = measurable.columnSpan
val rowSpan = measurable.rowSpan
Constraints.fixed(
width = baseConstraints.maxWidth * columnSpan,
height = baseConstraints.maxHeight * rowSpan
)
}
// measure children
val placeables = measurables.mapIndexed { index, measurable ->
measurable.measure(cellConstraints[index])
}
// place children
layout(
width = constraints.maxWidth,
height = constraints.maxHeight,
) {
var x = 0
var y = 0
var childIndex = 0
gridInfo.forEach { info ->
repeat(info.numChildren) {
val placeable = placeables[childIndex++]
placeable.placeRelative(
x = x,
y = y,
)
x += placeable.width
}
x = 0
y += info.rowSpan * baseConstraints.maxHeight
}
}
}
}
private fun calculateGridInfo(
spans: List<GridData>,
columns: Int,
): List<GridInfo> {
var currentColumnSpan = 0
var currentRowSpan = 0
var numChildren = 0
return buildList {
spans.forEach { span ->
val columnSpan = span.columnSpan.coerceAtMost(columns)
val rowSpan = span.rowSpan
if (currentColumnSpan + columnSpan <= columns) {
currentColumnSpan += columnSpan
currentRowSpan = max(currentRowSpan, rowSpan)
++numChildren
} else {
add(
GridInfo(
numChildren = numChildren,
columnSpan = currentColumnSpan,
rowSpan = currentRowSpan
)
)
currentColumnSpan = columnSpan
currentRowSpan = rowSpan
numChildren = 1
}
}
add(
GridInfo(
numChildren = numChildren,
columnSpan = currentColumnSpan,
rowSpan = currentRowSpan,
)
)
}
}
Code where Grid will be inserted or other components, it is generated based on data that comes from API:
Box(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor.value)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
// We only consume nested flings in the main-axis, allowing cross-axis flings to propagate
// as normal
val consumeFlingNestedScrollConnection =
remember { ConsumeFlingNestedScrollConnection(consumeHorizontal = true) }
Column(
modifier = Modifier
.background(
backgroundColor.value,
RoundedCornerShape(topStart = Dimen30, topEnd = Dimen30)
)
.nestedScroll(connection = consumeFlingNestedScrollConnection)
.fillMaxWidth()
) {
HorizontalPager(
count = size,
state = pagerState,
itemSpacing = Dimen20,
modifier = Modifier.padding(top = Dimen33),
userScrollEnabled = false
) { page ->
Column(
modifier = Modifier
// We don't any nested flings to continue in the pager, so we add a
// connection which consumes them.
.nestedScroll(connection = consumeFlingNestedScrollConnection)
// Constraint the content width to be <= than the width of the pager.
.fillParentMaxWidth()
.wrapContentSize()
) {
// content (where grid could be, content is generated dinamically based on data that comes from api
}
}
}
}
}
How Grid is added to that layout:
Grid(
columns = 5,
modifier = Modifier
.padding(start = Dimen20, end = Dimen20, top = Dimen16)
.fillMaxWidth()
.wrapContentSize()
) {
// cards content
}
The crash points to the baseConstraints of the Grid code, but I can't figure out why and I can't solve the problem.
When you use Constraints.fixed() you need to have Constraints maxWidth and maxHeight that are not infinite.
When you apply Modifier.verticalScroll you transform your maxHeight to Constraints.Infinity which is 2147483647. I explained in detail in this answer about constraints with vertical scroll and Constraints.
And created a sample to show why it crashes
#Composable
private fun Grid(
modifier: Modifier = Modifier,
content: #Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
val placeables = measurables.map { measurable: Measurable ->
measurable.measure(
Constraints.fixed(
width = constraints.maxWidth,
height = constraints.maxHeight
)
)
}
layout(constraints.maxWidth, constraints.maxHeight) {
placeables.forEach {
it.placeRelative(0, 0)
}
}
}
}
demonstration why it crashes
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
HorizontalPager(count = 3) {
Column(
modifier = Modifier.wrapContentSize()
) {
Grid() {
BoxWithConstraints() {
Text("Constraints: ${this.constraints}")
}
}
}
}
}
What you should be doing is to constrain maxHeight to parent or screen size.
One, and simple way to do is, passing it via Modifier.onSizeChanged to your Layout, but be careful using Modifier.onSizeChanged which you might trigger recomposition continuously when mutableState is used to update or set size of another Composable.
var height by remember {
mutableStateOf(0)
}
Box(
modifier = Modifier
.fillMaxSize()
.onSizeChanged {
if (height == 0) {
height = it.height
}
}
){
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
HorizontalPager(count = 3) {
Column(
modifier = Modifier.wrapContentSize()
) {
Grid(height = height) {
BoxWithConstraints {
Text("Constraints: ${this.constraints}")
}
}
}
}
}
}
And use parent height instead of infinite height due to Modifier.verticalScroll
#Composable
private fun Grid(
modifier: Modifier = Modifier,
height:Int,
content: #Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
val placeables = measurables.map { measurable: Measurable ->
measurable.measure(
Constraints.fixed(
width = constraints.maxWidth,
height = constraints.maxHeight.coerceAtMost(height)
)
)
}
layout(constraints.maxWidth, constraints.maxHeight) {
placeables.forEach {
it.placeRelative(0, 0)
}
}
}
}
Changing your Compsable based on sample above you can achieve what you wish to create
I have a Compose component with a box and 2 components inside it, but they are never visible both at the same time. I want to adjust size of this box to the first component and stay unchanged when this component will be not visible.
Box(
modifier = modifier.then(Modifier.background(bgColor)),
contentAlignment = Alignment.Center
) {
if (componentVisible1) {
Button(
modifier = Modifier.height(48.dp).widthIn(144.dp),
onClick = onClicked,
enabled = enabled
) {
Text(
text = "text1",
)
}
}
if (component2Visible) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp).background(buttonColor, CircleShape).padding(2.dp),
strokeWidth = 2.dp
)
}
}
Now, width of the box reduces when the component1 is not visible.
You can use SubcomposeLayout to pass a Composable's dimension if another one depends on it without recomposition or in your case even if it's not even in composition.
#Composable
internal fun DimensionSubcomposeLayout(
modifier: Modifier = Modifier,
mainContent: #Composable () -> Unit,
dependentContent: #Composable (DpSize) -> Unit
) {
val density = LocalDensity.current
SubcomposeLayout(
modifier = modifier
) { constraints: Constraints ->
// Subcompose(compose only a section) main content and get Placeable
val mainPlaceable: Placeable = subcompose(SlotsEnum.Main, mainContent)
.map {
it.measure(constraints.copy(minWidth = 0, minHeight = 0))
}.first()
val dependentPlaceable: Placeable =
subcompose(SlotsEnum.Dependent) {
dependentContent(
DpSize(
density.run { mainPlaceable.width.toDp() },
density.run { mainPlaceable.height.toDp() }
)
)
}
.map { measurable: Measurable ->
measurable.measure(constraints)
}.first()
layout(mainPlaceable.width, mainPlaceable.height) {
dependentPlaceable.placeRelative(0, 0)
}
}
}
/**
* Enum class for SubcomposeLayouts with main and dependent Composables
*/
enum class SlotsEnum { Main, Dependent }
And you can use it to set dimensions of Box based on measuring or subcomposing your Button.
#Composable
private fun DimensionSample() {
var componentVisible1 by remember { mutableStateOf(false) }
var component2Visible by remember { mutableStateOf(true) }
Column {
Button(onClick = {
componentVisible1 = !componentVisible1
component2Visible = !component2Visible
}) {
Text(text = "Toggle")
}
val button = #Composable {
Button(
modifier = Modifier
.height(48.dp)
.widthIn(144.dp),
onClick = {}
) {
Text(
text = "text1",
)
}
}
val circularProgressIndicator = #Composable {
CircularProgressIndicator(
modifier = Modifier
.size(24.dp)
.background(Color.Yellow, CircleShape)
.padding(2.dp),
strokeWidth = 2.dp
)
}
DimensionSubcomposeLayout(mainContent = {
button()
}) {
Box(
modifier = Modifier
.size(it)
.then(Modifier.background(Color.Red)),
contentAlignment = Alignment.Center
) {
if (componentVisible1) {
button()
}
if (component2Visible) {
circularProgressIndicator()
}
}
}
}
}
I'm working on chart layout and want all my "dots" on the chart have the same specified size. But the layout inspector shows incorrect value:
Here is my chart composable:
#Composable
private fun Chart() {
val data = DoubleArray(100) { it.toDouble() }
val state = LineChartScaffoldState(
LineChartState(
data = data,
gap = 0.dp,//Dp.Unspecified,
scrollState = rememberScrollState()
)
)
LineChartScaffold(
modifier = Modifier.fillMaxSize(),
state = state
) {
Box {
Column(
Modifier
.fillMaxSize()
.padding(start = 0.dp)
) {
LineChart(
modifier = Modifier
.weight(weight = 1f, fill = false)
.horizontalScroll(state.lineChartState.scrollState),
state = it.lineChartState
) {
Dot(
modifier = Modifier.size(10.dp), // BUT I HAVE SPECIFIED 10 DP SIZE
data = data[it]
)
}
}
}
}
}
Here is my line chart scaffold that is using for sharing the state object.
#Composable
fun LineChartScaffold(
modifier: Modifier = Modifier,
state: LineChartScaffoldState,
content: #Composable (state: LineChartScaffoldState) -> Unit
) {
Box(modifier = modifier) {
content(state)
}
}
My line chart layout:
#Composable
fun LineChart(
modifier: Modifier = Modifier,
state: LineChartState,
item: #Composable (index: Int) -> Unit
) {
Layout(
modifier = modifier,
content = {
for (i in state.data.indices) {
item(i)
}
}
) { measurables, constraints ->
val gap = if (state.gap.isUnspecified) {
0
} else {
state.gap.roundToPx()
}
val placeables = measurables.map { it.measure(constraints) }
layout(constraints.maxWidth, constraints.maxHeight) {
placeables.forEachIndexed { i, it ->
val x = (it.width + gap) * i
val y = (constraints.maxHeight - it.height) * 0.01 * i // TODO: Remove debug percentage calculation
Log.d("debug","x=$x, y=$y")
it.place(x.toInt(), y.toInt())
}
}
}
}
And the dot composable:
#Composable
fun Dot(
modifier: Modifier = Modifier,
data: Double,
) {
Image(
modifier = modifier,
painter = painterResource(id = R.drawable.ic_launcher_background),
contentDescription = null,
alignment = Alignment.TopCenter,
)
}
Why I get dots width 9 dp instead specified 10 dp?
How can i set width of a group of composables, siblings layout from top to bottom, to width of longest one?
What i try to build is exactly same thing as in the images above. For simplicity let's say quote the component at the top and message box which contains message and another container that stores date and message status.
The longest one of quote and message box must be set as parent width and other one must be set to same width as longest one which requires a remeasuring for short one i assume.
Also if message box gets to resized there needs to be an internal parameter that passes this width to set position of container that stores date and status. As can be seen clearly with bounds message text is moved to start while status to end when quote is longer than message box. When message has more than one line message box width and height are set with a calculation as telegram or whatsapp does.
Built this initially with Layout as
#Composable
private fun DynamicLayout(
modifier: Modifier = Modifier,
quote: #Composable () -> Unit,
message: #Composable () -> Unit
) {
val content = #Composable {
quote()
message()
}
Layout(content = content, modifier = modifier) { measurables, constraints ->
val placeableQuote = measurables.first().measure(constraints)
val quoteWidth = placeableQuote.width
val placeableMessage =
measurables.last()
.measure(Constraints(minWidth = quoteWidth, maxWidth = constraints.maxWidth))
val messageWidth = placeableMessage.width
val maxWidth = quoteWidth.coerceAtLeast(messageWidth)
val totalHeight = placeableQuote.height + placeableMessage.height
layout(maxWidth, totalHeight) {
placeableQuote.placeRelative(x = 0, y = 0)
placeableMessage.placeRelative(x = 0, y = placeableQuote.height)
}
}
}
Where i measure message box using width of quote constraint it works but only when quote is longer.
DynamicLayout(
quote = {
Text(
"QUOTE with a very long text",
modifier = Modifier
.background(Color(0xffF44336))
.height(60.dp),
color = Color.White
)
},
message = {
Text(
"MESSAGE Content",
modifier = Modifier
.background(Color(0xff9C27B0)),
color = Color.White
)
}
)
DynamicLayout(
quote = {
Text(
"QUOTE",
modifier = Modifier
.background(Color(0xffF44336))
.height(60.dp),
color = Color.White
)
},
message = {
Text(
"MESSAGE with very long Content",
modifier = Modifier
.background(Color(0xff9C27B0)),
color = Color.White
)
}
)
As it's must be remeasured i think solution for this question should be done with SubComposeLayout but couldn't figure out how to use it for this setup?
#Composable
private fun SubComponentLayout(
modifier: Modifier = Modifier,
mainContent: #Composable () -> Unit,
dependentContent: #Composable (Int) -> Unit
) {
SubcomposeLayout(modifier = modifier) { constraints ->
val mainMeasurables: List<Measurable> = subcompose(SlotsEnum.Main, mainContent)
val mainPlaceables: List<Placeable> = mainMeasurables.map {
it.measure(constraints)
}
val maxSize =
mainPlaceables.fold(IntSize.Zero) { currentMax: IntSize, placeable: Placeable ->
IntSize(
width = maxOf(currentMax.width, placeable.width),
height = maxOf(currentMax.height, placeable.height)
)
}
var maxWidth =
mainPlaceables.maxOf { it.width }
layout(maxSize.width, maxSize.height) {
println("🔥 SubcomposeLayout-> layout() maxSize width: ${maxSize.width}, height: ${maxSize.height}")
val dependentMeasurables: List<Measurable> = subcompose(
slotId = SlotsEnum.Dependent,
content = {
println("🍏 SubcomposeLayout-> layout()->subcompose() mainWidth ZERO")
dependentContent(0)
}
)
val dependentPlaceables: List<Placeable> = dependentMeasurables.map {
it.measure(constraints)
}
maxWidth = maxWidth.coerceAtLeast(
dependentPlaceables.maxOf { it.width }
)
subcompose(SlotsEnum.NEW) {
println("🍒 SubcomposeLayout-> layout()->subcompose() maxWidth: $maxWidth")
dependentContent(maxWidth)
}
mainPlaceables.forEach { it.placeRelative(0, 0) }
dependentPlaceables.forEach { it.placeRelative(0, 150) }
}
}
}
Why cannot remeasure same component second time with same id? When i try to call subCompose with SlotsEnum.Dependent it throws an exception
subcompose(SlotsEnum.NEW) {
println("🍒 SubcomposeLayout-> layout()->subcompose() maxWidth: $maxWidth")
dependentContent(maxWidth)
}
Still not remeasuring correctly after calling it? How can setting sibling can be solved with SubcomposeLayout?
I made a sample based on the sample provided by official documents and #chuckj's answer here.
Orange and pink containers are Columns, which direct children of DynamicWidthLayout, that uses SubcomposeLayout to remeasure.
#Composable
private fun DynamicWidthLayout(
modifier: Modifier = Modifier,
mainContent: #Composable () -> Unit,
dependentContent: #Composable (IntSize) -> Unit
) {
SubcomposeLayout(modifier = modifier) { constraints ->
var mainPlaceables: List<Placeable> = subcompose(SlotsEnum.Main, mainContent).map {
it.measure(constraints)
}
var maxSize =
mainPlaceables.fold(IntSize.Zero) { currentMax: IntSize, placeable: Placeable ->
IntSize(
width = maxOf(currentMax.width, placeable.width),
height = maxOf(currentMax.height, placeable.height)
)
}
val dependentMeasurables: List<Measurable> = subcompose(SlotsEnum.Dependent) {
// 🔥🔥 Send maxSize of mainComponent to
// dependent composable in case it might be used
dependentContent(maxSize)
}
val dependentPlaceables: List<Placeable> = dependentMeasurables
.map { measurable: Measurable ->
measurable.measure(Constraints(maxSize.width, constraints.maxWidth))
}
// Get maximum width of dependent composable
val maxWidth = dependentPlaceables.maxOf { it.width }
println("🔥 DynamicWidthLayout-> maxSize width: ${maxSize.width}, height: ${maxSize.height}")
// If width of dependent composable is longer than main one, remeasure main one
// with dependent composable's width using it as minimumWidthConstraint
if (maxWidth > maxSize.width) {
println("🚀 DynamicWidthLayout REMEASURE MAIN COMPONENT")
// !!! 🔥🤔 CANNOT use SlotsEnum.Main here why?
mainPlaceables = subcompose(2, mainContent).map {
it.measure(Constraints(maxWidth, constraints.maxWidth))
}
}
// Our final maxSize is longest width and total height of main and dependent composables
maxSize = IntSize(
maxSize.width.coerceAtLeast(maxWidth),
maxSize.height + dependentPlaceables.maxOf { it.height }
)
layout(maxSize.width, maxSize.height) {
// Place layouts
mainPlaceables.forEach { it.placeRelative(0, 0) }
dependentPlaceables.forEach {
it.placeRelative(0, mainPlaceables.maxOf { it.height })
}
}
}
}
enum class SlotsEnum { Main, Dependent }
Usage
#Composable
private fun TutorialContent() {
val density = LocalDensity.current.density
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
var mainText by remember { mutableStateOf(TextFieldValue("Main Component")) }
var dependentText by remember { mutableStateOf(TextFieldValue("Dependent Component")) }
OutlinedTextField(
modifier = Modifier
.padding(horizontal = 8.dp)
.fillMaxWidth(),
value = mainText,
label = { Text("Main") },
placeholder = { Text("Set text to change main width") },
onValueChange = { newValue: TextFieldValue ->
mainText = newValue
}
)
OutlinedTextField(
modifier = Modifier
.padding(horizontal = 8.dp)
.fillMaxWidth(),
value = dependentText,
label = { Text("Dependent") },
placeholder = { Text("Set text to change dependent width") },
onValueChange = { newValue ->
dependentText = newValue
}
)
DynamicWidthLayout(
modifier = Modifier
.padding(8.dp)
.background(Color.LightGray)
.padding(8.dp),
mainContent = {
println("🍏 DynamicWidthLayout-> MainContent {} composed")
Column(
modifier = Modifier
.background(orange400)
.padding(4.dp)
) {
Text(
text = mainText.text,
modifier = Modifier
.background(blue400)
.height(40.dp),
color = Color.White
)
}
},
dependentContent = { size: IntSize ->
// 🔥 Measure max width of main component in dp retrieved
// by subCompose of dependent component from IntSize
val maxWidth = with(density) {
size.width / this
}.dp
println(
"🍎 DynamicWidthLayout-> DependentContent composed " +
"Dependent size: $size, "
+ "maxWidth: $maxWidth"
)
Column(
modifier = Modifier
.background(pink400)
.padding(4.dp)
) {
Text(
text = dependentText.text,
modifier = Modifier
.background(green400),
color = Color.White
)
}
}
)
}
}
And full source code is here.
#Composable
fun PreviewLayout() {
fun getRandomString(length: Int): String {
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
return (1..length)
.map { allowedChars.random() }
.joinToString("")
}
val horizontalScrollState = rememberScrollState()
LazyColumn(
modifier = Modifier
.background(Color.Blue)
.fillMaxHeight()
.wrapContentWidth()
.horizontalScroll(horizontalScrollState)
) {
items(5) { index ->
Text(
text = getRandomString((index + 1) * 4).uppercase(),
color = Color.Black,
fontSize = 16.sp,
modifier = Modifier
.padding(8.dp)
.background(Color.Yellow)
)
}
}
}
Preview of the layout:
I'd like to have the items width be the same as the largest item in the list.
Notice the .horizontalScroll(horizontalScrollState), this is to allow horizontal scrolling.
What I'd like:
I need to use a LazyColumn but if I could use a Column I'd write it this way:
Column(
modifier = Modifier
.background(Color.Blue)
.horizontalScroll(horizontalScrollState)
.fillMaxHeight()
.width(IntrinsicSize.Min)
) {
repeat(5) { index ->
Text(
text = getRandomString((index + 1) * 4).uppercase(),
color = Color.Black,
fontSize = 16.sp,
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.background(Color.Yellow)
)
}
}
You need to calculate width of the widest element separately. You can do it by placing an invisible copy of you cell with widest content in a Box along with LazyColumn.
In your sample it's easy - just get the longest string. If in the real project you can't decide which of contents is gonna be the widest one, you have two options:
1.1. Place all of them one on top of each other. You can do it only if you have some limited number of cells,
1.2. Otherwise you have to made some approximation and filter a short list of the ones you expect to be the widest ones.
Because of horizontalScroll maxWidth constraint is infinity, you have to pass calculated width manually. You can get it with onSizeChanged:
#Composable
fun TestScreen(
) {
fun getRandomString(length: Int): String {
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
return (1..length)
.map { allowedChars.random() }
.joinToString("")
}
val items = remember {
List(30) { index ->
getRandomString((index + 1) * 4).uppercase()
}
}
val maxLengthItem = remember(items) {
items.maxByOrNull { it.length }
}
val (maxLengthItemWidthDp, setMaxLengthItemWidthDp) = remember {
mutableStateOf<Dp?>(null)
}
val horizontalScrollState = rememberScrollState()
Box(
Modifier
.background(Color.Blue)
.horizontalScroll(horizontalScrollState)
) {
LazyColumn(
Modifier.fillMaxWidth()
) {
items(items) { item ->
Cell(
item,
modifier = if (maxLengthItemWidthDp != null) {
Modifier.width(maxLengthItemWidthDp)
} else {
Modifier
}
)
}
}
if (maxLengthItem != null) {
val density = LocalDensity.current
Cell(
maxLengthItem,
modifier = Modifier
.requiredWidthIn(max = Dp.Infinity)
.onSizeChanged {
setMaxLengthItemWidthDp(with(density) { it.width.toDp() })
}
.alpha(0f)
)
}
}
}
#Composable
fun Cell(
item: String,
modifier: Modifier,
) {
Text(
text = item,
color = Color.Black,
fontSize = 16.sp,
modifier = modifier
.padding(8.dp)
.background(Color.Yellow)
)
}
Result:
This is not possible when horizontal srolling is enabled.
Regular Modifier.fillMaxWidth can't work inside the scrolling
horizontally layouts as the items are measured with
Constraints.Infinity as the constraints for the main axis.
If you want your Column solution to work, you need to place it inside another contain (like a Box) and apply horizontal scrolling to the parent. Scrolling on the Column itself needs to be removed:
Box(
modifier = Modifier
.requiredWidth(250.dp)
.fillMaxHeight()
.horizontalScroll(rememberScrollState())
) {
Column(
modifier = Modifier
.background(Color.Blue)
.fillMaxHeight()
.width(IntrinsicSize.Min)
) {
repeat(5) { index ->
Text(
text = getRandomString((index + 1) * 40).uppercase(),
color = Color.Black,
fontSize = 16.sp,
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.background(Color.Yellow)
)
}
}
}
#Composable
fun PreviewLayout() {
fun getRandomString(length: Int): String {
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
return (1..length)
.map { allowedChars.random() }
.joinToString("")
}
val horizontalScrollState = rememberScrollState()
LazyColumn(
modifier = Modifier
.background(Color.Blue)
.fillMaxHeight()
.horizontalScroll(horizontalScrollState)
) {
items(5) { index ->
Text(
text = getRandomString((index + 1) * 4).uppercase(),
color = Color.Black,
fontSize = 16.sp,
modifier = Modifier
.padding(8.dp)
.width(width = 200.dp)
.background(Color.Yellow)
)
}
}
}