Прокрутка

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

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

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

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

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

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

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

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

@Composable
private fun ScrollableSample() {
    // actual composable state
    var offset by remember { mutableStateOf(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())
    }
}

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

Вложенная прокрутка

Вложенная прокрутка — это система, в которой несколько компонентов прокрутки, содержащихся друг в друге, работают вместе, реагируя на один жест прокрутки и сообщая свои дельты прокрутки (изменения).

Система вложенной прокрутки позволяет координировать между компонентами, которые прокручиваются и иерархически связаны (чаще всего путем совместного использования одного и того же родителя). Эта система связывает контейнеры прокрутки и позволяет взаимодействовать с дельтами прокрутки, которые распространяются и совместно используются.

Compose предоставляет несколько способов обработки вложенной прокрутки между компонуемыми элементами. Типичным примером вложенной прокрутки является список внутри другого списка, а более сложным случаем является сворачивающаяся панель инструментов .

Автоматическая вложенная прокрутка

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

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

В следующем примере показаны элементы с примененным к ним модификатором verticalScroll внутри контейнера, к которому также применен модификатор verticalScroll .

@Composable
private fun AutomaticNestedScroll() {
    val gradient = Brush.verticalGradient(0f to Color.Gray, 1000f to Color.White)
    Box(
        modifier = Modifier
            .background(Color.LightGray)
            .verticalScroll(rememberScrollState())
            .padding(32.dp)
    ) {
        Column {
            repeat(6) {
                Box(
                    modifier = Modifier
                        .height(128.dp)
                        .verticalScroll(rememberScrollState())
                ) {
                    Text(
                        "Scroll here",
                        modifier = Modifier
                            .border(12.dp, Color.DarkGray)
                            .background(brush = gradient)
                            .padding(24.dp)
                            .height(150.dp)
                    )
                }
            }
        }
    }
}

Два вложенных вертикальных прокручиваемых элемента пользовательского интерфейса, реагирующих на жесты внутри и снаружи внутреннего элемента

Использование модификатора nestedScroll

Если вам нужно создать расширенную скоординированную прокрутку между несколькими элементами, модификатор nestedScroll дает вам больше гибкости, определяя вложенную иерархию прокрутки. Как упоминалось в предыдущем разделе, некоторые компоненты имеют встроенную поддержку вложенной прокрутки. Однако для компонуемых элементов, которые не прокручиваются автоматически, таких как Box или Column , дельты прокрутки на таких компонентах не будут распространяться во вложенной системе прокрутки, и дельты не достигнут NestedScrollConnection или родительского компонента. Чтобы решить эту проблему, вы можете использовать nestedScroll для предоставления такой поддержки другим компонентам, включая пользовательские компоненты.

Вложенный цикл прокрутки

Вложенный цикл прокрутки — это поток дельт прокрутки, которые отправляются вверх и вниз по иерархическому дереву через все компоненты (или узлы), являющиеся частью вложенной системы прокрутки, например, с помощью прокручиваемых компонентов и модификаторов или nestedScroll .

Фазы вложенного цикла прокрутки

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

Фазы вложенного цикла прокрутки

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

Фаза предварительной прокрутки - отправка вверх

Это дает вложенным родительским элементам прокрутки (компонуемым с использованием модификаторов nestedScroll или scrollable) возможность что-то сделать с дельтой до того, как сам узел сможет ее использовать.

Фаза предварительной прокрутки - всплывание вниз

В фазе потребления узла сам узел будет использовать любую дельту, которая не была использована его родителями. Это когда движение прокрутки фактически выполнено и видно.

фаза потребления узла

В течение этой фазы ребенок может выбрать, потреблять ли весь или часть оставшегося свитка. Все, что останется, будет отправлено обратно наверх для прохождения фазы после свитка.

Наконец, на этапе пост-прокрутки все, что сам узел не потребил, будет снова отправлено его предкам для потребления.

Фаза после прокрутки - отправка вверх

Фаза после прокрутки работает аналогично фазе до прокрутки, когда любой из родителей может решить, потреблять или нет.

Фаза после прокрутки - всплывание вниз

Аналогично прокрутке, когда жест перетаскивания завершается, намерение пользователя может быть преобразовано в скорость, которая используется для перетаскивания (прокрутки с использованием анимации) прокручиваемого контейнера. Перетаскивание также является частью вложенного цикла прокрутки, и скорости, генерируемые событием перетаскивания, проходят через похожие фазы: предперетаскивание, потребление узла и постперетаскивание. Обратите внимание, что анимация перетаскивания связана только с жестом касания и не будет запускаться другими событиями, такими как a11y или аппаратная прокрутка.

Участвуйте во вложенном цикле прокрутки

