Pantrix UI

Pagination

Moves between the pages of a long list or a table.

import com.pantrixui.components.data.pagination.PantrixPagination

Examples

Moving between pages

At its plainest: the page on screen (page), how many pages there are (pageCount) and what happens when a page is asked for (onPageChange). Which page the app is on is the screen's state; the row does not hold it: it reports the number of the page that was asked for, and shows it once page says so.

Press a number, or one of the arrows at the two ends: the page that was asked for is written on the card's "Last event" line. The page on screen has a fill and is no button: a press on it reports nothing. Previous cannot be pressed on the first page, nor next on the last.

There are ten pages: the row shows the first and the last, the one on screen and the ones on its two sides, and folds those between into a gap of three dots. On a phone held upright that much does not fit. There the row, by the phone's width, either shows fewer numbers or is in its short form: previous, "Page 1 of 10" and next.

val report = LocalExampleEvents.current
var page by rememberSaveable { mutableIntStateOf(1) }
PantrixPagination(
    page = page,
    pageCount = 10,
    onPageChange = { asked ->
        page = asked
        report("Page $asked was asked for")
    },
    tagKey = "plain"
)

A list shown a page at a time

A list of forty-eight orders, five to a page: that makes ten pages, with three orders left for the last. The number of pages is worked out from the list itself. The row only reports which page was asked for; showing that page's orders is the screen's work.

Press next, or a number where the numbers fit: the list goes to that page's orders, and the text under it says which orders are shown. Go to the last page: there are three orders there, and next cannot be pressed.

The list is five orders tall on every page: on the last page the two rows that have no order stand empty. Had the list grown shorter, the row under it would have moved up, out from under the finger that pressed it.

var page by rememberSaveable { mutableIntStateOf(1) }
val first = (page - 1) * ORDERS_PER_PAGE + 1
val last = minOf(page * ORDERS_PER_PAGE, ORDER_COUNT)
OrderRows(first = first, last = last, rows = ORDERS_PER_PAGE, tagKey = "order")
PantrixText(
    text = "$first–$last of $ORDER_COUNT orders",
    color = PantrixTheme.colors.textSecondary,
    tagKey = "orders_shown"
)
PantrixPagination(
    page = page,
    pageCount = (ORDER_COUNT + ORDERS_PER_PAGE - 1) / ORDERS_PER_PAGE,
    onPageChange = { page = it },
    tagKey = "orders"
)

@Composable
private fun OrderRows(
    first: Int,
    last: Int,
    rows: Int,
    tagKey: String
) {
    PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
        Column {
            for (order in first until first + rows) {
                val listed = order <= last
                // The line of an empty row keeps its place too, but is not drawn.
                val line = if (listed) Color.Unspecified else Color.Transparent
                if (order > first) PantrixHorizontalDivider(color = line)
                Row(
                    modifier = Modifier.fillMaxWidth().padding(PantrixTheme.spacing.md),
                    horizontalArrangement = Arrangement.SpaceBetween
                ) {
                    if (listed) {
                        PantrixText(text = "Order #${FIRST_ORDER + order}", tagKey = "${tagKey}_$order")
                        PantrixText(text = "Items: ${1 + order % 4}", color = PantrixTheme.colors.textSecondary)
                    } else {
                        // An empty text is a line tall too; it has nothing to say to a screen reader.
                        PantrixText(text = "", role = TextRole.Decorative)
                    }
                }
            }
        }
    }
}

private const val ORDER_COUNT = 48

private const val ORDERS_PER_PAGE = 5

private const val FIRST_ORDER = 4800

How many pages beside the one on screen and at the ends

siblingCount says how many pages are shown on each side of the page on screen, and boundaryCount how many at each end of the row; each is one unless given. Four rows of twenty pages show the same page: press a page in one of them, and all four go there.

The first row is given neither. The second shows two pages on each side of the page on screen, and the third two pages at each end. In the last one both are zero: only the page on screen is left in the middle, and the others are reached by the arrows.

A row has as many items whichever page is on screen. Press 1 in the first row: at an end one of the gaps goes, and the pages at that end take its place; in the last row the second page stands beside the first.

The rows are in a frame that scrolls sideways: there the width has no end, and the rows keep their numbers on a narrow screen too.

