Прокрутка

Модификаторы прокрутки

Модификаторы verticalScroll и horizontalScroll предоставляют самый простой способ позволить пользователю прокручивать элемент, когда границы его содержимого превышают его максимальный размер. С модификаторами verticalScroll и horizontalScroll вам не нужно перемещать или смещать содержимое.

@Composable
private fun ScrollBoxes() {
    Column(
        modifier = Modifier
            .background(Color.LightGray)
            .size(100.dp)
            .verticalScroll(rememberScrollState())
    ) {
        repeat(10) {
            Text("Item $it", modifier = Modifier.padding(2.dp))
        }
    }
}

Простой вертикальный список, реагирующий на жесты прокрутки.
Рисунок 1. Простой вертикальный список, реагирующий на жесты прокрутки.

ScrollState позволяет изменять положение прокрутки или получать её текущее состояние. Для создания объекта с параметрами по умолчанию используйте rememberScrollState() .

@Composable
private fun ScrollBoxesSmooth() {
    // Smoothly scroll 100px on first composition
    val state = rememberScrollState()
    LaunchedEffect(Unit) { state.animateScrollTo(100) }

    Column(
        modifier = Modifier
            .background(Color.LightGray)
            .size(100.dp)
            .padding(horizontal = 8.dp)
            .verticalScroll(state)
    ) {
        repeat(10) {
            Text("Item $it", modifier = Modifier.padding(2.dp))
        }
    }
}

Модификатор прокручиваемой области

Модификатор scrollableArea — это фундаментальный строительный блок для создания пользовательских прокручиваемых контейнеров. Он обеспечивает более высокий уровень абстракции над модификатором scrollable , обрабатывая общие требования, такие как интерпретация изменений жестов, обрезка контента и эффекты перепрокрутки.

Хотя scrollableArea используется для пользовательских реализаций, для стандартных списков с прокруткой, как правило, следует отдавать предпочтение готовым решениям, таким как verticalScroll , horizontalScroll или компонуемым компонентам, например LazyColumn . Эти высокоуровневые компоненты проще для распространенных случаев использования и сами создаются с помощью scrollableArea .

Разница между модификаторами scrollableArea и scrollable

Основное различие между scrollableArea и scrollable заключается в том, как они интерпретируют жесты прокрутки пользователя:

  • scrollable (необработанное значение дельты): Дельта напрямую отражает физическое перемещение ввода пользователя (например, перетаскивание указателя) по экрану.
  • scrollableArea (содержимое-ориентированная дельта): delta семантически инвертируется, чтобы представить выбранное изменение положения прокрутки, благодаря чему содержимое будет выглядеть перемещающимся в соответствии с жестом пользователя, который обычно противоположен движению указателя.

Представьте себе это так: scrollable показывает, как перемещался указатель мыши, а scrollableArea преобразует это перемещение указателя в то, как должно перемещаться содержимое в типичном прокручиваемом представлении. Именно эта инверсия объясняет, почему scrollableArea кажется более естественным при реализации стандартного прокручиваемого контейнера.

В следующей таблице приведено краткое описание знаков дельты для распространенных сценариев:

Жест пользователя

Дельта, передаваемая в dispatchRawDelta через scrollable

delta сообщается в dispatchRawDelta через scrollableArea *

Указатель движется ВВЕРХ

Отрицательный

Положительный

Указатель движется ВНИЗ

Положительный

Отрицательный

Указатель перемещается влево.

Отрицательный

Положительный результат (отрицательный результат для RTL)

Указатель движется ВПРАВО

Положительный

Отрицательный результат (положительный результат для RTL)

(*) Примечание о знаке дельты scrollableArea : Знак дельты от scrollableArea не является простой инверсией. Он учитывает следующее:

  1. Ориентация : вертикальная или горизонтальная.
  2. LayoutDirection : LTR или RTL (особенно важно для горизонтальной прокрутки).
  3. флаг reverseScrolling : Указывает, инвертируется ли направление прокрутки.

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

Когда использовать модификатор scrollableArea