Участие в цикле означает перехват, потребление и сообщение о потреблении дельт по иерархии. Compose предоставляет набор инструментов для влияния на то, как работает вложенная система прокрутки и как напрямую взаимодействовать с ней, например, когда вам нужно что-то сделать с дельтами прокрутки до того, как прокручиваемый компонент даже начнет прокручиваться.

Если вложенный цикл прокрутки — это система, действующая на цепочке узлов, модификатор nestedScroll — это способ перехвата и вставки в эти изменения, а также влияния на данные (дельты прокрутки), которые распространяются в цепочке. Этот модификатор может быть размещен в любом месте иерархии, и он взаимодействует с экземплярами вложенных модификаторов прокрутки вверх по дереву, чтобы обмениваться информацией через этот канал. Строительными блоками этого модификатора являются NestedScrollConnection и NestedScrollDispatcher .

NestedScrollConnection предоставляет способ реагировать на фазы цикла вложенной прокрутки и влиять на систему вложенной прокрутки. Он состоит из четырех методов обратного вызова, каждый из которых представляет одну из фаз потребления: pre/post-scroll и pre/post-fling:

val nestedScrollConnection = object : NestedScrollConnection {
    override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
        println("Received onPreScroll callback.")
        return Offset.Zero
    }

    override fun onPostScroll(
        consumed: Offset,
        available: Offset,
        source: NestedScrollSource
    ): Offset {
        println("Received onPostScroll callback.")
        return Offset.Zero
    }
}

Каждый обратный вызов также дает информацию о распространяемой дельте: available дельта для этой конкретной фазы и consumed дельта, потребленная в предыдущих фазах. Если в какой-то момент вы захотите остановить распространение дельт вверх по иерархии, вы можете использовать вложенное соединение прокрутки, чтобы сделать это:

val disabledNestedScrollConnection = remember {
    object : NestedScrollConnection {
        override fun onPostScroll(
            consumed: Offset,
            available: Offset,
            source: NestedScrollSource
        ): Offset {
            return if (source == NestedScrollSource.SideEffect) {
                available
            } else {
                Offset.Zero
            }
        }
    }
}

Все обратные вызовы предоставляют информацию о типе NestedScrollSource .

NestedScrollDispatcher инициализирует вложенный цикл прокрутки. Использование диспетчера и вызов его методов запускает цикл. Прокручиваемые контейнеры имеют встроенный диспетчер, который отправляет дельты, захваченные во время жестов, в систему. По этой причине большинство вариантов использования настройки вложенной прокрутки включают использование NestedScrollConnection вместо диспетчера, чтобы реагировать на уже существующие дельты, а не отправлять новые. См. NestedScrollDispatcherSample для получения дополнительных сведений об использовании.

Изменение размера изображения при прокрутке

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

Изменение размера изображения в зависимости от положения прокрутки

Этот фрагмент демонстрирует изменение размера изображения в LazyColumn на основе вертикальной позиции прокрутки. Изображение уменьшается, когда пользователь прокручивает страницу вниз, и увеличивается, когда он прокручивает страницу вверх, оставаясь в пределах заданных минимальных и максимальных границ размера:

@Composable
fun ImageResizeOnScrollExample(
    modifier: Modifier = Modifier,
    maxImageSize: Dp = 300.dp,
    minImageSize: Dp = 100.dp
) {
    var currentImageSize by remember { mutableStateOf(maxImageSize) }
    var imageScale by remember { mutableFloatStateOf(1f) }

    val nestedScrollConnection = remember {
        object : NestedScrollConnection {
            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
                // Calculate the change in image size based on scroll delta
                val delta = available.y
                val newImageSize = currentImageSize + delta.dp
                val previousImageSize = currentImageSize

                // Constrain the image size within the allowed bounds
                currentImageSize = newImageSize.coerceIn(minImageSize, maxImageSize)
                val consumed = currentImageSize - previousImageSize

                // Calculate the scale for the image
                imageScale = currentImageSize / maxImageSize

                // Return the consumed scroll amount
                return Offset(0f, consumed.value)
            }
        }
    }

    Box(Modifier.nestedScroll(nestedScrollConnection)) {
        LazyColumn(
            Modifier
                .fillMaxWidth()
                .padding(15.dp)
                .offset {
                    IntOffset(0, currentImageSize.roundToPx())
                }
        ) {
            // Placeholder list items
            items(100, key = { it }) {
                Text(
                    text = "Item: $it",
                    style = MaterialTheme.typography.bodyLarge
                )
            }
        }

        Image(
            painter = ColorPainter(Color.Red),
            contentDescription = "Red color image",
            Modifier
                .size(maxImageSize)
                .align(Alignment.TopCenter)
                .graphicsLayer {
                    scaleX = imageScale
                    scaleY = imageScale
                    // Center the image vertically as it scales
                    translationY = -(maxImageSize.toPx() - currentImageSize.toPx()) / 2f
                }
        )
    }
}