var page by rememberSaveable { mutableIntStateOf(10) }
val secondary = PantrixTheme.colors.textSecondary
HorizontalScrollFrame(tagKey = "around") {
    Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
        PantrixText(text = "Neither is given: one page on each side, one at each end", color = secondary)
        PantrixPagination(page = page, pageCount = 20, onPageChange = { page = it }, tagKey = "around_usual")
        PantrixText(text = "siblingCount = 2", color = secondary)
        PantrixPagination(
            page = page,
            pageCount = 20,
            onPageChange = { page = it },
            siblingCount = 2,
            tagKey = "around_siblings"
        )
        PantrixText(text = "boundaryCount = 2", color = secondary)
        PantrixPagination(
            page = page,
            pageCount = 20,
            onPageChange = { page = it },
            boundaryCount = 2,
            tagKey = "around_boundaries"
        )
        PantrixText(text = "siblingCount = 0, boundaryCount = 0", color = secondary)
        PantrixPagination(
            page = page,
            pageCount = 20,
            onPageChange = { page = it },
            siblingCount = 0,
            boundaryCount = 0,
            tagKey = "around_none"
        )
    }
}

With buttons that go to the first and the last page

firstAndLast = true puts one more button outside previous and outside next: one goes to the first page, the other to the last. It is for going from one end to the other in one press, in a list of many pages such as an archive.

Press the last page button: the row goes to the fortieth page; there neither next nor the last page button can be pressed. The first page button goes to the first page, and there the two buttons at the start cannot be pressed. The page that was asked for is written on the card's "Last event" line.

Four buttons leave less room for the numbers: at the same width this row folds before one with two buttons does. On a phone held upright it is in its short form: two buttons, "Page 12 of 40" and two buttons. On the narrowest phones, where four buttons leave no room for the words, the end of the words is cut and the buttons are not.

val report = LocalExampleEvents.current
var page by rememberSaveable { mutableIntStateOf(12) }
PantrixPagination(
    page = page,
    pageCount = 40,
    onPageChange = { asked ->
        page = asked
        report("Page $asked was asked for")
    },
    firstAndLast = true,
    tagKey = "ends"
)

Where it is too narrow for the numbers

The same row at three widths: the first as wide as the example, the second in a frame of 320dp, the third in one of 240dp. The row fits its place step by step. The first frame is too narrow for the seven items of ten pages: there the row gives up the pages beside the page on screen, and stands with five items. In the second frame that does not fit either: the row is laid out as previous, "Page 3 of 10" and next. Nothing was given for this: the row looks at the width left to it, not at the window.

The three show the same page: press next in one of the frames, and all three go to that page. A row folds the same way on every page: none of them changes its layout as the page moves. Make the window narrower: the row at the top also shows fewer numbers first, and then goes to its short form.

The frames' widths were chosen for the 32dp items of desktop and the web. On a phone the items are larger, and the touch target is kept free around them: there both frames are too narrow for the numbers, and the short row stands in both. The row at the top, by the phone's width, either shows fewer numbers or is short too.

var page by rememberSaveable { mutableIntStateOf(3) }
PantrixPagination(page = page, pageCount = 10, onPageChange = { page = it }, tagKey = "roomy")
PantrixSurface(modifier = Modifier.width(320.dp), border = PantrixTheme.colors.divider) {
    PantrixPagination(
        page = page,
        pageCount = 10,
        onPageChange = { page = it },
        modifier = Modifier.padding(horizontal = PantrixTheme.spacing.md),
        tagKey = "fewer"
    )
}
PantrixSurface(modifier = Modifier.width(240.dp), border = PantrixTheme.colors.divider) {
    PantrixPagination(
        page = page,
        pageCount = 10,
        onPageChange = { page = it },
        modifier = Modifier.padding(horizontal = PantrixTheme.spacing.md),
        tagKey = "narrow"
    )
}

The short row at every width

PaginationStyle(layout = PaginationLayout.Compact) asks for the short row at every width: where the numbers would fit, too, the row is previous, the page's place and next. It is for a place where people step from page to page and do not jump to a number, such as the foot of a table.

Press next: the words become "Page 2 of 5"; a screen reader is told the changed words without being asked. Next cannot be pressed on the fifth page, nor previous on the first.