Модификатор scrollableArea следует использовать, когда необходимо создать собственный компонент прокрутки, для которого недостаточно подходят модификаторы horizontalScroll , verticalScroll или Lazy layouts. Это часто связано со следующими случаями:

  • Настраиваемая логика компоновки : когда расположение элементов динамически изменяется в зависимости от положения прокрутки.
  • Уникальные визуальные эффекты : применение преобразований, масштабирования или других эффектов к элементам интерфейса при прокрутке.
  • Прямое управление : Требуется точный контроль над механикой прокрутки, выходящий за рамки возможностей verticalScroll или отложенной компоновки.

Создавайте пользовательские списки в виде колеса с помощью scrollableArea

В следующем примере показано, как использовать scrollableArea для создания пользовательского вертикального списка, в котором элементы уменьшаются по мере удаления от центра, создавая визуальный эффект «колеса». Такое преобразование, зависящее от прокрутки, идеально подходит для использования scrollableArea .

Рисунок 2. Настраиваемый вертикальный список с использованием scrollableArea .

@Composable
private fun ScrollableAreaSample() {
    // ...
    Layout(
        modifier =
            Modifier
                .size(150.dp)
                .scrollableArea(scrollState, Orientation.Vertical)
                .background(Color.LightGray),
        // ...
    ) { measurables, constraints ->
        // ...
        // Update the maximum scroll value to not scroll beyond limits and stop when scroll
        // reaches the end.
        scrollState.maxValue = (totalHeight - viewportHeight).coerceAtLeast(0)

        // Position the children within the layout.
        layout(constraints.maxWidth, viewportHeight) {
            // The current vertical scroll position, in pixels.
            val scrollY = scrollState.value
            val viewportCenterY = scrollY + viewportHeight / 2

            var placeableLayoutPositionY = 0
            placeables.forEach { placeable ->
                // This sample applies a scaling effect to items based on their distance
                // from the center, creating a wheel-like effect.
                // ...
                // Place the item horizontally centered with a layer transformation for
                // scaling to achieve wheel-like effect.
                placeable.placeRelativeWithLayer(
                    x = constraints.maxWidth / 2 - placeable.width / 2,
                    // Offset y by the scroll position to make placeable visible in the viewport.
                    y = placeableLayoutPositionY - scrollY,
                ) {
                    scaleX = scaleFactor
                    scaleY = scaleFactor
                }
                // Move to the next item's vertical position.
                placeableLayoutPositionY += placeable.height
            }
        }
    }
}
// ...

Прокручиваемый модификатор

Модификатор scrollable отличается от модификаторов scroll тем, что scrollable отслеживает жесты прокрутки и фиксирует изменения, но не смещает содержимое автоматически. Вместо этого эта функция передается пользователю через ScrollableState , который необходим для корректной работы этого модификатора.

При создании ScrollableState необходимо указать функцию consumeScrollDelta , которая будет вызываться на каждом шаге прокрутки (при вводе жеста, плавной прокрутке или перемещении курсора) с изменением расстояния в пикселях. Эта функция должна возвращать величину пройденного расстояния прокрутки, чтобы обеспечить корректное распространение события в случаях, когда имеются вложенные элементы с модификатором scrollable .

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

@Composable
private fun ScrollableSample() {
    // actual composable state
    var offset by remember { mutableFloatStateOf(0f) }
    Box(
        Modifier
            .size(150.dp)
            .scrollable(
                orientation = Orientation.Vertical,
                // Scrollable state: describes how to consume
                // scrolling delta and update offset
                state = rememberScrollableState { delta ->
                    offset += delta
                    delta
                }
            )
            .background(Color.LightGray),
        contentAlignment = Alignment.Center
    ) {
        Text(offset.toString())
    }
}

Элемент пользовательского интерфейса, определяющий нажатие пальца и отображающий числовое значение его местоположения.
Рисунок 3. Элемент пользовательского интерфейса, определяющий нажатие пальца и отображающий числовое значение его местоположения.
{% verbatim %} {% endverbatim %} {% verbatim %} {% endverbatim %}