Ключевые моменты кодекса

  • Этот код использует NestedScrollConnection для перехвата событий прокрутки.
  • onPreScroll вычисляет изменение размера изображения на основе дельты прокрутки.
  • Переменная состояния currentImageSize хранит текущий размер изображения, ограниченный minImageSize и maxImageSize. imageScale является производным от currentImageSize .
  • Смещение LazyColumn основано на currentImageSize .
  • Image использует модификатор graphicsLayer для применения рассчитанного масштаба.
  • translationY в graphicsLayer обеспечивает сохранение вертикального центрирования изображения при его масштабировании.

Результат

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

Рисунок 1. Эффект масштабирования изображения при прокрутке.

Взаимодействие вложенной прокрутки

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

Эта проблема является результатом ожиданий, заложенных в прокручиваемых компонуемых элементах. Прокручиваемые компонуемые элементы имеют правило «вложенной прокрутки по умолчанию», которое означает, что любой прокручиваемый контейнер должен участвовать во вложенной цепочке прокрутки, как в качестве родителя через NestedScrollConnection , так и в качестве дочернего элемента через NestedScrollDispatcher . Затем дочерний элемент будет управлять вложенной прокруткой для родителя, когда дочерний элемент находится на границе. Например, это правило позволяет Compose Pager и Compose LazyRow хорошо работать вместе. Однако, когда прокрутка взаимодействия выполняется с помощью ViewPager2 или RecyclerView , поскольку они не реализуют NestedScrollingParent3 , непрерывная прокрутка от дочернего элемента к родительскому невозможна.

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

Взаимодействующий родительский View содержащий дочерний ComposeView

Взаимодействующий родительский View — это тот, который уже реализует NestedScrollingParent3 и, следовательно, может получать дельты прокрутки от взаимодействующего вложенного дочернего компонуемого элемента. ComposeView будет действовать как дочерний элемент в этом случае и должен будет (косвенно) реализовать NestedScrollingChild3 . Одним из примеров взаимодействующего родителя является androidx.coordinatorlayout.widget.CoordinatorLayout .

Если вам требуется вложенная прокручиваемая совместимость между прокручиваемыми родительскими контейнерами View и вложенными прокручиваемыми дочерними компонуемыми объектами, вы можете использовать rememberNestedScrollInteropConnection() .

rememberNestedScrollInteropConnection() позволяет и запоминает NestedScrollConnection , который обеспечивает взаимодействие вложенной прокрутки между родительским View , реализующим NestedScrollingParent3 , и дочерним Compose. Это следует использовать вместе с модификатором nestedScroll . Поскольку вложенная прокрутка включена по умолчанию на стороне Compose, вы можете использовать это соединение для включения как вложенной прокрутки на стороне View , так и добавления необходимой связующей логики между Views и компонуемыми элементами.

Распространенным вариантом использования является использование CoordinatorLayout , CollapsingToolbarLayout и дочернего компонуемого элемента, как показано в этом примере:

<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:fitsSystemWindows="true">

        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <!--...-->

        </com.google.android.material.appbar.CollapsingToolbarLayout>

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

В вашей Activity или Fragment вам необходимо настроить дочерний компонуемый элемент и требуемый NestedScrollConnection :

open class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<ComposeView>(R.id.compose_view).apply {
            setContent {
                val nestedScrollInterop = rememberNestedScrollInteropConnection()
                // Add the nested scroll connection to your top level @Composable element
                // using the nestedScroll modifier.
                LazyColumn(modifier = Modifier.nestedScroll(nestedScrollInterop)) {
                    items(20) { item ->
                        Box(
                            modifier = Modifier
                                .padding(16.dp)
                                .height(56.dp)
                                .fillMaxWidth()
                                .background(Color.Gray),
                            contentAlignment = Alignment.Center
                        ) {
                            Text(item.toString())
                        }
                    }
                }
            }
        }
    }
}

Родительский компонуемый элемент, содержащий дочерний AndroidView

Этот сценарий охватывает реализацию API взаимодействия вложенной прокрутки на стороне Compose — когда у вас есть родительский компонуемый элемент, содержащий дочерний AndroidView . AndroidView реализует NestedScrollDispatcher , поскольку он действует как дочерний элемент для прокручиваемого родителя Compose, а также NestedScrollingParent3 , поскольку он действует как родитель для прокручиваемого дочернего элемента View . Затем родительский элемент Compose сможет получать дельты вложенной прокрутки от вложенного прокручиваемого дочернего элемента View .

В следующем примере показано, как можно реализовать взаимодействие вложенной прокрутки в этом сценарии, а также сворачивающуюся панель инструментов Compose:

