LazyColumn inside Column with verticalScroll modifier throws exception - android

In one of my composables, a Lazycolumn is nested inside a Column composable. I want to be able to scroll the entire Column along with the Lazycolumn. But, specifying the verticalScroll modifier property on Column results in the following exception causing the app to crash. How can I fix this?
Exception
java.lang.IllegalStateException: Vertically scrollable component was measured with an infinity maximum height constraints, which is disallowed. One of the common reasons is nesting layouts like LazyColumn and Column(Modifier.verticalScroll()).
Composable
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(bottom = 100.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
) {
items(
items = allItems!!,
key = { item ->
item.id
}
) { item ->
ShoppingListScreenItem(
navController = navController,
item = item,
sharedViewModel = sharedViewModel
) { isChecked ->
scope.launch {
shoppingListScreenViewModel.changeItemChecked(item!!, isChecked)
}
}
}
}
...
Button(
modifier = Modifier.padding(vertical = 24.dp),
onClick = {
navController.navigate(NavScreens.AddItemScreen.route) {
popUpTo(NavScreens.AddItemScreen.route) {
inclusive = true
}
}
}
) {
Text("Go to add item screen")
}
}

This happens when you wish to measure your LazyColumn with Constraints with Constraints.Infinity for the maxHeight which is not permitted as described in error log. There should be a fixed height or you shouldn't have another Scrollable with same orientation.
Column(
modifier = Modifier
// This is the cause
.verticalScroll(rememberScrollState())
) {
LazyColumn(
// and not having a Modifier that could return non-infinite max height contraint
modifier = Modifier
.fillMaxWidth()
) {
}
If you don't know exact height you can assign Modifier.weight(1f) to LazyColumn.
Contraints
This section is extra about `Constraints` and measurement you can skip this part if you are not interested in how it works.
What i mean by measuring with with Constraints.Infinity is when you create a Layout in Compose you use
Layout(modifier=modifier, content=content){
measurables: List<Measurable>, constraints: Constraints ->
}
You get child Composables as List<Measurable> which you can measure with Constraints provided by parent or the one you see fit by updating existing one with Constraints.copy or fixed one when you build a custom Composable with Layout.
val placeable = measurable.measure(constraints)
Constraints min/max width/height changes based on size modifier or scroll. When there is a scroll and you don't use any size modifier, Constraints return
minHeight =0, maxHeight= Int.MAX_VALUE as Constraints.Infinity
Modifier.fillMaxWidth()
Modifier.fillMaxWidth().weight(1f)
Easiest way to check Constraints with different Modifiers or with scroll is getting it from BoxWithConstraints

Related

How to show semi-transparent loading overlay above full Composable

I am trying to create a Composable that wraps another content Composable and displays a CircularProgressBar as an overlay on top of it, covering the whole content Composable.
I almost got it working as wished, see the following to images:
Initial state
Loading state
But as you can see, the overlay fills the complete screen instead of just the gray LazyRow item and the text field. Thus, the button is pushed off screen.
This is my current code:
#Composable
fun LoadingBox(
modifier: Modifier = Modifier,
isLoading: Boolean = false,
loadingText: String,
content: #Composable() () -> Unit
) {
Box(modifier = modifier
.fillMaxWidth()
.wrapContentHeight()
) {
content()
if (isLoading) {
Surface(
modifier = Modifier
.fillMaxSize()
.alpha(0.5f),
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator()
Text(text = loadingText)
}
}
}
}
}
On the screenshots, I am providing the gray box and the text field as the content parameter. So the overlay should only cover the gray LazyRow item and the text field.
I already stumbled across instrinsic measures, however I cannot use them as the App crashes when I provide a LazyRow as content due to following error:
java.lang.IllegalStateException: Asking for intrinsic measurements of SubcomposeLayout layouts is not supported. This includes components that are built on top of SubcomposeLayout, such as lazy lists, BoxWithConstraints, TabRow, etc. To mitigate this:
- if intrinsic measurements are used to achieve 'match parent' sizing,, consider replacing the parent of the component with a custom layout which controls the order in which children are measured, making intrinsic measurement not needed
You should:
add contentAlignment = Alignment.Center in the parent Box
Remove the Surface
remove the verticalArrangement in the Column
Add an other Box which you can fill with a translucent background
Something like:
Box(modifier = modifier
.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
content()
if (isLoading) {
Box(
Modifier
.matchParentSize()
.background(Color.Gray.copy(alpha = 0.5f))
)
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
androidx.compose.material.CircularProgressIndicator()
Text(text = loadingText)
}
}
}