var page by rememberSaveable { mutableIntStateOf(1) }
PantrixPagination(
    page = page,
    pageCount = 5,
    onPageChange = { page = it },
    style = PaginationStyle(layout = PaginationLayout.Compact),
    tagKey = "compact"
)

A row that cannot be pressed while the list loads

enabled = false turns the whole row off: the numbers, previous and next cannot be pressed, and the row is drawn in the theme's disabled colours. It is used so that a second page is not asked for while one is loading: enabled = !loading.

While the switch is on, the list counts as loading: press next or, where the numbers fit, a number; the card's "Last event" line does not change. Turn the switch off and press again: the page that was asked for is reported. The page on screen is told in a row that cannot be pressed too: by its fill in a row of numbers, by its words in the short row.

If the list starts loading while the keyboard is on a number or an arrow, the focus stays there: what it is on cannot be pressed, but once the list has come the keyboard goes on from where it was.

val report = LocalExampleEvents.current
var loading by rememberSaveable { mutableStateOf(true) }
var page by rememberSaveable { mutableIntStateOf(4) }
PantrixSwitchButton(
    checked = loading,
    onCheckedChange = { loading = it },
    label = "The list is loading",
    tagKey = "list_loading"
)
PantrixPagination(
    page = page,
    pageCount = 10,
    onPageChange = { asked ->
        page = asked
        report("Page $asked was asked for")
    },
    enabled = !loading,
    tagKey = "while_loading"
)

In a table's foot: page size and go to page

A table's foot: PantrixPageSize chooses how many orders a page shows, PantrixPagination moves between the pages, and PantrixPageJump goes to the page whose number is typed. The three are in a FlowRow: where there is room they stand side by side, and in a narrow place the one that does not fit goes down to the next line; on a phone held upright the three stand one under another. A Row does not take what does not fit down a line: it squeezes the last one into the room that is left. The three may not be the same height: itemVerticalAlignment lines them up by their middles.

All three read the screen's state, and none knows of the others. Change the page size: forty-eight orders make sixteen pages three at a time, and five pages ten at a time; the row's numbers and the number of digits the field takes follow the new number of pages. As the size changes the first order shown stays on screen: the page that is opened is the one that order falls on at the new size. On the fourth page, with orders 16–20 shown, choose 10: the second page comes, orders 11–20. For forty-eight orders the sizes are small: 3 and 5, before the first of the ready sizes (DefaultPageSizes), 10.

Type a number in the field and press Enter: that page is opened, the field is emptied and the focus stays in the field. A number larger than the last page opens the last page; the field takes nothing but digits. The page that was asked for, whatever asked for it, is written on the card's "Last event" line.

val report = LocalExampleEvents.current
var pageSize by rememberSaveable { mutableIntStateOf(ORDERS_PER_PAGE) }
var page by rememberSaveable { mutableIntStateOf(1) }
val pageCount = (ORDER_COUNT + pageSize - 1) / pageSize
val first = (page - 1) * pageSize + 1
val last = minOf(page * pageSize, ORDER_COUNT)
val open: (Int) -> Unit = { asked ->
    page = asked
    report("Page $asked was asked for")
}
OrderRows(first = first, last = last, rows = pageSize, tagKey = "row")
PantrixText(
    text = "$first–$last of $ORDER_COUNT orders",
    color = PantrixTheme.colors.textSecondary,
    tagKey = "foot_shown"
)
FlowRow(
    horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.lg),
    verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm),
    itemVerticalAlignment = Alignment.CenterVertically
) {
    PantrixPageSize(
        pageSize = pageSize,
        onPageSizeChange = { size ->
            // The page that is opened is the one the first order shown falls on at the new size: the order stays on screen.
            val kept = (first - 1) / size + 1
            pageSize = size
            page = kept
            report("$size orders to a page: page $kept was opened")
        },
        pageSizes = listOf(3, 5) + DefaultPageSizes.first(),
        tagKey = "foot"
    )
    PantrixPagination(page = page, pageCount = pageCount, onPageChange = open, tagKey = "foot")
    PantrixPageJump(pageCount = pageCount, onPageChange = open, tagKey = "foot")
}

