Модификаторы прокрутки
Модификаторы 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
обеспечивает сохранение вертикального центрирования изображения при его масштабировании.
Результат
Предыдущий фрагмент приводит к эффекту масштабирования изображения при прокрутке:
Взаимодействие вложенной прокрутки
При попытке вложить прокручиваемые элементы 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
.
Дополнительные ресурсы
{% дословно %}Рекомендовано для вас
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- Понимать жесты
- Перенести
CoordinatorLayout
в Compose - Использование представлений в Compose
Модификаторы прокрутки
Модификаторы 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
обеспечивает сохранение вертикального центрирования изображения при его масштабировании.
Результат
Предыдущий фрагмент приводит к эффекту масштабирования изображения при прокрутке:
Взаимодействие вложенной прокрутки
При попытке вложить прокручиваемые элементы 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
.
Дополнительные ресурсы
{% дословно %}Рекомендовано для вас
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- Понимать жесты
- Перенести
CoordinatorLayout
в Compose - Использование представлений в Compose