HorizontalPager content pages with different height - android

Inside a scrollable column I have a horizontal pager with two pages on it. One of them contains a very long list of content, while the other one is much shorter. On the smaller one I have a huge area at the bottom that I can scroll down on with no content, I suspect because of the height of the long list. Also, if I scrolled to the bottom on the longer page, and then swipe to the other page, then I just get a blank screen, and have to scroll far up to the actual content.
How can I limit the user from scrolling down below the actual content on the shorter page?
Column(
modifier = Modifier
.align(Alignment.TopStart)
.fillMaxWidth()
.verticalScroll(rememberScrollState())
) {
someOtherContent() // for example around 400.dp height
HorizontalPager(
state = pagerState,
verticalAlignment = Alignment.Top
) { i ->
when (i) {
0 -> {
Column(
modifier = Modifier.fillMaxWidth().height(450.dp)
) {
... not too much content
}
}
1 -> {
Column(
modifier = Modifier.fillMaxWidth().height(2000.dp)
) {
... many rows of content
}
}
}
}
}

Related

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

How to make a child composable match parent composable's height only if the child is smaller than the parent?

I have a peculiar problem with Compose. I have a scrolling view, which may have either one or more children. I would need to make the content scroll if it's larger than the scroll view, but always fill the remaining height if the content is not as high as the scroll view.
The naive way I thought it would originally work was like this:
val scrollState = rememberScrollState()
Column(
modifier = modifier.fillMaxSize()
.verticalScroll(scrollState)
) {
Box(modifier = Modifier.fillMaxHeight()) {
// Content here should fill the screen and scroll if needed
}
}
However, this doesn't seem to work. While the Column fills the screen, the Box will only take as much space as its children need.
Currently I've achieved this functionality with a hack like this:
var contentHeight by remember { mutableStateOf(0) }
var containerHeight by remember { mutableStateOf(0) }
val scrollState = rememberScrollState()
Column(
modifier.fillMaxSize()
.onGloballyPositioned { containerHeight = it.size.height }
.verticalScroll(scrollState)
) {
val contentModifier = Modifier
.onGloballyPositioned {
if (contentHeight == 0) contentHeight = it.boundsInParent().height.toInt()
}
.let {
if (contentHeight > 0 && contentHeight < containerHeight) {
it.height(with(LocalDensity.current) {
containerHeight.toDp()
}
} else it
}
// The actual content
Box(modifier = contentModifier) {
// ...
}
}
This "works", however it makes the content flash for a single frame and causes multiple recompositions. Is there a cleaner way of achieving the same result?

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

LazyColumn to not recompose some items

With RecyclerView, I can make some ViewHolder not recyclable (follow some answers in
I want my RecyclerView to not recycle some items).
Can I make LazyColumn to not recompose some items (similar to make RecyclerView don't recycle some ViewHolder)? I have few items in LazyColumn with some big images, it recompose after scrolling down and up so scroll is not smooth.
I met the same problem and use Column instead with a modifier vertical scroll. If you don't want it recycle view, just load all ( few items)
Column(
modifier = Modifier
.constrainAs(listView) {
top.linkTo(
parent.top
)
}
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
list.forEachIndexed { index, itemModel ->
ItemView(itemModel, index) {
// on item click
}
}
Spacer(modifier = Modifier.height(40.dp))
}
I had a similar issue with a composable that is heavily manipulating bitmaps and then draw them on a canvas. You noticed that processing while scrolling the lazyColumn up and down.
To face this issue a stored my manipulated bitmaps in a List<ImageBitmap> as rememberSaveable
val rememberFruits by rememberSaveable(images) {
mutableStateOf(doBitmapOperations(images))
}
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(height)
.constrainAs(circle) {}
) {
rememberFruits
.forEach { createScaledImageBitmap ->
val (image, offset) = createScaledImageBitmap
drawImage(
image = image,
topLeft = offset
)
}
}
My item in the lazyColumn was still recomposed but the heavy bitmap operations were no more executed while scrolling and making scrolling up and down smooth.
hope it helps!

Jetpack Compose - HorizontalPager item spacing/padding for items with max width

Using Jetpack Compose and the accompanist pager, I'm trying to create a HorizontalPager where:
Edges of items to the left and right of current item are shown
There is a max width to the Pager items
As an example, I wrote the following code (for simplicities sake, I made Text items, but in reality, I'm actually making more complex Card items):
#Composable
fun MyText(modifier: Modifier) {
Text(
text = LOREM_IPSUM_TEXT,
modifier = modifier
.wrapContentHeight()
.border(BorderStroke(1.dp, Color.Red))
)
}
#ExperimentalPagerApi
#Composable
fun MyPager(pagerItem: #Composable () -> Unit = {}) {
Scaffold {
Column(
modifier = Modifier
.fillMaxSize()
// In case items in the VP are taller than the screen -> scrollable
.verticalScroll(rememberScrollState())
) {
HorizontalPager(
contentPadding = PaddingValues(32.dp),
itemSpacing = 16.dp,
count = 3,
) {
pagerItem()
}
}
}
}
#ExperimentalPagerApi
#Preview
#Composable
fun MyPager_200dpWidth() {
MyPager { MyText(modifier = Modifier.widthIn(max = 200.dp)) }
}
#ExperimentalPagerApi
#Preview
#Composable
fun MyPager_500dpWidth() {
MyPager { MyText(modifier = Modifier.widthIn(max = 500.dp)) }
}
#ExperimentalPagerApi
#Preview
#Composable
fun MyPager_FillMaxWidth() {
MyPager { MyText(modifier = Modifier.fillMaxWidth()) }
}
The issue I'm having is that when I make the item have a max width that seems to be smaller than the screen width (see MyPager_200dpWidth), I no longer see the items on the side anymore. On the other hand, using items with larger max widths (See MyPager_500dpWidth) or fillMaxWidth (See MyPager_FillMaxWidth) allows me to see the items on the side.
It seems weird to me that items with the smaller max width are actually taking up more horizontal space than the items with the larger max width. This can be shown in the Preview section...
See how the images on the left (MyPager_500dpWidth) and middle (MyPager_FillMaxWidth) show the next item, while the image on the right (MyPager_200dpWidth`) takes up the whole screen? Here's another comparison when hovering my mouse over the items to see the skeleton boxes...
Just wondering if someone could help me figure out how I can solve this use case. Is it possible that there's a bug in Compose?
The page size is controlled by the HorizontalPager.contentPadding parameter.
Applying widthIn(max = 200.dp) to your text only reduces the size of your element inside the page, while the page size remains unchanged.
Applying more padding should solve your problem, for example, contentPadding = PaddingValues(100.dp) looks like this:
You can fix this by adding your contentPadding like this
val horizontalPadding = 16.dp
val itemWidth = 340.dp
val screenWidth = LocalConfiguration.current.screenWidthDp
val contentPadding = PaddingValues(start = horizontalPadding, end = (screenWidth - itemWidth + horizontalPadding).dp)

Categories

Resources