Пейджер в Compose

Чтобы перелистывать контент по горизонтали или вертикали, можно использовать композиции HorizontalPager и VerticalPager. Они выполняют те же функции, что и ViewPager в системе просмотра. По умолчанию HorizontalPager занимает всю ширину экрана, а VerticalPager – всю высоту. Пейджеры также могут передавать только одну страницу за раз. Все эти значения по умолчанию можно изменить.

HorizontalPager

Чтобы создать пейджер с горизонтальной прокруткой влево и вправо, используйте следующий код: HorizontalPager:

Рисунок 1. Демоверсия HorizontalPager

// Display 10 items
val pagerState = rememberPagerState(pageCount = {
    10
})
HorizontalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier.fillMaxWidth()
    )
}

VerticalPager

Чтобы создать пейджер с прокруткой вверх и вниз, используйте VerticalPager:

Рисунок 2. Демоверсия VerticalPager

// Display 10 items
val pagerState = rememberPagerState(pageCount = {
    10
})
VerticalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier.fillMaxWidth()
    )
}

Отложенное создание

Страницы в HorizontalPager и VerticalPager создаются по мере необходимости. Когда пользователь прокручивает страницы, компонент удаляет ненужные страницы.

Загрузка большего количества страниц за пределами экрана

По умолчанию загружаются только видимые на экране страницы. Чтобы загрузить больше страниц за пределами экрана, задайте для параметра beyondBoundsPageCount значение больше нуля.

Прокрутка до элемента в пейджере

Чтобы прокрутить до определенной страницы в пейджере, создайте объект PagerState с помощью rememberPagerState() и передайте его в качестве параметра state пейджеру. В этом состоянии можно позвонить по номеру PagerState#scrollToPage() в рамках CoroutineScope:

val pagerState = rememberPagerState(pageCount = {
    10
})
HorizontalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier
            .fillMaxWidth()
            .height(100.dp)
    )
}

// scroll to page
val coroutineScope = rememberCoroutineScope()
Button(onClick = {
    coroutineScope.launch {
        // Call scroll to on pagerState
        pagerState.scrollToPage(5)
    }
}, modifier = Modifier.align(Alignment.BottomCenter)) {
    Text("Jump to Page 5")
}

Если вы хотите добавить анимацию на страницу, используйте функцию PagerState#animateScrollToPage():

val pagerState = rememberPagerState(pageCount = {
    10
})

HorizontalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier
            .fillMaxWidth()
            .height(100.dp)
    )
}

// scroll to page
val coroutineScope = rememberCoroutineScope()
Button(onClick = {
    coroutineScope.launch {
        // Call scroll to on pagerState
        pagerState.animateScrollToPage(5)
    }
}, modifier = Modifier.align(Alignment.BottomCenter)) {
    Text("Jump to Page 5")
}

Как получать уведомления об изменениях состояния страницы

PagerState содержит три свойства с информацией о страницах: currentPage, settledPage и targetPage.

  • currentPage – ближайшая к позиции привязки страница. По умолчанию позиция привязки находится в начале макета.
  • settledPage – номер страницы, когда анимация или прокрутка не выполняются. Это свойство отличается от currentPage тем, что currentPage обновляется сразу, если страница достаточно близка к позиции прикрепления, а settledPage остается неизменным, пока не будут выполнены все анимации.
  • targetPage – предлагаемая позиция остановки при прокрутке.

Функция snapshotFlow позволяет отслеживать изменения этих переменных и реагировать на них. Например, чтобы отправлять событие Аналитики при каждом изменении страницы, можно сделать следующее:

val pagerState = rememberPagerState(pageCount = {
    10
})

LaunchedEffect(pagerState) {
    // Collect from the a snapshotFlow reading the currentPage
    snapshotFlow { pagerState.currentPage }.collect { page ->
        // Do something with each page change, for example:
        // viewModel.sendPageSelectedEvent(page)
        Log.d("Page change", "Page changed to $page")
    }
}

VerticalPager(
    state = pagerState,
) { page ->
    Text(text = "Page: $page")
}

Как добавить индикатор страницы

Чтобы добавить индикатор на страницу, используйте объект PagerState, чтобы получить информацию о том, какая страница выбрана из общего количества страниц, и нарисуйте собственный индикатор.