@Composable
private fun NestedScrollInteropComposeParentWithAndroidChildExample() {
    val toolbarHeightPx = with(LocalDensity.current) { ToolbarHeight.roundToPx().toFloat() }
    val toolbarOffsetHeightPx = remember { mutableStateOf(0f) }

    // Sets up the nested scroll connection between the Box composable parent
    // and the child AndroidView containing the RecyclerView
    val nestedScrollConnection = remember {
        object : NestedScrollConnection {
            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
                // Updates the toolbar offset based on the scroll to enable
                // collapsible behaviour
                val delta = available.y
                val newOffset = toolbarOffsetHeightPx.value + delta
                toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f)
                return Offset.Zero
            }
        }
    }

    Box(
        Modifier
            .fillMaxSize()
            .nestedScroll(nestedScrollConnection)
    ) {
        TopAppBar(
            modifier = Modifier
                .height(ToolbarHeight)
                .offset { IntOffset(x = 0, y = toolbarOffsetHeightPx.value.roundToInt()) }
        )

        AndroidView(
            { context ->
                LayoutInflater.from(context)
                    .inflate(R.layout.view_in_compose_nested_scroll_interop, null).apply {
                        with(findViewById<RecyclerView>(R.id.main_list)) {
                            layoutManager = LinearLayoutManager(context, VERTICAL, false)
                            adapter = NestedScrollInteropAdapter()
                        }
                    }.also {
                        // Nested scrolling interop is enabled when
                        // nested scroll is enabled for the root View
                        ViewCompat.setNestedScrollingEnabled(it, true)
                    }
            },
            // ...
        )
    }
}

private class NestedScrollInteropAdapter :
    Adapter<NestedScrollInteropAdapter.NestedScrollInteropViewHolder>() {
    val items = (1..10).map { it.toString() }

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): NestedScrollInteropViewHolder {
        return NestedScrollInteropViewHolder(
            LayoutInflater.from(parent.context)
                .inflate(R.layout.list_item, parent, false)
        )
    }

    override fun onBindViewHolder(holder: NestedScrollInteropViewHolder, position: Int) {
        // ...
    }

    class NestedScrollInteropViewHolder(view: View) : ViewHolder(view) {
        fun bind(item: String) {
            // ...
        }
    }
    // ...
}

В этом примере показано, как можно использовать API с scrollable модификатором:

@Composable
fun ViewInComposeNestedScrollInteropExample() {
    Box(
        Modifier
            .fillMaxSize()
            .scrollable(rememberScrollableState {
                // View component deltas should be reflected in Compose
                // components that participate in nested scrolling
                it
            }, Orientation.Vertical)
    ) {
        AndroidView(
            { context ->
                LayoutInflater.from(context)
                    .inflate(android.R.layout.list_item, null)
                    .apply {
                        // Nested scrolling interop is enabled when
                        // nested scroll is enabled for the root View
                        ViewCompat.setNestedScrollingEnabled(this, true)
                    }
            }
        )
    }
}

И наконец, этот пример показывает, как вложенный API взаимодействия прокрутки используется с BottomSheetDialogFragment для достижения успешного поведения перетаскивания и закрытия:

class BottomSheetFragment : BottomSheetDialogFragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        val rootView: View = inflater.inflate(R.layout.fragment_bottom_sheet, container, false)

        rootView.findViewById<ComposeView>(R.id.compose_view).apply {
            setContent {
                val nestedScrollInterop = rememberNestedScrollInteropConnection()
                LazyColumn(
                    Modifier
                        .nestedScroll(nestedScrollInterop)
                        .fillMaxSize()
                ) {
                    item {
                        Text(text = "Bottom sheet title")
                    }
                    items(10) {
                        Text(
                            text = "List item number $it",
                            modifier = Modifier.fillMaxWidth()
                        )
                    }
                }
            }
            return rootView
        }
    }
}

Обратите внимание, что rememberNestedScrollInteropConnection() установит NestedScrollConnection в элементе, к которому вы его прикрепляете. NestedScrollConnection отвечает за передачу дельт с уровня Compose на уровень View . Это позволяет элементу участвовать во вложенной прокрутке, но не включает прокрутку элементов автоматически. Для компонуемых элементов, которые не прокручиваются автоматически, таких как Box или Column , дельты прокрутки на таких компонентах не будут распространяться во вложенной системе прокрутки, и дельты не достигнут NestedScrollConnection предоставляемого rememberNestedScrollInteropConnection() , поэтому эти дельты не достигнут родительского компонента View . Чтобы решить эту проблему, убедитесь, что вы также установили модификаторы scrollable для этих типов вложенных компонуемых элементов. Вы можете обратиться к предыдущему разделу о вложенной прокрутке для получения более подробной информации.

Несотрудничающий родительский View содержащий дочерний ComposeView

Несотрудничающий View — это тот, который не реализует необходимые интерфейсы NestedScrolling на стороне View . Обратите внимание, что это означает, что взаимодействие вложенной прокрутки с этими Views не работает из коробки. Несотрудничающие Views — это RecyclerView и ViewPager2 .

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

{% дословно %} {% endverbatim %} {% дословно %} {% endverbatim %} ,

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

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

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

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

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

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

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

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

