Модификаторы прокрутки
Модификаторы 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() { 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
.
Рекомендуется для вас
- Примечание. Текст ссылки отображается, когда JavaScript отключен.
- Понимание жестов
- Перенос
CoordinatorLayout
в Compose - Использование представлений в Compose