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

2.2k Views Asked by At

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

2

There are 2 best solutions below

0
On

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.

0
On

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.