Verschachteltes Scrollen (Nested Scrolling) ist ein System, bei dem mehrere ineinander verschachtelte scrollbare Komponenten zusammenarbeiten, indem sie auf eine einzelne Scroll-Geste reagieren und ihre Scroll-Deltas (Änderungen) untereinander kommunizieren.
Das System für verschachteltes Scrollen ermöglicht die Koordination zwischen Komponenten, die scrollbar und hierarchisch verknüpft sind (meistens durch die gemeinsame Nutzung desselben übergeordneten Elements). Dieses System verknüpft Scrollcontainer 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 komplexerer Fall ist eine minimierbare Symbolleiste.
Automatisches verschachteltes Scrollen
Für einfaches verschachteltes Scrollen ist keine Aktion Ihrerseits erforderlich. Gesten, die eine Scrollaktion initiieren, 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 Compose-Komponenten und ‑Modifikatoren standardmäßig unterstützt:
verticalScroll,
horizontalScroll,
scrollable,
Lazy-APIs und TextField. Das bedeutet, dass beim Scrollen eines inneren untergeordneten Elements verschachtelter Komponenten die vorherigen Modifikatoren die Scroll-Deltas an die übergeordneten Elemente weitergeben, die verschachteltes Scrollen unterstützen.
Das folgende Beispiel zeigt Elemente mit einem verticalScroll-Modifikator in einem Container, 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 ein erweitertes koordiniertes Scrollen zwischen mehreren Elementen erstellen möchten, bietet der Modifikator nestedScroll mehr Flexibilität, da Sie eine verschachtelte Scrollhierarchie definieren können. Wie im vorherigen Abschnitt erwähnt, unterstützen einige Komponenten das verschachtelte Scrollen. Bei nicht automatisch scrollbaren Composables wie Box oder Column werden Scroll-Deltas jedoch nicht im verschachtelten Scrollsystem weitergegeben und erreichen weder NestedScrollConnection noch die übergeordnete Komponente. Um dieses Problem zu beheben, können Sie nestedScroll verwenden, um diese Unterstützung anderen Komponenten, einschließlich benutzerdefinierter Komponenten, zu gewähren.
Verschachtelter Scrollzyklus
Der verschachtelte Scrollzyklus ist der Fluss von Scroll-Deltas, die über alle Komponenten (oder Knoten) des verschachtelten Scrollsystems in der Hierarchie nach oben und unten gesendet werden, z. B. mithilfe von scrollbaren Komponenten und Modifikatoren oder nestedScroll.
Phasen des verschachtelten Scrollzyklus
Wenn eine scrollbare Komponente ein Triggerereignis (z. B. eine Geste) erkennt, werden die generierten Deltas an das verschachtelte Scrollsystem gesendet, bevor die eigentliche Scrollaktion ausgelöst wird. Sie durchlaufen drei Phasen: Pre-Scroll, Knotenverbrauch und Post-Scroll.