@Composable
private fun ScrollableSample() {
    // actual composable state
    var offset by remember { mutableStateOf(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())
    }
}

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

Вложенная прокрутка

Вложенная прокрутка — это система, в которой несколько компонентов прокрутки, содержащихся друг в друге, работают вместе, реагируя на один жест прокрутки и сообщая свои дельты прокрутки (изменения).

Система вложенной прокрутки позволяет координировать между компонентами, которые прокручиваются и иерархически связаны (чаще всего путем совместного использования одного и того же родителя). Эта система связывает контейнеры прокрутки и позволяет взаимодействовать с дельтами прокрутки, которые распространяются и совместно используются.

Compose предоставляет несколько способов обработки вложенной прокрутки между компонуемыми элементами. Типичным примером вложенной прокрутки является список внутри другого списка, а более сложным случаем является сворачивающаяся панель инструментов .

Автоматическая вложенная прокрутка

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

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

В следующем примере показаны элементы с примененным к ним модификатором verticalScroll внутри контейнера, к которому также применен модификатор verticalScroll .

@Composable
private fun AutomaticNestedScroll() {
    val gradient = Brush.verticalGradient(0f to Color.Gray, 1000f to Color.White)
    Box(
        modifier = Modifier
            .background(Color.LightGray)
            .verticalScroll(rememberScrollState())
            .padding(32.dp)
    ) {
        Column {
            repeat(6) {
                Box(
                    modifier = Modifier
                        .height(128.dp)
                        .verticalScroll(rememberScrollState())
                ) {
                    Text(
                        "Scroll here",
                        modifier = Modifier
                            .border(12.dp, Color.DarkGray)
                            .background(brush = gradient)
                            .padding(24.dp)
                            .height(150.dp)
                    )
                }
            }
        }
    }
}

Два вложенных вертикальных прокручиваемых элемента пользовательского интерфейса, реагирующих на жесты внутри и снаружи внутреннего элемента

Использование модификатора nestedScroll

Если вам нужно создать расширенную скоординированную прокрутку между несколькими элементами, модификатор nestedScroll дает вам больше гибкости, определяя вложенную иерархию прокрутки. Как упоминалось в предыдущем разделе, некоторые компоненты имеют встроенную поддержку вложенной прокрутки. Однако для компонуемых элементов, которые не прокручиваются автоматически, таких как Box или Column , дельты прокрутки на таких компонентах не будут распространяться во вложенной системе прокрутки, и дельты не достигнут NestedScrollConnection или родительского компонента. Чтобы решить эту проблему, вы можете использовать nestedScroll для предоставления такой поддержки другим компонентам, включая пользовательские компоненты.

Вложенный цикл прокрутки

Вложенный цикл прокрутки — это поток дельт прокрутки, которые отправляются вверх и вниз по иерархическому дереву через все компоненты (или узлы), являющиеся частью вложенной системы прокрутки, например, с помощью прокручиваемых компонентов и модификаторов или nestedScroll .

Фазы вложенного цикла прокрутки

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

Фазы вложенного цикла прокрутки

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

Фаза предварительной прокрутки - отправка вверх

Это дает вложенным родительским элементам прокрутки (компонуемым с использованием модификаторов nestedScroll или scrollable) возможность что-то сделать с дельтой до того, как сам узел сможет ее использовать.

Фаза предварительной прокрутки - всплывание вниз

В фазе потребления узла сам узел будет использовать любую дельту, которая не была использована его родителями. Это когда движение прокрутки фактически выполнено и видно.

фаза потребления узла

В течение этой фазы ребенок может выбрать, потреблять ли весь или часть оставшегося свитка. Все, что останется, будет отправлено обратно наверх для прохождения фазы после свитка.

Наконец, на этапе пост-прокрутки все, что сам узел не потребил, будет снова отправлено его предкам для потребления.

Фаза после прокрутки - отправка вверх

Фаза после прокрутки работает аналогично фазе до прокрутки, когда любой из родителей может решить, потреблять или нет.

Фаза после прокрутки - всплывание вниз

Аналогично прокрутке, когда жест перетаскивания завершается, намерение пользователя может быть преобразовано в скорость, которая используется для перетаскивания (прокрутки с использованием анимации) прокручиваемого контейнера. Перетаскивание также является частью вложенного цикла прокрутки, и скорости, генерируемые событием перетаскивания, проходят через похожие фазы: предперетаскивание, потребление узла и постперетаскивание. Обратите внимание, что анимация перетаскивания связана только с жестом касания и не будет запускаться другими событиями, такими как a11y или аппаратная прокрутка.

Участвуйте во вложенном цикле прокрутки

Участие в цикле означает перехват, потребление и сообщение о потреблении дельт по иерархии. Compose предоставляет набор инструментов для влияния на то, как работает вложенная система прокрутки и как напрямую взаимодействовать с ней, например, когда вам нужно что-то сделать с дельтами прокрутки до того, как прокручиваемый компонент даже начнет прокручиваться.