@Composable
private fun OrderRows(
    first: Int,
    last: Int,
    rows: Int,
    tagKey: String
) {
    PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
        Column {
            for (order in first until first + rows) {
                val listed = order <= last
                // The line of an empty row keeps its place too, but is not drawn.
                val line = if (listed) Color.Unspecified else Color.Transparent
                if (order > first) PantrixHorizontalDivider(color = line)
                Row(
                    modifier = Modifier.fillMaxWidth().padding(PantrixTheme.spacing.md),
                    horizontalArrangement = Arrangement.SpaceBetween
                ) {
                    if (listed) {
                        PantrixText(text = "Order #${FIRST_ORDER + order}", tagKey = "${tagKey}_$order")
                        PantrixText(text = "Items: ${1 + order % 4}", color = PantrixTheme.colors.textSecondary)
                    } else {
                        // An empty text is a line tall too; it has nothing to say to a screen reader.
                        PantrixText(text = "", role = TextRole.Decorative)
                    }
                }
            }
        }
    }
}

private const val ORDER_COUNT = 48

private const val ORDERS_PER_PAGE = 5

private const val FIRST_ORDER = 4800

In Arabic: a row that starts on the right, and Arabic digits

The same row in an Arabic section: the inner theme is given locale alone, PantrixLocale.Arabic. The row starts on the right: previous is on the right, the arrows turn round, and the numbers grow towards the left. The numbers are written in Arabic digits: the language's pack carries its digits too (PantrixStrings.digits). The one below is the short row: its words are Arabic, and its numbers are written in the same digits.

Press a page: the card's "Last event" line writes the page that was asked for in the app's language. The digits change the writing alone: onPageChange reports the same number in every language. The names said to a screen reader are Arabic too, and are written in the same digits.

On a phone held upright the row at the top, by the phone's width, also shows fewer numbers or is short.

val report = LocalExampleEvents.current
var page by rememberSaveable { mutableIntStateOf(3) }
val open: (Int) -> Unit = { asked ->
    page = asked
    report("Page $asked was asked for")
}
PantrixTheme(locale = PantrixLocale.Arabic) {
    Column(
        modifier = Modifier.fillMaxWidth(),
        verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.md)
    ) {
        PantrixPagination(page = page, pageCount = 10, onPageChange = open, tagKey = "arabic")
        PantrixPagination(
            page = page,
            pageCount = 10,
            onPageChange = open,
            style = PaginationStyle(layout = PaginationLayout.Compact),
            tagKey = "arabic_short"
        )
    }
}

With its own colours, and on a fill

colors takes the colour of the numbers and of the short row's words (content), of previous, next and the gaps' marks (control), and of the fill and the number of the page on screen (selectedContainer, selectedContent) one by one; one that is left out stays the theme's. In the first row the arrows and the fill of the page on screen are in the app's own purple, which is in none of the theme's roles. The theme cannot know what reads on a fill it does not have: the colour of its number is given too. The other numbers are given no colour, and stay in the theme's text colour.

The second row is on a fill that is not one of the theme's grounds, and no colour is given: the fill says what is written on it, and all of the row is written in that colour. The page on screen is told by its fill, a share of the same colour.

val own = ownColors()
var page by rememberSaveable { mutableIntStateOf(2) }
PantrixPagination(
    page = page,
    pageCount = 3,
    onPageChange = { page = it },
    colors = PaginationColors(control = own.color, selectedContainer = own.color, selectedContent = own.onColor),
    tagKey = "own"
)
PantrixSurface(
    modifier = Modifier.fillMaxWidth(),
    color = PantrixTheme.colors.primary.color,
    shape = PantrixTheme.shapes.md
) {
    PantrixPagination(
        page = page,
        pageCount = 3,
        onPageChange = { page = it },
        modifier = Modifier.padding(horizontal = PantrixTheme.spacing.md),
        tagKey = "filled"
    )
}

Sizes and shape

style gives the row's sizes; only the fields that are set change. itemSize is the size of a number, of previous and of next, itemShape their corners, and itemGap the room between two neighbours. The arrows' icon grows and shrinks with its item: it is 4dp more than half of it.

