matip
matip

Reputation: 890

Compose Horizontal Pager items with same height but minimum of item width

I am new to compose and currently am facing problem when trying to create HorizontalPager. Previously it was displayed so that pager item fills width and then height is same as width:

Surface(
    modifier = Modifier
        .fillMaxWidth()
        .aspectRatio(1.0f),
    elevation = 16.dp
) { ... }

Now I need to change it so that item still fills width but height wrap content and then every item's height is being evened out to heighest item. Also I would like minimum height be the same as width.

I found something like Intrinsic measurements but when tried to use it nothing happened:

Surface(
        modifier = Modifier
            .fillMaxWidth()
            .height(IntrinsicSize.Min),
        elevation = 16.dp
    ) { ... }

Also I have no idea how to set min height to be the same as width

Upvotes: 8

Views: 4332

Answers (3)

David Sucharda
David Sucharda

Reputation: 51

So I tried to find a way to keep HorizontalPager with all of its functionality (not using beyondBoundsPageCount and keeping "lazy" functionality). And I actually draw the largest item, calculate its size and then set the size to all other items. Something like:

@Composable
fun PagerExample() {
    val loremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut " +
        "labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " +
        "aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore " +
        "eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt " +
        "mollit anim id est laborum."
    val pages = listOf(
        loremIpsum,
        loremIpsum.take(140),
        loremIpsum.take(10),
    )
    val pagerState = rememberPagerState { pages.size }
    var pageHeight by remember(pages) { mutableStateOf((-1).dp) }

    Surface {
        Box {
            if (pageHeight < 0.dp) {
                pages.maxByOrNull { it.length }?.let { largestPage ->
                    val density = LocalDensity.current
                    Text(
                        modifier = Modifier.onGloballyPositioned {
                            with(density) {
                                pageHeight = it.size.height.toDp()
                            }
                        },
                        text = largestPage
                    )
                }
            }
            HorizontalPager(state = pagerState) {
                val heightModifier = if (pageHeight > 0.dp) {
                    Modifier.height(pageHeight)
                } else {
                    Modifier
                }
                Column(
                    modifier = Modifier
                        .padding(16.dp)
                        .then(heightModifier)
                        .background(Color.Red),
                ) {
                    Text(text = "Page $it / ${pagerState.pageCount}")
                    Spacer(modifier = Modifier.height(16.dp))
                    Text(text = pages[it])
                }
            }
        }
    }
}

The code will:

  1. Finds the largest item (if it can be done).
  2. Draws the item.
  3. Measure its height and convert it into Dp.
  4. Set the height to all items of the Pager.
  5. The largest item will be removed because we already have the height which is remembered based on items. So if the items change it should reset and cause the largest item to draw again.

It also keeps the cards on their original height if the fixed height could not be calculated for some reason.

I keep the Pager drawn above the largest item so the user can always see proper data and not the data of some far away item but it could be placed into else branch to not be displayed.


Note that to not actually draw the largest item you can use SubcomposeLayout to only measure it. Something like this:

if (pageHeight < 0.dp) {
    pages.maxByOrNull { it.length }?.let { largestPage ->
        SubcomposeLayout { constraints ->
            val contentPlaceable = subcompose("TmpLargestPage") {
                Text(text = largestPage)
            }.first().measure(constraints)

            pageHeight = contentPlaceable.height.toDp()

            layout(constraints.maxWidth, contentPlaceable.height) {
                //NO DRAW//contentPlaceable.place(0, 0)
            }
        }
    }
}

Upvotes: 2

Javi Garc&#237;a
Javi Garc&#237;a

Reputation: 17

Is it the actual size of the items that you want to change? Or just the size of the pager?

Just setting beyondBoundsPageCount to the pageCount will make the pager render all the pages at the same time, and it will set its size to wrap the biggest element.

@Composable
fun PagerExample() {
    val pages = listOf(
        loremIpsum,
        loremIpsum.take(140),
        loremIpsum.take(10),
    )
    val pagerState = rememberPagerState {
        pages.size
    }
    Surface {
        HorizontalPager(
            state = pagerState,
            beyondBoundsPageCount = pagerState.pageCount
        ) {
            Column(
                modifier = Modifier
                    .padding(16.dp)
                    .background(Color.Red),
            ) {
                Text(text = "Page $it / ${pagerState.pageCount}")
                Spacer(modifier = Modifier.height(16.dp))
                Text(text = pages[it])
            }
        }
    }
}

See the result here

If you don't want it aligned to the center, just use the pager property verticalAlignment = Alignment.Top,

Important note:

This does not change the actual size of the elements inside the pager but makes the pager wrap the size of the biggest element.

Note as well that it will make all the pages be rendered, losing then the "lazy" functionality of the pager. If you have many pages, it may result in performance issues.

Upvotes: 0

JHowzer
JHowzer

Reputation: 4264

I encountered this same problem. Unfortunately, there's no obvious standard practice in any of the Android Developer documentation.

First approach: IntrinsicSize

First, I tried with IntrinsicSize.Min, which did not work. I used the following code. But it won't even render in the Design panel. Instead it shows warnings/issues (java.lang.IllegalStateException: Asking for intrinsic measurements of SubcomposeLayout layouts is not supported).

private const val LO_IP = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."

@OptIn(ExperimentalFoundationApi::class)
@Preview(widthDp = 380, backgroundColor = 0xF2F5F7, showBackground = true)
@Composable
private fun Carousel_IntrinsicSize_Test() {
    val pagerState = rememberPagerState(pageCount = { 2 })
    HorizontalPager(
        state = pagerState,
        beyondBoundsPageCount = pagerState.pageCount,
        modifier = Modifier.height(intrinsicSize = IntrinsicSize.Max),
    ) { page ->
        Text(
            modifier = Modifier
                .fillMaxSize()
                .background(color = Color.Red),
            text = when (page) {
                0 -> LO_IP
                else -> LO_IP.take(140)
            },
        )
    }
}

Approach 2: onSizeChanged + MutableState

Inspired by some of the posts in Get height of element Jetpack Compose, I tried an approach where I stored the height of the largest element in the HorizontalPager as a MutableState and used that to determine the minimum height of every element in the HorizontalPager

@OptIn(ExperimentalFoundationApi::class)
@Preview(widthDp = 380, backgroundColor = 0xF2F5F7, showBackground = true)
@Composable
private fun Carousel_OnSizeChanged_Test() {
    var minHeight by remember { mutableStateOf(0.dp) }
    val density = LocalDensity.current
    val pagerState = rememberPagerState(pageCount = { 2 })
    HorizontalPager(
        state = pagerState,
        beyondBoundsPageCount = pagerState.pageCount,
        modifier = Modifier.wrapContentHeight(),
    ) { page ->
        Text(
            modifier = Modifier
                .fillMaxWidth()
                .heightIn(min = minHeight)
                .background(color = Color.Red)
                .onSizeChanged {
                    density.run {
                        val height = it.height.toDp()
                        if(height > minHeight) { minHeight = height }
                    }
                },
            text = when (page) {
                0 -> LO_IP
                else -> LO_IP.take(140)
            },
        )
    }
}

demo

As can be seen in the demo above, the heights of all the elements stay the same. While this is the easiest (and only) working solution that I've found, I'm unfortunately unable to speak to whether it's the most performant/optimal approach. As discussed in some of the solutions in Get height of element Jetpack Compose, there may be a risk of unnecessary recompositions.

Upvotes: 5

Related Questions