Например, чтобы создать индикатор в виде круга, можно повторить количество кругов и изменить цвет круга в зависимости от того, выбрана ли страница, используя pagerState.currentPage:

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    modifier = Modifier.fillMaxSize()
) { page ->
    // Our page content
    Text(
        text = "Page: $page",
    )
}
Row(
    Modifier
        .wrapContentHeight()
        .fillMaxWidth()
        .align(Alignment.BottomCenter)
        .padding(bottom = 8.dp),
    horizontalArrangement = Arrangement.Center
) {
    repeat(pagerState.pageCount) { iteration ->
        val color = if (pagerState.currentPage == iteration) Color.DarkGray else Color.LightGray
        Box(
            modifier = Modifier
                .padding(2.dp)
                .clip(CircleShape)
                .background(color)
                .size(16.dp)
        )
    }
}

Индикатор страниц в виде круга под контентом.
Рисунок 3. Навигатор с кружком под контентом

Как применить к контенту эффекты прокрутки объектов

Например, можно использовать позицию прокрутки, чтобы применять эффекты к элементам пейджера. Чтобы узнать, насколько далеко страница находится от выбранной, используйте значок PagerState.currentPageOffsetFraction. Затем вы можете применить к контенту эффекты преобразования в зависимости от расстояния до выбранной страницы.

Рисунок 4. Применение преобразований к контенту Pager

Например, чтобы настроить прозрачность объектов в зависимости от того, насколько они удалены от центра, измените alpha, используя Modifier.graphicsLayer на объекте внутри пейджера:

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(state = pagerState) { page ->
    Card(
        Modifier
            .size(200.dp)
            .graphicsLayer {
                // Calculate the absolute offset for the current page from the
                // scroll position. We use the absolute value which allows us to mirror
                // any effects for both directions
                val pageOffset = (
                    (pagerState.currentPage - page) + pagerState
                        .currentPageOffsetFraction
                    ).absoluteValue

                // We animate the alpha, between 50% and 100%
                alpha = lerp(
                    start = 0.5f,
                    stop = 1f,
                    fraction = 1f - pageOffset.coerceIn(0f, 1f)
                )
            }
    ) {
        // Card content
    }
}

Пользовательские размеры страниц

По умолчанию HorizontalPager и VerticalPager занимают всю ширину или высоту соответственно. Вы можете задать для переменной pageSize значение Fixed, Fill (по умолчанию) или указать собственный способ расчета размера.

Например, чтобы задать фиксированную ширину страницы 100.dp, выполните следующие действия:

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    pageSize = PageSize.Fixed(100.dp)
) { page ->
    // page content
}

Чтобы задать размер страниц на основе размера области просмотра, используйте специальный расчет размера страницы. Создайте специальный объект PageSize и разделите availableSpace на три, учитывая расстояние между элементами:

private val threePagesPerViewport = object : PageSize {
    override fun Density.calculateMainAxisPageSize(
        availableSpace: Int,
        pageSpacing: Int
    ): Int {
        return (availableSpace - 2 * pageSpacing) / 3
    }
}

Отступы контента

HorizontalPager и VerticalPager позволяют изменять отступы контента, что влияет на максимальный размер и выравнивание страниц.

Например, если задать значение start, страницы будут выровнены по правому краю:

Пейджер с отступом в начале, контент выровнен по правому краю
Рисунок 5. Пейджер с отступом в начале.

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    contentPadding = PaddingValues(start = 64.dp),
) { page ->
    // page content
}

Если задать одинаковые значения для полей start и end, элемент будет выровнен по горизонтали:

Пейджер с полями в начале и конце, контент выровнен по центру
Рисунок 6. Навигация по страницам с горизонтальными отступами.

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    contentPadding = PaddingValues(horizontal = 32.dp),
) { page ->
    // page content
}

Если задать отступ end, страницы будут выровнены по началу:

Пейджер с отступами в начале и конце, контент выровнен по началу
Рисунок 7. Пейджер с отступами в конце.

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    contentPadding = PaddingValues(end = 64.dp),
) { page ->
    // page content
}

Вы можете задать значения top и bottom, чтобы получить аналогичный эффект для VerticalPager. Значение 32.dp приведено здесь только в качестве примера. Вы можете задать для каждого из полей любое значение.

Как настроить прокрутку

Компонуемые функции HorizontalPager и VerticalPager по умолчанию определяют, как жесты прокрутки работают с пейджером. Однако вы можете изменить настройки по умолчанию, например pagerSnapDistance или flingBehavior.

Расстояние привязки

По умолчанию HorizontalPager и VerticalPager позволяют прокручивать жестом проведения по экрану не более одной страницы за раз. Чтобы изменить это значение, задайте атрибут pagerSnapDistance для элемента flingBehavior:

val pagerState = rememberPagerState(pageCount = { 10 })

