Beim verschachtelten Scrollen arbeiten mehrere ineinander verschachtelte scrollbare Komponenten zusammen, indem sie auf eine einzelne Scroll-Geste reagieren und ihre Scroll-Deltas (Änderungen) kommunizieren.
Das System für verschachteltes Scrollen ermöglicht die Koordination zwischen scrollbaren und hierarchisch verknüpften Komponenten (meistens durch gemeinsame übergeordnete Elemente). Dieses System verknüpft scrollbare Container und ermöglicht die Interaktion mit den Scroll-Deltas, die zwischen ihnen weitergegeben und geteilt werden.
Compose bietet mehrere Möglichkeiten, verschachteltes Scrollen zwischen Composables zu verarbeiten. Ein typisches Beispiel für verschachteltes Scrollen ist eine Liste in einer anderen Liste. Ein komplexeres Beispiel ist eine einklappbare Symbolleiste.
Automatisches verschachteltes Scrollen
Für einfaches verschachteltes Scrollen ist keine Aktion erforderlich. Gesten, die eine Scroll-Aktion auslösen, werden automatisch von untergeordneten an übergeordnete Elemente weitergegeben. Wenn das untergeordnete Element nicht weiter scrollen kann, wird die Geste vom übergeordneten Element verarbeitet.
Automatisches verschachteltes Scrollen wird von einigen Komponenten und Modifikatoren von
Compose unterstützt und ist standardmäßig aktiviert:
verticalScroll,
horizontalScroll,
scrollable,
Lazy APIs und TextField. Wenn der Nutzer also ein untergeordnetes Element verschachtelter Komponenten scrollt, geben die vorherigen Modifikatoren die Scroll-Deltas an die übergeordneten Elemente weiter, die verschachteltes Scrollen unterstützen.
Im folgenden Beispiel sind Elemente mit einem
verticalScroll
-Modifikator in einem Container zu sehen, auf den ebenfalls ein verticalScroll
-Modifikator angewendet wurde.
@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) ) } } } } }
Modifikator nestedScroll verwenden
Wenn Sie einen erweiterten koordinierten Scroll zwischen mehreren Elementen erstellen müssen,
der
nestedScroll
Modifikator bietet mehr Flexibilität, da er eine Hierarchie für verschachteltes Scrollen definiert. Wie im vorherigen Abschnitt erwähnt, unterstützen einige Komponenten verschachteltes Scrollen. Bei Composables, die nicht automatisch scrollbar sind, z. B. Box oder Column, werden Scroll-Deltas für solche Komponenten jedoch nicht im System für verschachteltes Scrollen weitergegeben und erreichen weder NestedScrollConnection noch die übergeordnete Komponente. Um dieses Problem zu beheben, können Sie nestedScroll verwenden, um diese Unterstützung auf andere Komponenten zu übertragen, einschließlich benutzerdefinierter Komponenten.
Zyklus für verschachteltes Scrollen
Der Zyklus für verschachteltes Scrollen ist der Fluss von Scroll-Deltas, die über alle Komponenten (oder Knoten) hinweg, die Teil des Systems für verschachteltes Scrollen sind, im Hierarchiebaum nach oben und unten weitergegeben werden. Dazu können beispielsweise scrollbare Komponenten und Modifikatoren oder nestedScroll verwendet werden.
Phasen des Zyklus für verschachteltes Scrollen
Wenn ein Triggerereignis (z. B. eine Geste) von einer scrollbaren Komponente erkannt wird, werden die generierten Deltas an das System für verschachteltes Scrollen gesendet und durchlaufen drei Phasen: Pre-Scroll, Knotennutzung und Post-Scroll.
In der ersten Phase, der Pre-Scroll-Phase, gibt die Komponente, die die Deltas des Triggerereignisses empfangen hat, diese Ereignisse im Hierarchiebaum nach oben an das oberste übergeordnete Element weiter. Die Delta-Ereignisse werden dann nach unten weitergegeben, d. h. Deltas werden vom obersten übergeordneten Element nach unten zum untergeordneten Element weitergegeben, das den Zyklus für verschachteltes Scrollen gestartet hat.
So können die übergeordneten Elemente für verschachteltes Scrollen (Composables mit nestedScroll oder scrollbaren Modifikatoren) etwas mit dem Delta tun, bevor es vom Knoten selbst genutzt werden kann.
In der Phase der Knotennutzung verwendet der Knoten selbst alle Deltas, die nicht von den übergeordneten Elementen verwendet wurden. Hier wird die Scrollbewegung tatsächlich ausgeführt und ist sichtbar.
In dieser Phase kann das untergeordnete Element den gesamten oder einen Teil des verbleibenden Scrolls nutzen. Alles, was übrig bleibt, wird wieder nach oben gesendet, um die Post-Scroll-Phase zu durchlaufen.
In der Post-Scroll-Phase wird alles, was der Knoten selbst nicht genutzt hat, wieder an seine Ancestors zur Nutzung weitergegeben.
Die Post-Scroll-Phase funktioniert ähnlich wie die Pre-Scroll-Phase. Alle übergeordneten Elemente können wählen, ob sie das Delta nutzen oder nicht.
Ähnlich wie beim Scrollen kann die Absicht des Nutzers nach Abschluss einer Drag-Geste in eine Geschwindigkeit umgewandelt werden, mit der der scrollbare Container mit einer Animation gescrollt wird. Das Fling ist auch Teil des Zyklus für verschachteltes Scrollen. Die durch das Drag-Ereignis generierten Geschwindigkeiten durchlaufen ähnliche Phasen: Pre-Fling, Knotennutzung und Post-Fling. Die Fling-Animation ist nur mit der Touch-Geste verknüpft und wird nicht durch andere Ereignisse wie A11y oder Hardware-Scroll ausgelöst.
Am Zyklus für verschachteltes Scrollen teilnehmen
Die Teilnahme am Zyklus bedeutet, die Nutzung von Deltas entlang der Hierarchie abzufangen, zu nutzen und zu melden. Compose bietet eine Reihe von Tools, mit denen Sie die Funktionsweise des Systems für verschachteltes Scrollen beeinflussen und direkt damit interagieren können, z. B. wenn Sie etwas mit den Scroll-Deltas tun müssen, bevor eine scrollbare Komponente überhaupt mit dem Scrollen beginnt.
Wenn der Zyklus für verschachteltes Scrollen ein System ist, das auf eine Kette von Knoten wirkt, ist der
nestedScroll
Modifikator eine Möglichkeit, diese Änderungen abzufangen und einzufügen und die Daten (Scroll-Deltas) zu beeinflussen, die in der Kette weitergegeben werden. Dieser Modifikator kann an einer beliebigen Stelle in der Hierarchie platziert werden und kommuniziert mit Instanzen des Modifikators für verschachteltes Scrollen im Baum, sodass Informationen über diesen Kanal geteilt werden können. Die Bausteine dieses Modifikators sind NestedScrollConnection und NestedScrollDispatcher.
NestedScrollConnection
bietet eine Möglichkeit, auf die Phasen des Zyklus für verschachteltes Scrollen zu reagieren und das System für verschachteltes Scrollen zu beeinflussen. Es besteht aus vier Callback-Methoden, die jeweils eine der Nutzungsphasen darstellen: Pre-Scroll, Post-Scroll, Pre-Fling und 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 } }
Jeder Callback enthält auch Informationen zum weitergegebenen Delta: das available-Delta für diese bestimmte Phase und das consumed-Delta, das in den vorherigen Phasen genutzt wurde. Wenn Sie die Weitergabe von Deltas in der Hierarchie nach oben beenden möchten, können Sie dazu die Verbindung für verschachteltes Scrollen verwenden:
val disabledNestedScrollConnection = remember { object : NestedScrollConnection { override fun onPostScroll( consumed: Offset, available: Offset, source: NestedScrollSource ): Offset { return if (source == NestedScrollSource.SideEffect) { available } else { Offset.Zero } } } }
Alle Callbacks enthalten Informationen zum
NestedScrollSource
Typ.
NestedScrollDispatcher
initialisiert den Zyklus für verschachteltes Scrollen. Wenn Sie einen Dispatcher verwenden und seine Methoden aufrufen, wird der Zyklus ausgelöst. Scrollbare Container haben einen integrierten Dispatcher, der Deltas, die während Gesten erfasst wurden, an das System sendet. Aus diesem Grund wird in den meisten Anwendungsfällen zum Anpassen des verschachtelten Scrollens NestedScrollConnection anstelle eines Dispatchers verwendet, um auf bereits vorhandene Deltas zu reagieren, anstatt neue zu senden.
Weitere Informationen finden Sie unter
NestedScrollDispatcherSample.
Bild beim Scrollen verkleinern
Wenn der Nutzer scrollt, können Sie einen dynamischen visuellen Effekt erstellen, bei dem sich die Größe des Bildes je nach Scrollposition ändert.
Bildgröße basierend auf der Scrollposition ändern
In diesem Snippet wird gezeigt, wie Sie die Größe eines Bildes in einer LazyColumn basierend auf
vertikalen Scrollposition ändern. Das Bild wird kleiner, wenn der Nutzer nach unten scrollt, und größer, wenn er nach oben scrollt. Dabei bleibt es innerhalb der definierten Mindest- und Höchstgröße:
@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 } ) } }
Wichtige Informationen zum Code
- In diesem Code wird eine
NestedScrollConnectionverwendet, um Scroll-Ereignisse abzufangen. onPreScrollberechnet die Änderung der Bildgröße basierend auf dem Scroll-Delta.- Die Statusvariable
currentImageSizespeichert die aktuelle Größe des Bildes, die zwischenminImageSizeundmaxImageSize. imageScaleliegt und von dercurrentImageSizeabgeleitet wird. - Die
LazyColumnwerden basierend auf dercurrentImageSizeverschoben. - Für das
Imagewird eingraphicsLayer-Modifikator verwendet, um die berechnete Skalierung anzuwenden. - Mit
translationYingraphicsLayerwird dafür gesorgt, dass das Bild beim Skalieren vertikal zentriert bleibt.
Ergebnis
Das vorherige Snippet führt zu einem Skalierungseffekt für das Bild beim Scrollen:
Interoperabilität für verschachteltes Scrollen
Wenn Sie versuchen, scrollbare View-Elemente in scrollbaren Composables zu verschachteln oder umgekehrt, können Probleme auftreten. Die auffälligsten Probleme treten auf, wenn Sie das untergeordnete Element scrollen und die Start- oder Endgrenzen erreichen und erwarten, dass das übergeordnete Element das Scrollen übernimmt. Dieses erwartete Verhalten tritt jedoch möglicherweise nicht oder nicht wie erwartet auf.
Dieses Problem ist auf die Erwartungen zurückzuführen, die in scrollbaren Composables enthalten sind.
Für scrollbare Composables gilt die Regel „Verschachteltes Scrollen standardmäßig“, d. h.
jeder scrollbare Container muss am Zyklus für verschachteltes Scrollen teilnehmen, sowohl als
übergeordnetes Element über
NestedScrollConnection,
als auch als untergeordnetes Element über
NestedScrollDispatcher.
Das untergeordnete Element würde dann einen verschachtelten Scroll für das übergeordnete Element auslösen, wenn es die Grenze erreicht. Mit dieser Regel können beispielsweise Pager und LazyRow von Compose gut zusammenarbeiten. Wenn jedoch Interoperabilitäts-Scrollen erfolgt
mit ViewPager2 oder RecyclerView, ist das kontinuierliche Scrollen vom untergeordneten zum übergeordneten Element nicht möglich, da diese
NestedScrollingParent3
nicht implementieren.
Wenn Sie die Interoperabilitäts-API für verschachteltes Scrollen zwischen scrollbaren View-Elementen und scrollbaren Composables aktivieren möchten, die in beiden Richtungen verschachtelt sind, können Sie die Interoperabilitäts-API für verschachteltes Scrollen verwenden, um diese Probleme in den folgenden Szenarien zu beheben.
Ein kooperierendes übergeordnetes View-Element mit einem untergeordneten ComposeView-Element
Ein kooperierendes übergeordnetes View implementiert bereits
NestedScrollingParent3
und kann daher Scroll-Deltas von einem kooperierenden verschachtelten
untergeordneten Composable empfangen. ComposeView fungiert in diesem Fall als untergeordnetes Element und muss
NestedScrollingChild3 (indirekt) implementieren.
Ein Beispiel für ein kooperierendes übergeordnetes Element ist androidx.coordinatorlayout.widget.CoordinatorLayout.
Wenn Sie die Interoperabilität für verschachteltes Scrollen zwischen scrollbaren View übergeordneten
Containern und verschachtelten scrollbaren untergeordneten Composables benötigen, können Sie
rememberNestedScrollInteropConnection() verwenden.
rememberNestedScrollInteropConnection()
ermöglicht und speichert die
NestedScrollConnection
, die die Interoperabilität für verschachteltes Scrollen zwischen einem übergeordneten View-Element, das
implementiert,
NestedScrollingParent3
und einem untergeordneten Compose-Element ermöglicht. Dies sollte in Verbindung mit einem
nestedScroll
Modifikator verwendet werden. Da verschachteltes Scrollen auf der Compose-Seite standardmäßig aktiviert ist, können Sie
diese Verbindung verwenden, um verschachteltes Scrollen auf der View Seite zu aktivieren und die erforderliche Glue-Logik zwischen Views und Composables hinzuzufügen.
Ein häufiger Anwendungsfall ist die Verwendung von CoordinatorLayout, CollapsingToolbarLayout und einem untergeordneten Composable, wie in diesem Beispiel gezeigt:
<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>
In Ihrer Aktivität oder Ihrem Fragment müssen Sie das untergeordnete Composable und die
erforderliche
NestedScrollConnection einrichten:
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()) } } } } } } }
Ein übergeordnetes Composable mit einem untergeordneten AndroidView-Element
Dieses Szenario behandelt die Implementierung der Interoperabilitäts-API für verschachteltes Scrollen auf der Compose-Seite, wenn Sie ein übergeordnetes Composable mit einem untergeordneten AndroidView-Element haben. Das AndroidView implementiert
NestedScrollDispatcher,
da es als untergeordnetes Element eines scrollbaren übergeordneten Compose-Elements fungiert, sowie
NestedScrollingParent3
, da es als übergeordnetes Element eines scrollbaren untergeordneten View-Elements fungiert. Das übergeordnete Compose-Element kann dann verschachtelte Scroll-Deltas von einem verschachtelten scrollbaren untergeordneten View-Element empfangen.
Im folgenden Beispiel wird gezeigt, wie Sie in diesem Szenario die Interoperabilität für verschachteltes Scrollen zusammen mit einer einklappbaren Symbolleiste von Compose erreichen können:
@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) {
// ...
}
}
// ...
}
In diesem Beispiel wird gezeigt, wie Sie die API mit einem scrollable-Modifikator verwenden können:
@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)
}
}
)
}
}
Und schließlich zeigt dieses Beispiel, wie die Interoperabilitäts-API für verschachteltes Scrollen mit
BottomSheetDialogFragment
verwendet wird, um ein erfolgreiches Drag-and-Dismiss-Verhalten zu erzielen:
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
}
}
}
Beachten Sie, dass
rememberNestedScrollInteropConnection()
eine
NestedScrollConnection
in dem Element installiert, an das Sie sie anhängen. NestedScrollConnection ist für die Übertragung der Deltas von der Compose-Ebene zur View-Ebene verantwortlich. So kann das Element am verschachtelten Scrollen teilnehmen, aber das Scrollen von Elementen wird nicht automatisch aktiviert. Bei Composables, die nicht automatisch scrollbar sind, z. B. Box oder Column, werden Scroll-Deltas für solche Komponenten nicht im System für verschachteltes Scrollen weitergegeben und erreichen nicht die NestedScrollConnection, die von rememberNestedScrollInteropConnection() bereitgestellt wird. Daher erreichen diese Deltas nicht die übergeordnete View-Komponente. Um dieses Problem zu beheben, müssen Sie auch scrollbare Modifikatoren für diese Arten von verschachtelten Composables festlegen. Weitere Informationen finden Sie im vorherigen Abschnitt zum verschachtelten
Scrollen für weitere
Informationen.
Ein nicht kooperierendes übergeordnetes View-Element mit einem untergeordneten ComposeView-Element
Ein nicht kooperierendes View-Element implementiert die erforderlichen NestedScrolling-Schnittstellen nicht auf der View-Seite. Das bedeutet, dass die Interoperabilität für verschachteltes Scrollen mit diesen Views nicht standardmäßig funktioniert. Nicht kooperierende Views sind RecyclerView und ViewPager2.
Zusätzliche Ressourcen
Empfehlungen für Sie
- Hinweis: Linktext wird angezeigt, wenn JavaScript deaktiviert ist
- Gesten
CoordinatorLayoutzu Compose migrieren- Views in Compose verwenden