Прокрутка

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

Модификаторы 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

дельта передана в dispatchRawDelta через scrollableArea *

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

Отрицательно

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

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

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

Отрицательно

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

Отрицательно

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

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

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

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

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

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

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

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

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

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

Создавайте собственные списки в виде колеса с помощью 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. Элемент пользовательского интерфейса, определяющий нажатие пальца и отображающий числовое значение местоположения пальца.
{% дословно %} {% endverbatim %} {% дословно %} {% endverbatim %}