Horizontal Scrolling inside LazyColumn in Jetpack Compose

I'm currently trying to recreate a behavior, upon adding a new element to a LazyColumn the items start shifting to the right, in order to represent a Tree and make the elements easier to read.
The mockup in question:
Documentation
Reading through the documentation of Jetpack Compose in Lists and grids I found the following.
Keep in mind that cases where you’re nesting different direction layouts, for example, a scrollable parent Row and a child LazyColumn, are allowed:
Row(
modifier = Modifier.horizontalScroll(scrollState)
) {
LazyColumn {
// ...
}
}
My implementation
Box(Modifier.padding(start = 10.dp)) {
Row(
modifier = Modifier
.horizontalScroll(scrollState)
.border(border = BorderStroke(1.dp, Color.Black))
) {
LazyColumn(
modifier = Modifier.fillMaxWidth()
) {
for (i in 0..25) {
item {
OptionItem(Modifier.padding(start = (i*20).dp))
}
item {
TaskItem(Modifier.padding(start = (i*10).dp))
}
}
}
}
.
.
.
}
OptionItem represents the element with the dot at the beginning, and TaskItem the other one.
When testing the LazyColumn, it appears as if instead of having a fixed size, the size of the column starts growing just after the elements have gone outside the screen, this causes a strange effect.
As you can see in the GIF, the width of the column starts increasing after the elements no longer fit in the screen.
The Question
I want to prevent this effect from happening, so is there any way I could maintain the width of the column to the maximum all the time?
The reason that applying a simple fillMaxWidth will not work because you are telling a composable to stretch to max, but that is impossible because the view itself can stretch indefinitely since it can be horizontally scrollable. I'm not sure why do you want to prevent this behavior but perhaps maybe you want your views to have some initial width then apply the padding, while maintaining the same width. what you can do in such case is simply give your composables a specific width, or what you can do is to get the width of the box and apply them to your composables by width (i used a text in this case)
val localDensity = LocalDensity.current
var lazyRowWidthDp by remember { mutableStateOf(0.dp) }
Box(
Modifier
.padding(start = 10.dp)
.onGloballyPositioned { layoutCoordinates -> // This function will get called once the layout has been positioned
lazyRowWidthDp =
with(localDensity) { layoutCoordinates.size.width.toDp() } // with Density is required to convert to correct Dp
}
) {
val scrollState = rememberScrollState()
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(scrollState)
) {
items(25) { i ->
Text(
text = "Hello",
modifier = Modifier
.padding(start = (i * 20).dp)
.width(lazyRowWidthDp)
.border(1.dp, Color.Green)
)
}
items(25) { i ->
Text(
text = "World",
modifier = Modifier
.padding(start = (i * 10).dp)
.width(lazyRowWidthDp)
.border(1.dp, Color.Green)
)
}
}
}
Edit:
you can apply horizontal scroll to the lazy column itself and it will scroll in both directions

Wrap Content in Jetpack Compose

