Nick Wilde
Nick Wilde

Reputation: 175

How to dynamically add pages to HorizontalPager in jetpack compose based on its size and size of its children

I have a simple list of items that have two string values title and subtitle. This items should be displayed in a horizontal pager. Pager height is always limited and not always the same. The list of items should be automatically divided into pages depending on their height and available height of the pager.

For example, if I have 10 items and there can only be placed first 3 items on one page, then add new page and place next items.

The Page composable looks like this:

@Composable
fun ItemsPage(items: List<Item>) {
    Column {
        items.forEach {
            SingleItem(item = it)
        }
    }
}

Composable for single item looks like this:

@Composable
fun SingleItem(item: Item) {
Column {
            Text(
                text = item.title,
                maxLines = 2,
                overflow = TextOverflow.Ellipsis
            )
            Text(
                text = item.subtitle,
                maxLines = 2,
                color = Color.LightGray,
                overflow = TextOverflow.Ellipsis
            )
        }
}

The problem is that depending on String length, Text can be up to 2 lines and I don't know what items and how many will have 2 lines of text.

Current output:

Output is

Expected output:

enter image description here enter image description here

I had this previously as a ViewItem in ViewPager2 and RecyclerView.Adapter and I solved it by inflating ViewItem XML, populating text views with text and measuring root view with View.measure(widthMeasureSpec, heightMeasureSpec). But now I need to migrate it to compose.

Is there anything similar in compose or any other solution? How can I measure each composable without actually showing/drawing it and efficiently, avoiding too many re-renders?

Upvotes: 1

Views: 1784

Answers (1)

EsatGozcu
EsatGozcu

Reputation: 422

You need to use Layout() to eliminate unfit items. You can also check this post. Here is our example:

MainScreen:

data class Item(val title: String, val subTitle: String)
data class ContainerItem(val placeable: Placeable, val yPosition: Int)
data class RowItem(val count: Int,var totalCount: Int)

@Composable
fun MainPage() {
    val items = remember{
        mutableStateListOf(
            Item("Title1", "subTitle"),
            Item("Title2", "subTitle"),
            Item("Title3", "subTitle"),
            Item("Title4", "subTitle"),
            Item("Title5", "subTitle"),
            Item("Title6", "subTitle"),
            Item("Title7", "subTitle"),
        )
    }

    val rowSize = remember { mutableStateOf(1)}
    val rowItems = remember { mutableStateListOf<RowItem>()}

    Box {
        Row {
            repeat(rowSize.value){
                Column(Modifier
                    .height(290.dp)
                    .background(Color.Red))
                {
                    //Keep index for each row
                    val index = remember { mutableStateOf(it)}
                    VerticalContainer(modifier = Modifier.background(Color.Green),
                        onPlacementComplete = { count ->
                            //Find total Count
                            var totalCount = count
                            for (i in rowItems) {
                                totalCount += i.count
                            }
                            //Add Item
                            rowItems.add(RowItem(count = count,totalCount))
                            //Increase row count
                            if (totalCount < items.size){
                                rowSize.value += 1
                            }
                        }) {
                        val start = remember { mutableStateOf(0)}
                        //Detect start value
                        start.value = if (index.value == 0) 0 else rowItems[index.value-1].totalCount
                        for (item in items.slice(start.value until items.size)) {
                            SingleItem(item)
                        }
                    }
                }
            }
        }
    }
}

Vertical Container:

@Composable
fun VerticalContainer(
    modifier: Modifier = Modifier,
    onPlacementComplete: (Int) -> Unit,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->

        val placeables = measurables.map { it.measure(constraints) };
        val items = mutableListOf<ContainerItem>()
        var yPosition = 0
        for (placeable in placeables) {
            if (yPosition + placeable.height > constraints.maxHeight) break
            items.add(ContainerItem(placeable, yPosition))
            yPosition += placeable.width
        }
        layout(
            width = items.maxOf { it.placeable.width },
            height = items.last().let { it.yPosition + it.placeable.height }
        ) {
            items.forEach {
                it.placeable.place(0, it.yPosition)
            }
            onPlacementComplete(items.count())
        }
    }
}

Single Item:

@Composable
fun SingleItem(item: Item) {
    Column {
        Text(
            text = item.title,
            maxLines = 2,
        )
        Text(
            text = item.subTitle,
            maxLines = 2,
            color = Color.Gray,
        )
    }
}

ScreenShot:

Height: 290.dp Height: 180.dp
290.dp 180.dp

Upvotes: 0

Related Questions