The middle row is as it is with nothing given. The one above it is built with 24dp items, fully round and with no room put between them; the one below it with 48dp items, medium corners and 8dp between them. The platform's touch target is kept free around the small items too: the items shrink, but come no closer to each other than that target.

A larger item is not written larger by itself: the bottom row is also given the theme's title style, with textStyle.

var page by rememberSaveable { mutableIntStateOf(2) }
PantrixPagination(
    page = page,
    pageCount = 3,
    onPageChange = { page = it },
    style = PaginationStyle(itemSize = 24.dp, itemShape = ShapeRole.Full, itemGap = 0.dp),
    tagKey = "small"
)
PantrixPagination(page = page, pageCount = 3, onPageChange = { page = it }, tagKey = "usual")
PantrixPagination(
    page = page,
    pageCount = 3,
    onPageChange = { page = it },
    textStyle = PantrixTheme.typography.title,
    style = PaginationStyle(itemSize = 48.dp, itemShape = ShapeRole.Medium, itemGap = 8.dp),
    tagKey = "large"
)

API

page

Moves between the pages of a long list or a table.

ParameterTypeDefaultWhat it is
pageIntthe page on screen, counted from one, as it is written on the row: a pager's or a carousel's index counts from nought, and is this less one. A number outside the pages is taken as the nearest page.
pageCountInthow many pages there are. A list with nothing in it has one page, an empty one: a count under one is taken as one.
onPageChange(page: Int) -> Unittold the page that was asked for, by its number or by a button. The row shows it once page says so.
modifierModifierModifierapplied to the row.
siblingCountInt1in a row of numbers, how many pages are shown on each side of the page on screen where there is room for them: none or more. More than there are pages is all of them.
boundaryCountInt1in a row of numbers, how many pages are always shown at each end where there is room for them: none or more.
firstAndLastBooleanfalsewhether the row has, outside previous and next, the two buttons that go to the first page and to the last.
enabledBooleantruewhether the row can be pressed: one that cannot is drawn in the theme's disabled colours.
textStyleTextStylePantrixTheme.typography.labelthe type the numbers and the words are set in: the theme's label unless given. An item that is made larger by the style is not written larger by itself.
stylePaginationStyle?nullthis call's own style: over every enclosing one and under the density. Only the fields it sets change, on every platform.
colorsPaginationColors?nullthis call's own colours. Only the ones it sets change; the rest stay the theme's.
tagKeyString?nulla locale-independent key for automation: the row's tag is Pagination_<tagKey>, and its parts are named inside it, Pagination_<tagKey>.Page_<number>, Pagination_<tagKey>.Step_previous, Pagination_<tagKey>.Step_next, Pagination_<tagKey>.Step_first, Pagination_<tagKey>.Step_last, and Pagination_<tagKey>.Text_position for the words of a row without numbers.

PaginationStyle

PantrixPagination's layout and geometry.

ParameterTypeDefaultWhat it is
layoutPaginationLayout?nullHow the pages are laid out: by their numbers on every platform, in a row that has the room.
itemSizeDpDp.UnspecifiedA page's number, previous and next: 40dp on Android, 36dp on iOS, 32dp on desktop and the web. A number too long for it makes its item wider. The icons in previous and next are half of it and 4dp.
itemShapeShapeSpecShapeSpec.UnspecifiedThe corners of an item, of its fill where it is the page on screen and of its ring: round on Android and iOS, small on desktop and the web.
itemGapDpDp.UnspecifiedBetween two neighbours of the row.

PaginationColors

What a PantrixPagination is painted with, for a row that does not take the theme's colours: a colour that is set is used, and one that is left out stays the theme's.

ParameterTypeDefaultWhat it is
contentColorColor.UnspecifiedThe numbers of the pages that are not on screen, and the words of a row that has no room for numbers. The theme's textPrimary.
controlColorColor.UnspecifiedPrevious and next, and the marks that stand for the pages folded away. The theme's textSecondary.
selectedContainerColorColor.UnspecifiedThe fill of the page on screen. The primary role's container.
selectedContentColorColor.UnspecifiedThe number of the page on screen: what the theme pairs with selectedContainer, which is the primary role's onContainer on the fill the row has by itself. The theme has no pair for a fill that is none of its roles' and writes on it as on the row's own: give such a fill its number too.

Accessibility