Если вложенный цикл прокрутки — это система, действующая на цепочке узлов, модификатор nestedScroll — это способ перехвата и вставки в эти изменения, а также влияния на данные (дельты прокрутки), которые распространяются в цепочке. Этот модификатор может быть размещен в любом месте иерархии, и он взаимодействует с экземплярами вложенных модификаторов прокрутки вверх по дереву, чтобы обмениваться информацией через этот канал. Строительными блоками этого модификатора являются NestedScrollConnection и NestedScrollDispatcher .

NestedScrollConnection предоставляет способ реагировать на фазы цикла вложенной прокрутки и влиять на систему вложенной прокрутки. Он состоит из четырех методов обратного вызова, каждый из которых представляет одну из фаз потребления: pre/post-scroll и pre/post-fling:

val nestedScrollConnection = object : NestedScrollConnection {
    override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
        println("Received onPreScroll callback.")
        return Offset.Zero
    }

    override fun onPostScroll(
        consumed: Offset,
        available: Offset,
        source: NestedScrollSource
    ): Offset {
        println("Received onPostScroll callback.")
        return Offset.Zero
    }
}

Каждый обратный вызов также дает информацию о распространяемой дельте: available дельта для этой конкретной фазы и consumed дельта, потребленная в предыдущих фазах. Если в какой-то момент вы захотите остановить распространение дельт вверх по иерархии, вы можете использовать вложенное соединение прокрутки, чтобы сделать это:

val disabledNestedScrollConnection = remember {
    object : NestedScrollConnection {
        override fun onPostScroll(
            consumed: Offset,
            available: Offset,
            source: NestedScrollSource
        ): Offset {
            return if (source == NestedScrollSource.SideEffect) {
                available
            } else {
                Offset.Zero
            }
        }
    }
}

Все обратные вызовы предоставляют информацию о типе NestedScrollSource .

NestedScrollDispatcher инициализирует вложенный цикл прокрутки. Использование диспетчера и вызов его методов запускает цикл. Прокручиваемые контейнеры имеют встроенный диспетчер, который отправляет дельты, захваченные во время жестов, в систему. По этой причине большинство вариантов использования настройки вложенной прокрутки включают использование NestedScrollConnection вместо диспетчера, чтобы реагировать на уже существующие дельты, а не отправлять новые. См. NestedScrollDispatcherSample для получения дополнительных сведений об использовании.

Изменение размера изображения при прокрутке

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

Изменение размера изображения в зависимости от положения прокрутки

Этот фрагмент демонстрирует изменение размера изображения в LazyColumn на основе вертикальной позиции прокрутки. Изображение уменьшается, когда пользователь прокручивает страницу вниз, и увеличивается, когда он прокручивает страницу вверх, оставаясь в пределах заданных минимальных и максимальных границ размера:

@Composable
fun ImageResizeOnScrollExample(
    modifier: Modifier = Modifier,
    maxImageSize: Dp = 300.dp,
    minImageSize: Dp = 100.dp
) {
    var currentImageSize by remember { mutableStateOf(maxImageSize) }
    var imageScale by remember { mutableFloatStateOf(1f) }

    val nestedScrollConnection = remember {
        object : NestedScrollConnection {
            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
                // Calculate the change in image size based on scroll delta
                val delta = available.y
                val newImageSize = currentImageSize + delta.dp
                val previousImageSize = currentImageSize

                // Constrain the image size within the allowed bounds
                currentImageSize = newImageSize.coerceIn(minImageSize, maxImageSize)
                val consumed = currentImageSize - previousImageSize

                // Calculate the scale for the image
                imageScale = currentImageSize / maxImageSize

                // Return the consumed scroll amount
                return Offset(0f, consumed.value)
            }
        }
    }

    Box(Modifier.nestedScroll(nestedScrollConnection)) {
        LazyColumn(
            Modifier
                .fillMaxWidth()
                .padding(15.dp)
                .offset {
                    IntOffset(0, currentImageSize.roundToPx())
                }
        ) {
            // Placeholder list items
            items(100, key = { it }) {
                Text(
                    text = "Item: $it",
                    style = MaterialTheme.typography.bodyLarge
                )
            }
        }

        Image(
            painter = ColorPainter(Color.Red),
            contentDescription = "Red color image",
            Modifier
                .size(maxImageSize)
                .align(Alignment.TopCenter)
                .graphicsLayer {
                    scaleX = imageScale
                    scaleY = imageScale
                    // Center the image vertically as it scales
                    translationY = -(maxImageSize.toPx() - currentImageSize.toPx()) / 2f
                }
        )
    }
}

