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

Модификаторы 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 или composable-функции, например LazyColumn. Эти компоненты более высокого уровня проще для распространенных вариантов использования и сами создаются с помощью scrollableArea.

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

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

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

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

В таблице ниже приведены знаки дельты для распространенных сценариев.

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

разница, о которой scrollable сообщил dispatchRawDelta

delta reported to dispatchRawDelta by scrollableArea*

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

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

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

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

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

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

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

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

Положительное (отрицательное для языков с письмом справа налево)

Указатель перемещается ВПРАВО.

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

Отрицательное (положительное для языков с письмом справа налево)

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

  1. Ориентация. Вертикальная или горизонтальная.
  2. LayoutDirection – направление письма (слева направо или справа налево). Это особенно важно для горизонтальной прокрутки.
  3. reverseScrolling flag – инвертировано ли направление прокрутки.

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

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

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

  • Специальная логика макета. Расположение элементов динамически меняется в зависимости от положения прокрутки.
  • Уникальные визуальные эффекты. Применение преобразований, масштабирования или других эффектов к дочерним элементам при прокрутке.
  • Прямое управление. Вам нужен точный контроль над механикой прокрутки, выходящий за рамки того, что предоставляют 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

Модификатор scrollable отличается от модификаторов прокрутки тем, что 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. Элемент интерфейса, который определяет нажатие пальцем и показывает числовое значение для его местоположения.