val fling = PagerDefaults.flingBehavior(
    state = pagerState,
    pagerSnapDistance = PagerSnapDistance.atMost(10)
)

Column(modifier = Modifier.fillMaxSize()) {
    HorizontalPager(
        state = pagerState,
        pageSize = PageSize.Fixed(200.dp),
        beyondViewportPageCount = 10,
        flingBehavior = fling
    ) {
        PagerSampleItem(page = it)
    }
}

Как создать автоматически перелистываемый пейджер

В этом разделе рассказывается, как создать автоматически пролистываемый пейджер с индикаторами страниц в Compose. Объекты в подборке автоматически прокручиваются по горизонтали, но пользователи также могут переключаться между ними вручную. Если пользователь взаимодействует с пейджером, автоматическая прокрутка останавливается.

Простой пример

Приведенные ниже фрагменты кода создают базовую реализацию автоматической прокрутки страниц с визуальным индикатором, в которой каждая страница отображается другим цветом:

@Composable
fun AutoAdvancePager(pageItems: List<Color>, modifier: Modifier = Modifier) {
    Box(modifier = Modifier.fillMaxSize()) {
        val pagerState = rememberPagerState(pageCount = { pageItems.size })
        val pagerIsDragged by pagerState.interactionSource.collectIsDraggedAsState()

        val pageInteractionSource = remember { MutableInteractionSource() }
        val pageIsPressed by pageInteractionSource.collectIsPressedAsState()

        // Stop auto-advancing when pager is dragged or one of the pages is pressed
        val autoAdvance = !pagerIsDragged && !pageIsPressed

        if (autoAdvance) {
            LaunchedEffect(pagerState, pageInteractionSource) {
                while (true) {
                    delay(2000)
                    val nextPage = (pagerState.currentPage + 1) % pageItems.size
                    pagerState.animateScrollToPage(nextPage)
                }
            }
        }

        HorizontalPager(
            state = pagerState
        ) { page ->
            Text(
                text = "Page: $page",
                textAlign = TextAlign.Center,
                modifier = modifier
                    .fillMaxSize()
                    .background(pageItems[page])
                    .clickable(
                        interactionSource = pageInteractionSource,
                        indication = LocalIndication.current
                    ) {
                        // Handle page click
                    }
                    .wrapContentSize(align = Alignment.Center)
            )
        }

        PagerIndicator(pageItems.size, pagerState.currentPage)
    }
}

Ключевые моменты

  • Функция AutoAdvancePager создает представление с горизонтальной прокруткой и автоматическим переходом. Он принимает на вход список объектов Color, которые используются в качестве фоновых цветов для каждой страницы.
  • pagerState создается с помощью rememberPagerState, в котором хранится состояние пейджера.
  • pagerIsDragged и pageIsPressed отслеживают взаимодействие пользователей.
  • LaunchedEffect автоматически переходит к следующей странице каждые две секунды, если пользователь не перетаскивает элемент или не нажимает на одну из страниц.
  • HorizontalPager – список страниц, на каждой из которых есть composable-функция Text с номером страницы. Модификатор заполняет страницу, задает цвет фона из pageItems и делает страницу кликабельной.

@Composable
fun PagerIndicator(pageCount: Int, currentPageIndex: Int, modifier: Modifier = Modifier) {
    Box(modifier = Modifier.fillMaxSize()) {
        Row(
            modifier = Modifier
                .wrapContentHeight()
                .fillMaxWidth()
                .align(Alignment.BottomCenter)
                .padding(bottom = 8.dp),
            horizontalArrangement = Arrangement.Center
        ) {
            repeat(pageCount) { iteration ->
                val color = if (currentPageIndex == iteration) Color.DarkGray else Color.LightGray
                Box(
                    modifier = modifier
                        .padding(2.dp)
                        .clip(CircleShape)
                        .background(color)
                        .size(16.dp)
                )
            }
        }
    }
}

Ключевые моменты

  • Компонуемый элемент Box выступает в качестве корневого элемента и содержит Row для горизонтального расположения индикаторов страниц.
  • Специальный индикатор страниц представляет собой ряд кругов, где каждый Box обрезан до CircleShape и обозначает страницу.
  • Кружок текущей страницы окрашен в DarkGray, а остальные – в LightGray. Параметр currentPageIndex определяет, какой круг будет отображаться темно-серым цветом.

Результат

В этом видео показан основной пейджер с автоматической прокруткой из предыдущих фрагментов кода:

Рисунок 8. Автоматически перелистываемый пейджер с двухсекундной задержкой между страницами.

Дополнительные ресурсы