Ключевые моменты кодекса

  • Этот код использует NestedScrollConnection для перехвата событий прокрутки.
  • onPreScroll вычисляет изменение размера изображения на основе дельты прокрутки.
  • Переменная состояния currentImageSize хранит текущий размер изображения, ограниченный minImageSize и maxImageSize. imageScale является производным от currentImageSize .
  • Смещение LazyColumn основано на currentImageSize .
  • Image использует модификатор graphicsLayer для применения рассчитанного масштаба.
  • translationY в graphicsLayer обеспечивает сохранение вертикального центрирования изображения при его масштабировании.

Результат

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

Рисунок 1. Эффект масштабирования изображения при прокрутке.

Взаимодействие вложенной прокрутки

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

Эта проблема является результатом ожиданий, заложенных в прокручиваемых компонуемых элементах. Прокручиваемые компонуемые элементы имеют правило «вложенной прокрутки по умолчанию», которое означает, что любой прокручиваемый контейнер должен участвовать во вложенной цепочке прокрутки, как в качестве родителя через NestedScrollConnection , так и в качестве дочернего элемента через NestedScrollDispatcher . Затем дочерний элемент будет управлять вложенной прокруткой для родителя, когда дочерний элемент находится на границе. Например, это правило позволяет Compose Pager и Compose LazyRow хорошо работать вместе. Однако, когда прокрутка взаимодействия выполняется с помощью ViewPager2 или RecyclerView , поскольку они не реализуют NestedScrollingParent3 , непрерывная прокрутка от дочернего элемента к родительскому невозможна.

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

Взаимодействующий родительский View содержащий дочерний ComposeView

Взаимодействующий родительский View — это тот, который уже реализует NestedScrollingParent3 и, следовательно, может получать дельты прокрутки от взаимодействующего вложенного дочернего компонуемого элемента. ComposeView будет действовать как дочерний элемент в этом случае и должен будет (косвенно) реализовать NestedScrollingChild3 . Одним из примеров взаимодействующего родителя является androidx.coordinatorlayout.widget.CoordinatorLayout .

Если вам требуется вложенная прокручиваемая совместимость между прокручиваемыми родительскими контейнерами View и вложенными прокручиваемыми дочерними компонуемыми объектами, вы можете использовать rememberNestedScrollInteropConnection() .

rememberNestedScrollInteropConnection() позволяет и запоминает NestedScrollConnection , который обеспечивает взаимодействие вложенной прокрутки между родительским View , реализующим NestedScrollingParent3 , и дочерним Compose. Это следует использовать вместе с модификатором nestedScroll . Поскольку вложенная прокрутка включена по умолчанию на стороне Compose, вы можете использовать это соединение для включения как вложенной прокрутки на стороне View , так и добавления необходимой связующей логики между Views и компонуемыми элементами.

Распространенным вариантом использования является использование CoordinatorLayout , CollapsingToolbarLayout и дочернего компонуемого элемента, как показано в этом примере:

<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:fitsSystemWindows="true">

        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <!--...-->

        </com.google.android.material.appbar.CollapsingToolbarLayout>

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

В вашей Activity или Fragment вам необходимо настроить дочерний компонуемый элемент и требуемый NestedScrollConnection :

open class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<ComposeView>(R.id.compose_view).apply {
            setContent {
                val nestedScrollInterop = rememberNestedScrollInteropConnection()
                // Add the nested scroll connection to your top level @Composable element
                // using the nestedScroll modifier.
                LazyColumn(modifier = Modifier.nestedScroll(nestedScrollInterop)) {
                    items(20) { item ->
                        Box(
                            modifier = Modifier
                                .padding(16.dp)
                                .height(56.dp)
                                .fillMaxWidth()
                                .background(Color.Gray),
                            contentAlignment = Alignment.Center
                        ) {
                            Text(item.toString())
                        }
                    }
                }
            }
        }
    }
}

Родительский компонуемый элемент, содержащий дочерний AndroidView

Этот сценарий охватывает реализацию API взаимодействия вложенной прокрутки на стороне Compose — когда у вас есть родительский компонуемый элемент, содержащий дочерний AndroidView . AndroidView реализует NestedScrollDispatcher , поскольку он действует как дочерний элемент для прокручиваемого родителя Compose, а также NestedScrollingParent3 , поскольку он действует как родитель для прокручиваемого дочернего элемента View . Затем родительский элемент Compose сможет получать дельты вложенной прокрутки от вложенного прокручиваемого дочернего элемента View .

В следующем примере показано, как можно реализовать взаимодействие вложенной прокрутки в этом сценарии, а также сворачивающуюся панель инструментов Compose:

