Пейджер в Compose

Для горизонтального или вертикального перелистывания контента можно использовать composable-функции HorizontalPager и VerticalPager. Они выполняют функции, аналогичные ViewPager в системе View. По умолчанию 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 компонуются и размещаются по мере необходимости. Когда пользователь прокручивает страницы, composable-функция удаляет те, которые больше не нужны.

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

По умолчанию пейджер загружает только видимые страницы на экране. Чтобы загружать больше страниц за пределами экрана, задайте для параметра 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 можно задать расчет размера по умолчанию (Fill), а также на основе Fixed или собственных настроек.

Например, чтобы задать фиксированную ширину страницы 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
}

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

Настройка прокрутки

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

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

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

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)
                )
            }
        }
    }
}

Важные сведения о коде

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

Результат

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

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

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