Прокрутка, Прокрутка

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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 .

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

Когда вы пытаетесь вложить прокручиваемые элементы 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 . Поскольку вложенная прокрутка включена по умолчанию на стороне создания, вы можете использовать это соединение, чтобы включить как вложенную прокрутку на стороне 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>

В вашей деятельности или фрагменте вам необходимо настроить дочернюю компоновку и необходимый NestedScrollConnection :

open class MainActivity : ComponentActivity() {
    @OptIn(ExperimentalComposeUiApi::class)
    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 . Чтобы решить эту проблему, убедитесь, что вы также установили модификаторы с возможностью прокрутки для этих типов вложенных компонуемых объектов. Вы можете обратиться к предыдущему разделу о вложенной прокрутке для получения более подробной информации.

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

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

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