@Composable
private fun NestedScrollInteropComposeParentWithAndroidChildExample() {
    val toolbarHeightPx = with(LocalDensity.current) { ToolbarHeight.roundToPx().toFloat() }
    val toolbarOffsetHeightPx = remember { mutableStateOf(0f) }

    // Sets up the nested scroll connection between the Box composable parent
    // and the child AndroidView containing the RecyclerView
    val nestedScrollConnection = remember {
        object : NestedScrollConnection {
            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
                // Updates the toolbar offset based on the scroll to enable
                // collapsible behaviour
                val delta = available.y
                val newOffset = toolbarOffsetHeightPx.value + delta
                toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f)
                return Offset.Zero
            }
        }
    }

    Box(
        Modifier
            .fillMaxSize()
            .nestedScroll(nestedScrollConnection)
    ) {
        TopAppBar(
            modifier = Modifier
                .height(ToolbarHeight)
                .offset { IntOffset(x = 0, y = toolbarOffsetHeightPx.value.roundToInt()) }
        )

        AndroidView(
            { context ->
                LayoutInflater.from(context)
                    .inflate(R.layout.view_in_compose_nested_scroll_interop, null).apply {
                        with(findViewById<RecyclerView>(R.id.main_list)) {
                            layoutManager = LinearLayoutManager(context, VERTICAL, false)
                            adapter = NestedScrollInteropAdapter()
                        }
                    }.also {
                        // Nested scrolling interop is enabled when
                        // nested scroll is enabled for the root View
                        ViewCompat.setNestedScrollingEnabled(it, true)
                    }
            },
            // ...
        )
    }
}

private class NestedScrollInteropAdapter :
    Adapter<NestedScrollInteropAdapter.NestedScrollInteropViewHolder>() {
    val items = (1..10).map { it.toString() }

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): NestedScrollInteropViewHolder {
        return NestedScrollInteropViewHolder(
            LayoutInflater.from(parent.context)
                .inflate(R.layout.list_item, parent, false)
        )
    }

    override fun onBindViewHolder(holder: NestedScrollInteropViewHolder, position: Int) {
        // ...
    }

    class NestedScrollInteropViewHolder(view: View) : ViewHolder(view) {
        fun bind(item: String) {
            // ...
        }
    }
    // ...
}

В этом примере показано, как можно использовать API с scrollable модификатором:

@Composable
fun ViewInComposeNestedScrollInteropExample() {
    Box(
        Modifier
            .fillMaxSize()
            .scrollable(rememberScrollableState {
                // View component deltas should be reflected in Compose
                // components that participate in nested scrolling
                it
            }, Orientation.Vertical)
    ) {
        AndroidView(
            { context ->
                LayoutInflater.from(context)
                    .inflate(android.R.layout.list_item, null)
                    .apply {
                        // Nested scrolling interop is enabled when
                        // nested scroll is enabled for the root View
                        ViewCompat.setNestedScrollingEnabled(this, true)
                    }
            }
        )
    }
}

И наконец, этот пример показывает, как вложенный API взаимодействия прокрутки используется с BottomSheetDialogFragment для достижения успешного поведения перетаскивания и закрытия:

class BottomSheetFragment : BottomSheetDialogFragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        val rootView: View = inflater.inflate(R.layout.fragment_bottom_sheet, container, false)

        rootView.findViewById<ComposeView>(R.id.compose_view).apply {
            setContent {
                val nestedScrollInterop = rememberNestedScrollInteropConnection()
                LazyColumn(
                    Modifier
                        .nestedScroll(nestedScrollInterop)
                        .fillMaxSize()
                ) {
                    item {
                        Text(text = "Bottom sheet title")
                    }
                    items(10) {
                        Text(
                            text = "List item number $it",
                            modifier = Modifier.fillMaxWidth()
                        )
                    }
                }
            }
            return rootView
        }
    }
}

Обратите внимание, что rememberNestedScrollInteropConnection() установит NestedScrollConnection в элементе, к которому вы его прикрепляете. NestedScrollConnection отвечает за передачу дельт с уровня Compose на уровень View . Это позволяет элементу участвовать во вложенной прокрутке, но не включает прокрутку элементов автоматически. Для компонуемых элементов, которые не прокручиваются автоматически, таких как Box или Column , дельты прокрутки на таких компонентах не будут распространяться во вложенной системе прокрутки, и дельты не достигнут NestedScrollConnection предоставляемого rememberNestedScrollInteropConnection() , поэтому эти дельты не достигнут родительского компонента View . Чтобы решить эту проблему, убедитесь, что вы также установили модификаторы scrollable для этих типов вложенных компонуемых элементов. Вы можете обратиться к предыдущему разделу о вложенной прокрутке для получения более подробной информации.

Несотрудничающий родительский View содержащий дочерний ComposeView

Несотрудничающий View — это тот, который не реализует необходимые интерфейсы NestedScrolling на стороне View . Обратите внимание, что это означает, что взаимодействие вложенной прокрутки с этими Views не работает из коробки. Несотрудничающие Views — это RecyclerView и ViewPager2 .

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

{% дословно %} {% endverbatim %} {% дословно %} {% endverbatim %}