In der ersten Phase, vor dem Scrollen, sendet die Komponente, die die Deltawerte des Triggerereignisses empfangen hat, diese Ereignisse über die Hierarchie nach oben an das oberste übergeordnete Element. Die Delta-Ereignisse werden dann nach unten weitergegeben. Das bedeutet, dass Deltas vom obersten übergeordneten Element nach unten zum untergeordneten Element weitergegeben werden, das den verschachtelten Scrollzyklus gestartet hat.
Dadurch können die übergeordneten Elemente des verschachtelten Scrollens (Kompositionen, die nestedScroll oder scrollbare Modifikatoren verwenden) etwas mit dem Delta tun, bevor der Knoten selbst es nutzen kann.
In der Knotenverbrauchsphase verwendet der Knoten selbst das Delta, das nicht von seinen übergeordneten Elementen verwendet wurde. Hier wird die Scrollbewegung tatsächlich ausgeführt und ist sichtbar.
In dieser Phase kann das Kind die verbleibende Scroll-Menge ganz oder teilweise verbrauchen. Alles, was übrig bleibt, wird zurückgeschickt, um die Phase nach dem Scrollen zu durchlaufen.
In der Phase nach dem Scrollen wird alles, was der Knoten selbst nicht verbraucht hat, wieder an seine Ancestors gesendet.
Die Phase nach dem Scrollen funktioniert ähnlich wie die Phase vor dem Scrollen. Jedes übergeordnete Element kann die Bewegung verarbeiten oder nicht.
Ähnlich wie beim Scrollen kann die Absicht des Nutzers nach Abschluss einer Ziehgeste in eine Geschwindigkeit umgewandelt werden, die zum schnellen Scrollen (Scrollen mit einer Animation) des scrollbaren Containers verwendet wird. Das Fling ist auch Teil des verschachtelten Scrollzyklus und die durch das Drag-Ereignis generierten Geschwindigkeiten durchlaufen ähnliche Phasen: Pre-Fling, Knotenverbrauch und Post-Fling. Die Wisch-Animation ist nur mit Touch-Gesten verknüpft und wird nicht durch andere Ereignisse wie Bedienungshilfen oder Hardware-Scrollen ausgelöst.
Am verschachtelten Scrollzyklus teilnehmen
Die Teilnahme am Zyklus bedeutet, dass Deltas entlang der Hierarchie abgefangen, genutzt und gemeldet werden. Compose bietet eine Reihe von Tools, mit denen Sie beeinflussen können, wie das verschachtelte Scrollsystem funktioniert und wie Sie 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 verschachtelte Scrollzyklus ein System ist, das auf eine Kette von Knoten einwirkt, ist der Modifikator nestedScroll 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 verschachtelten Scroll-Modifikator-Instanzen im Baum, um Informationen über diesen Kanal zu teilen. Die Bausteine dieses Modifikators sind NestedScrollConnection und NestedScrollDispatcher.
NestedScrollConnection bietet eine Möglichkeit, auf die Phasen des verschachtelten Scrollzyklus zu reagieren und das verschachtelte Scrollsystem zu beeinflussen. Sie besteht aus vier Callback-Methoden, die jeweils eine der Verbrauchsphasen darstellen: vor/nach dem Scrollen und vor/nach dem 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 liefert auch Informationen über das Delta, das weitergegeben wird: available-Delta für diese bestimmte Phase und consumed-Delta, das in den vorherigen Phasen verbraucht wurde. Wenn Sie die Weitergabe von Deltas in der Hierarchie irgendwann beenden möchten, können Sie dazu die verschachtelte Scrollverbindung 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 Typ NestedScrollSource.
NestedScrollDispatcher initialisiert den verschachtelten Scrollzyklus. 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 werden, an das System sendet. Aus diesem Grund wird bei den meisten Anwendungsfällen für die Anpassung 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.
Bildgröße beim Scrollen anpassen
Wenn der Nutzer scrollt, können Sie einen dynamischen visuellen Effekt erstellen, bei dem sich die Größe des Bildes je nach Scrollposition ändert.
Größe eines Bildes basierend auf der Scrollposition anpassen
In diesem Snippet wird gezeigt, wie die Größe eines Bildes in einem LazyColumn basierend auf der vertikalen Scrollposition angepasst wird. Das Bild wird kleiner, wenn der Nutzer nach unten scrollt, und größer, wenn er nach oben scrollt. Es bleibt dabei 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 Punkte zum Code
- In diesem Code wird ein
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 voncurrentImageSizeabgeleitet wird. - Die
LazyColumn-Offsets basieren auf demcurrentImageSize. - Der
Imageverwendet einengraphicsLayer-Modifikator, um die berechnete Skalierung anzuwenden. - Das
translationYinnerhalb desgraphicsLayersorgt dafür, dass das Bild beim Skalieren vertikal zentriert bleibt.
Ergebnis
Das obige Snippet führt beim Scrollen zu einem Skalierungseffekt für das Bild:
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 im untergeordneten 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 auf oder funktioniert nicht wie erwartet.
Dieses Problem ist auf die Erwartungen zurückzuführen, die in scrollbaren Composables eingebaut sind.
Scrollbare Composables haben eine „Standardregel für verschachteltes Scrollen“. Das bedeutet, dass jeder scrollbare Container an der verschachtelten Scrollkette teilnehmen muss, sowohl als übergeordnetes Element über NestedScrollConnection als auch als untergeordnetes Element über NestedScrollDispatcher.
Das untergeordnete Element würde dann ein verschachteltes Scrollen für das übergeordnete Element auslösen, wenn das untergeordnete Element an der Grenze ist. Diese Regel ermöglicht beispielsweise, dass Compose Pager und Compose LazyRow gut zusammenarbeiten. Wenn jedoch Interoperabilitäts-Scrolling mit ViewPager2 oder RecyclerView durchgeführt wird, ist das kontinuierliche Scrollen vom untergeordneten zum übergeordneten Element nicht möglich, da diese NestedScrollingParent3 nicht implementieren.
Wenn Sie die Interop API für verschachteltes Scrollen zwischen scrollbaren View-Elementen und scrollbaren Composables aktivieren möchten, die in beide Richtungen verschachtelt sind, können Sie die Interop API für verschachteltes Scrollen verwenden, um diese Probleme in den folgenden Szenarien zu beheben.
Ein kooperierendes übergeordnetes Element View, das ein untergeordnetes Element ComposeView enthält
Ein kooperierendes übergeordnetes Element View ist ein Element, das bereits NestedScrollingParent3 implementiert und daher Scroll-Deltas von einem kooperierenden verschachtelten untergeordneten Compose-Element empfangen kann. ComposeView würde in diesem Fall als untergeordnetes Element fungieren und müsste (indirekt) NestedScrollingChild3 implementieren.
Ein Beispiel für ein kooperierendes übergeordnetes Element ist androidx.coordinatorlayout.widget.CoordinatorLayout.
Wenn Sie eine Interoperabilität für verschachteltes Scrollen zwischen scrollbaren übergeordneten View-Containern und verschachtelten scrollbaren untergeordneten Composables benötigen, können Sie rememberNestedScrollInteropConnection() verwenden.
rememberNestedScrollInteropConnection() lässt die NestedScrollConnection zu und merkt sie sich. Dadurch wird die Interoperabilität des verschachtelten Scrollens zwischen einem übergeordneten View-Element, das NestedScrollingParent3 implementiert, 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 einer untergeordneten zusammensetzbaren Funktion, 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 Compose-Element 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 Compose-Element, das ein untergeordnetes AndroidView enthält
In diesem Szenario wird die Implementierung der Interop API für verschachteltes Scrollen auf der Compose-Seite behandelt, wenn Sie ein übergeordnetes Compose-Element haben, das ein untergeordnetes AndroidView enthält. AndroidView implementiert NestedScrollDispatcher, da es als untergeordnetes Element eines übergeordneten Compose-Scrollelements fungiert, sowie NestedScrollingParent3, da es als übergeordnetes Element eines untergeordneten View-Scrollelements fungiert. Das übergeordnete Compose-Element kann dann verschachtelte Scroll-Deltas von einem verschachtelten scrollbaren untergeordneten Element View empfangen.
Das folgende Beispiel zeigt, wie Sie in diesem Szenario die Interoperabilität für verschachteltes Scrollen zusammen mit einer minimierbaren Symbolleiste in 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( title = {}, 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(R.layout.list_item, null) .apply { // Nested scrolling interop is enabled when // nested scroll is enabled for the root View ViewCompat.setNestedScrollingEnabled(this, true) } } ) } }
In diesem Beispiel wird gezeigt, wie die Interop-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() ein NestedScrollConnection in dem Element installiert, an das Sie es anhängen. NestedScrollConnection ist für die Übertragung der Deltas von der Compose-Ebene auf die View-Ebene verantwortlich. Dadurch kann das Element am verschachtelten Scrollen teilnehmen, aber das Scrollen von Elementen wird nicht automatisch aktiviert. Bei nicht automatisch scrollbaren Composables wie Box oder Column werden Scroll-Deltas nicht im verschachtelten Scrollsystem weitergegeben und erreichen nicht die von rememberNestedScrollInteropConnection() bereitgestellte NestedScrollConnection. Daher erreichen diese Deltas nicht die übergeordnete View-Komponente. Achten Sie darauf, dass Sie auch für diese Arten von verschachtelten Composables scrollbare Modifikatoren festlegen. Weitere Informationen finden Sie im vorherigen Abschnitt zum verschachtelten Scrollen.
Ein nicht kooperierendes übergeordnetes Element View, das ein untergeordnetes Element ComposeView enthält
Eine nicht kooperierende Ansicht ist eine Ansicht, die die erforderlichen NestedScrolling-Schnittstellen auf der View-Seite nicht implementiert. Das bedeutet, dass die Interoperabilität des verschachtelten Scrollens mit diesen Views nicht standardmäßig funktioniert. Nicht kooperierende Views sind RecyclerView und ViewPager2.
Zusätzliche Ressourcen
Empfehlungen für Sie
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Gesten
CoordinatorLayoutzu Compose migrieren- Ansichten in Compose verwenden