How to make the parent layout - Box wrap its content in Jetpack compose?
The current implementation below fills the entire screen, I only want the Box to wrap around its child - Switch. How do I define wrap content for the Box?
#Composable
fun TestScreen(modifier: Modifier = Modifier) {
Box(modifier = Modifier.background(Color.Yellow)){
val switchState = remember { mutableStateOf(true) }
Switch(
checked = switchState.value,
enabled= true,
onCheckedChange = { switchState.value = it }
)
}
}
What you see on screen is not about Modifier.wrapContentSize. Modifier.wrapContentSize and having no size Modifier returns same constraints. You can check out Constraints section of this answer.
Why your Box covers entire screen is probably something more sneaky you have a Surface with Modifier.fillMaxSize() as direct parent of TestScreen.
Surface is a Box with propagateMinConstraints: Boolean = true which forces minimum width and height to its direct descendent. This answer explains with examples how it works.
Your Composable is actually as this when parent is not Surface as i mentioned above.
Surface(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(10.dp)
) {
TestScreen()
}
}
Since you haven't specified on what container/parent composable your posted composable is being called, I can only suggest using Modifier's wrapContentSize.
Box(
modifier = Modifier
.background(Color.Yellow)
.wrapContentSize() // wrap content height and width
){
val switchState = remember { mutableStateOf(true) }
Switch(
checked = switchState.value,
enabled= true,
onCheckedChange = { switchState.value = it }
)
}

Jetpack Compose - How to control IME (keyboard) in a scrollable non-lazy column?

According to https://google.github.io/accompanist/insets/ we can use a nested scroll connection to control the keyboard appearance when scrolling.
The sample code has the modern impl for a LazyColumn:
LazyColumn(
reverseLayout = true,
modifier = Modifier
.weight(1f)
.nestedScroll(connection = rememberImeNestedScrollConnection())
) {
items(listItems) { imageUrl -> ListItem(imageUrl, Modifier.fillMaxWidth())
}
}
The doc has a sample for ScrollableColumn which is no longer a thing as of beta06:
// Here we're using ScrollableColumn, but it also works with LazyColumn, etc.
ScrollableColumn(
// We use the nestedScroll modifier, passing in the
// the connection from rememberImeNestedScrollConnection()
modifier = Modifier.nestedScroll(
connection = rememberImeNestedScrollConnection()
)
) {
// list content
}
What is the equivalent for Modifier.verticalScroll()? I've tried combining it with nestedScroll() to no effect.
val imeNestedScrollConnection = rememberImeNestedScrollConnection()
Column(
modifier = modifier
.fillMaxHeight()
.fillMaxWidth()
.verticalScroll(scrollState)
.nestedScroll(imeNestedScrollConnection)
.padding(horizontal = spacing, vertical = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) { ... }
Other things I've tried
Changing the order of the two scroll modifiers (no difference)
Having only the nestedScroll modifier (column no longer scrolls at all)

How do we get the position/size of a Composable in a screen?

In Compose, how do we get the position or size of a Composable in a screen ? For example, I'm trying to focus the map camera between specific bounds and adding padding. Here I need to get the padding corresponding to the pager top position and TopBar bottom position.
Currently the code of this screen is the following:
BoxWithConstraints {
MapViewWithMarkers(...)
TopAppBar(
modifier = Modifier
.fillMaxWidth()
.statusBarsPadding(),
backgroundColor = Color.Transparent,
elevation = 0.dp,
)
HorizontalPager(
state = pagerState,
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.navigationBarsPadding()
.padding(bottom = 32.dp),
itemSpacing = 8.dp,
) { page ->
val hikeOnMapCard = hikeMarkerList[page]
HikeOnMapCard(hikeOnMapCard) {
viewModel.hikeOnMapCardClicked(hikeOnMapCard.id)
}
}
}
I would like to forward to the MapViewWithMarkers Composable the padding corresponding to the TopAppBar size and Pager size on this screen
Thanks !
To get the position and the size of a composable you can use the onGloballyPositioned modifier.
Something like:
var sizeTopBar by remember { mutableStateOf(IntSize.Zero) }
var positionInRootTopBar by remember { mutableStateOf(Offset.Zero) }
TopAppBar(
modifier = Modifier
.onGloballyPositioned { coordinates ->
// size
sizeTopBar = coordinates.size
// global position (local also available)
positionInRootTopBar = coordinates.positionInRoot()
}
//...
)
With complex layout to measure and layout multiple composables, use the Layout composable instead. This composable allows you to measure and lay out children manually.

Categories

Resources