Horizontal Scrolling inside LazyColumn in Jetpack Compose - android

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

Related

Jetpack Compose - Setting dimensions for Accompanist placeholder

The library I'm using: "com.google.accompanist:accompanist-placeholder-material:0.23.1"
I want to display a placeholder in the place of (or over) a component when it's in the loading state.
I do the following for a Text:
MaterialTheme() {
var placeholderVisible by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
while (true) {
delay(1000)
placeholderVisible = !placeholderVisible
}
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.border(1.dp, Color.Red)
.padding(16.dp)
) {
Text(
modifier = Modifier
.then(
if (placeholderVisible) {
Modifier.height(28.dp).width(62.dp)
} else {
Modifier
}
)
.placeholder(
visible = placeholderVisible,
highlight = PlaceholderHighlight.shimmer()
),
text = if (placeholderVisible) "" else "Hello"
)
}
}
}
And I get this:
I want instead that no matter how big I set the placeholder's height or width, it will not participate in any way in the measuring process and, if I want to, to be able to draw itself even over other components (in this case let's say the red border).
As an effect of what I want, the box with red border will always have the dimension as if that Modifier.height(28.dp).width(62.dp) is not there.
I know I can draw outside a component's borders using drawWithContent, specifying the size of a rectangle or a circle (or whatever) to be component's size + x.dp.toPx() (or something like that). But how do I do this with Modifier.placeholder?
Ideally, I would need something like Modifier.placeholder(height = 28.dp, width = 62.dp)
So, with or without this ideal Modifier, the UI should never change (except, of course, the shimmer box that may be present or not).
I think I can pull this off by modifying the source code of this Modifier, but I hope I won't need to turn to that.
Just replace your Text() with below code, maybe conditional Modifier is the issue in above code!
Text(
modifier = Modifier
.size(width = 62.dp, height = 28.dp)
.placeholder(
visible = placeholderVisible,
highlight = PlaceholderHighlight.shimmer()
),
text = if (placeholderVisible) "" else "Hello",
textAlign = TextAlign.Center
)

LazyColumn inside Column with verticalScroll modifier throws exception

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

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.

Jetpack Compose find parents width/length

I want to explicitely retrieve the value of the fillMaxSize().
Suppose i have:
Box(Modifier
.fillMaxSize()
.background(Color.Yellow))
{
var size = ?
Box(Modifier
.size(someSize)
.background(Color.Blue))
{Text("Test")}
I want to change the size of my second Box multiple times (will probably stem from some viewmodel) and then reset it to maxSize.
How can i do that, I don't know any 'getMaxSize()'-method?
If you really need the raw size value, you can use the following code:
var size by remember { mutableStateOf(IntSize.Zero) }
Box(Modifier
.fillMaxSize()
.background(Color.Yellow)
.onSizeChanged {
size = it
}
) {
Box(
Modifier
.then(
with(LocalDensity.current) {
Modifier.size(
width = size.width.toDp(),
height = size.height.toDp(),
)
}
)
.background(Color.Blue)
) { Text("Test") }
}
But note, that is't not optimal in terms of performance: this view gets rendered two times. First time second box gets size zero, then the onSizeChanged block gets called, and then view gets rendered for the second time.
Be especially careful if using remember in top level views, because changing state will trigger full view stack re-render. Usually you want split your screen into views with states, so changing one view state only will re-render this view.
Also you can use BoxWithConstraints where you can get maxWidth/maxHeight inside the BoxWithConstraintsScope: it's much less code and a little better on performance.
BoxWithConstraints(
Modifier
.fillMaxSize()
.background(Color.Yellow)
) {
Box(
Modifier
.size(
width = maxWidth,
height = maxHeight,
)
.background(Color.Blue)
) { Text("Test") }
}
But usually if you wanna indicate size dependencies, using modifiers without direct knowing the size should be enough. It's more "Compose" way of writing code and more optimized one.
So if you wanna you second box be same size as the first one, just use .fillMaxSize() on it too. If you wanna it to be some part of the parent, you can add fraction param. To make second box size be half size of the first one, you do:
Box(
Modifier
.fillMaxSize(fraction = 0.5f)
) { Text("Test") }
If you wanna different parts for width/height:
Box(
Modifier
.fillMaxWidth(fraction = 0.3f)
.fillMaxHeight(fraction = 0.7f)
) { Text("Test") }
In your first Box you can use the onGloballyPositioned modifier to get the size.
It is called with the final LayoutCoordinates of the Layout when the global position of the content may have changed.
Then use coordinates.size to get the size of the first Box.
var size by remember { mutableStateOf(Size.Zero)}
Box(
Modifier
.fillMaxSize()
.background(Color.Yellow)
.onGloballyPositioned { coordinates ->
size = coordinates.size.toSize()
}
Box(){ /*....*/ }
)
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
var parentSize by remember {
mutableStateOf(Size.Zero)
}
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.background(Color.Green)
) {
Box(
modifier = Modifier
.size(100.dp)
.align(Alignment.Center)
.background(Color.Red)
.onGloballyPositioned {
//here u can access the parent layout coordinate size
parentSize = it.parentLayoutCoordinates?.size?.toSize()?: Size.Zero
}
) {
Column(Modifier.fillMaxSize()) {
Text(text = "parent size = $parentSize")
}
}
}
}

When will Subcompose Layout support Intrinsic layout, if ever?

I see this function in the source code of androidx.compose.ui.layout.SubcomposeLayout.kt in androidx.compose.ui:ui:1.0.0-beta02.
private fun createMeasurePolicy(
block: SubcomposeMeasureScope.(Constraints) -> MeasureResult
): MeasurePolicy = object : LayoutNode.NoIntrinsicsMeasurePolicy(
error = "Intrinsic measurements are not currently supported by SubcomposeLayout"
) {
...
}
It looks like I can't use intrinsic measurement when the composable will be rendered within a subcomposable.
For reference, I'm trying to use a view like this inside a ModalBottomSheet. The intention is to have a scrollable view within the sheet, with a sticky view always at the bottom (like a button). I'd like the scrollable content to only take up as much space as it needs, and not always be full screen when in the sheets expanded state, which weight(1f) does.
Column(
modifier = Modifier
.height(IntrinsicSize.Min)
.wrapContentHeight(Alignment.Bottom),
verticalArrangement = Arrangement.Bottom
) {
Column(
content = sheetContent,
modifier = Modifier
.weight(1f)
.wrapContentHeight(Alignment.Bottom)
)
Box {
bottomStickyContent?.let { it() }
}
}
Sounds like the answer is no, SubcomposeLayout will not get Intrinsic support anytime soon, if ever.
I solved this problem by updating by code to use constraint layout.
ConstraintLayout {
val (sticky, column) = createRefs()
Column(
content = sheetContent,
modifier = Modifier
.constrainAs(column) {
top.linkTo(parent.top)
bottom.linkTo(sticky.top)
height = Dimension.preferredWrapContent
}
.wrapContentHeight(Alignment.Bottom)
)
Box(
modifier = Modifier
.constrainAs(sticky) {
bottom.linkTo(parent.bottom)
}
) {
bottomStickyContent?.let { it() }
}
}

Categories

Resources