Modifikatoren für das Scrollen
Die Modifikatoren verticalScroll
und horizontalScroll
sind die einfachste Möglichkeit, dem Nutzer das Scrollen eines Elements zu ermöglichen, wenn dessen Inhalt größer ist als seine maximale Größe. Mit den Modifikatoren verticalScroll
und horizontalScroll
müssen Sie den Inhalt weder übersetzen noch verschieben.
@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)) } } }
Mit ScrollState
können Sie die Scrollposition ändern oder ihren aktuellen Status abrufen. Zum Erstellen mit Standardparametern verwenden Sie 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)) } } }
Scrollbarer Modifikator
Der scrollable
-Modifikator unterscheidet sich insofern von den Scroll-Modifikatoren, dass scrollable
die Scrollbewegungen erkennt und die Deltas erfasst, seinen Inhalt aber nicht automatisch versetzt. Dies wird stattdessen über ScrollableState
an den Nutzer delegiert, der erforderlich ist, damit dieser Modifikator korrekt funktioniert.
Beim Erstellen von ScrollableState
musst du eine consumeScrollDelta
-Funktion bereitstellen, die bei jedem Scrollschritt (durch Gesteneingabe, gleichmäßiges Scrollen oder Wischen) mit dem Delta in Pixeln aufgerufen wird. Diese Funktion muss die verbrauchte Scrollstrecke zurückgeben, damit das Ereignis richtig weitergegeben wird, wenn es verschachtelte Elemente mit dem Modifikator scrollable
gibt.
Das folgende Snippet erkennt die Touch-Gesten und zeigt einen numerischen Wert für einen Versatz an, aber platziert keine Elemente:
@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()) } }
Verschachteltes Scrollen
Verschachteltes Scrollen ist ein System, bei dem mehrere ineinander enthaltene Scrolling-Komponenten zusammenarbeiten, indem sie auf eine einzelne Scroll-Geste reagieren und ihre Scrolldeltas (Änderungen) kommunizieren.
Das verschachtelte Scrolling-System ermöglicht die Koordination zwischen Komponenten, die scrollbar und hierarchisch verknüpft sind (meist durch gemeinsames übergeordnetes Element). Dieses System verknüpft scrollbare Container und ermöglicht eine Interaktion mit den Scrolldeltas, die weitergegeben und untereinander geteilt werden.
Mit der Funktion „Compose“ haben Sie mehrere Möglichkeiten, verschachteltes Scrollen zwischen zusammensetzbaren Funktionen zu verarbeiten. Ein typisches Beispiel für verschachteltes Scrollen ist eine Liste innerhalb einer anderen Liste. Ein komplexeres Beispiel ist die Minimierbare Symbolleiste.
Automatisches verschachteltes Scrollen
Beim einfachen verschachtelten Scrollen müssen Sie nichts weiter tun. Gesten, die eine Scrollaktion auslösen, werden automatisch von untergeordneten Elementen an übergeordnete Elemente weitergegeben. Wenn das untergeordnete Element also nicht weiterscrollen kann, wird die Geste vom übergeordneten Element verarbeitet.
Automatisch verschachteltes Scrollen wird von einigen Komponenten und Modifikatoren von Composer unterstützt und standardmäßig bereitgestellt: verticalScroll
, horizontalScroll
, scrollable
, Lazy
APIs und TextField
. Wenn der Nutzer also durch ein untergeordnetes untergeordnetes Element von verschachtelten Komponenten scrollt, leiten die vorherigen Modifikatoren die Scroll-Deltas an die übergeordneten Elemente weiter, die verschachteltes Scrollen unterstützen.
Im folgenden Beispiel werden Elemente mit einem verticalScroll
-Modifikator in einem Container gezeigt, auf den auch ein verticalScroll
-Modifikator angewendet wird.
@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) ) } } } } }
Den nestedScroll
-Modifikator verwenden
Wenn Sie ein erweitertes koordiniertes Scrollen zwischen mehreren Elementen erstellen müssen, bietet der Modifikator nestedScroll
mehr Flexibilität, da eine verschachtelte Scrollhierarchie definiert wird. Wie im vorherigen Abschnitt erwähnt, bieten einige Komponenten integrierte Unterstützung für verschachteltes Scrollen. Bei zusammensetzbaren Funktionen, die nicht automatisch scrollbar sind, wie Box
oder Column
, werden Scroll-Deltas bei solchen Komponenten jedoch im verschachtelten Scroll-System nicht weitergegeben und die Deltas erreichen weder die NestedScrollConnection
noch die übergeordnete Komponente. Sie können dieses Problem beheben, indem Sie nestedScroll
verwenden, um anderen Komponenten, einschließlich benutzerdefinierter Komponenten, eine solche Unterstützung zu bieten.
Verschachtelter Scrollzyklus
Der verschachtelte Scrollzyklus ist der Fluss von Scrolldeltas, die im Hierarchiebaum nach oben und unten durch alle Komponenten (oder Knoten) ausgelöst werden, die Teil des verschachtelten Scrollingsystems sind, z. B. mithilfe von scrollbaren Komponenten und Modifikatoren oder nestedScroll
.
Phasen des verschachtelten Scrollzyklus
Wenn ein Triggerereignis (z. B. eine Geste) von einer scrollbaren Komponente erkannt wird, bevor die eigentliche Scrollaktion ausgelöst wird, werden die generierten Deltas an das verschachtelte Scrollsystem gesendet und durchlaufen drei Phasen: Pre-Scrolling, Knotenverbrauch und Post-Scrollen.
In der ersten Pre-Scrolling-Phase löst die Komponente, die die Triggerereignis-Deltas empfangen hat, diese Ereignisse über den Hierarchiebaum nach oben zum obersten übergeordneten Element aus. Die Delta-Ereignisse werden dann nach unten geleitet, was bedeutet, dass Deltas vom Stamm mit dem obersten übergeordneten Element nach unten zum untergeordneten Element übertragen werden, das den verschachtelten Scroll-Zyklus gestartet hat.
Dadurch haben die verschachtelten übergeordneten Scroll-Elemente (zusammensetzbare Funktionen mit nestedScroll
oder scrollbaren Modifikatoren) die Möglichkeit, das Delta zu verwenden, bevor der Knoten es selbst verarbeiten kann.
In der Phase der Knotennutzung verwendet der Knoten selbst das Delta, das von seinen übergeordneten Elementen nicht verwendet wurde. Dies ist der Zeitpunkt, an dem das Scrollen tatsächlich abgeschlossen ist und sichtbar ist.
Während dieser Phase kann das untergeordnete Element entscheiden, den verbleibenden Scrollvorgang vollständig oder teilweise aufzubrauchen. Alles, was übrig ist, wird wieder nach oben gesendet, um die Post-Scroll-Phase zu durchlaufen.
In der Post-Scroll-Phase wird alles, was der Knoten selbst nicht verarbeitet hat, noch einmal an seine Ancestors gesendet.
Die Post-Scroll-Phase funktioniert ähnlich wie die Pre-Scroll-Phase, bei der die übergeordneten Elemente entscheiden, ob sie Inhalte verbrauchen oder nicht.
Ähnlich wie beim Scrollen kann die Absicht des Nutzers nach dem Ende einer Ziehgeste in eine Geschwindigkeit umgewandelt werden, mit der der scrollbare Container geflogen wird (Scrollen mit einer Animation). Das Fling ist ebenfalls Teil des verschachtelten Scroll-Zyklus und die durch das Drag-Ereignis erzeugten Geschwindigkeiten durchlaufen ähnliche Phasen: Vor-Fling, Knotenverbrauch und Nach-Feing. Beachten Sie, dass die Fling-Animation nur mit Touch-Gesten verknüpft ist und nicht durch andere Ereignisse ausgelöst wird, z. B. durch Scrollen oder Scrollen auf der Hardware.
Am verschachtelten Scroll-Zyklus teilnehmen
Teilnahme am Zyklus bedeutet, dass der Konsum von Deltas entlang der Hierarchie abgefangen, verbraucht und gemeldet wird. Compose bietet eine Reihe von Tools, die beeinflussen, wie das verschachtelte Scrolling-System funktioniert und wie direkt damit interagiert werden kann, z. B. wenn Sie etwas mit den Scrolldeltas bearbeiten müssen, bevor eine scrollbare Komponente überhaupt scrollt.
Wenn es sich beim verschachtelten Scrollzyklus um ein System handelt, das auf eine Knotenkette wirkt, können diese Änderungen mit dem Modifikator nestedScroll
abgefangen und in diese eingefügt sowie die in der Kette weitergegebenen Daten (Scroll-Deltas) beeinflusst werden. Dieser Modifikator kann an einer beliebigen Stelle in der Hierarchie platziert werden und kommuniziert mit verschachtelten Scrollmodifikatorinstanzen in der Baumstruktur, damit er Informationen über diesen Kanal teilen kann. 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 Phasen des Verbrauchs darstellen: Pre/Post-Scrolling und 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 } }
Jeder Callback enthält auch Informationen über das weitergegebene Delta: das Delta available
für diese bestimmte Phase und das in den vorherigen Phasen verwendete consumed
-Delta. Wenn Sie die Weitergabe von Deltas in der Hierarchie an einem beliebigen Punkt beenden möchten, können Sie dazu die verschachtelte Scroll-Verbindung 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 liefern Informationen zum Typ NestedScrollSource
.
NestedScrollDispatcher
initialisiert den verschachtelten Scrollzyklus. Durch die Verwendung eines Disponenten und das Aufrufen seiner Methoden
wird der Zyklus ausgelöst. Scrollbare Container haben einen integrierten Disponenten, 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 Disponenten verwendet, um auf bereits vorhandene Deltas zu reagieren, anstatt neue zu senden.
Weitere Verwendungen finden Sie unter NestedScrollDispatcherSample
.
Verschachtelte Scroll-Interoperabilität
Wenn Sie scrollbare View
-Elemente in scrollbaren zusammensetzbaren Funktionen verschachteln oder umgekehrt, können Probleme auftreten. Die auffälligsten treten auf, wenn Sie durch das untergeordnete Element scrollen, seine Start- oder Endgrenzen erreichen und erwarten, dass das übergeordnete Element das Scrollen übernimmt. Dieses erwartete Verhalten kann jedoch entweder nicht eintreten oder nicht wie erwartet funktionieren.
Dieses Problem ist auf die Erwartungen zurückzuführen, die von den Funktionen für scrollbare zusammensetzbare Funktionen erfüllt wurden.
Zusammensetzbare Funktionen mit Scrollen haben eine Standardregel für verschachteltes Scrollen. Das bedeutet, dass jeder scrollbare Container an der verschachtelten Scroll-Kette teilnehmen muss, sowohl als übergeordneter Container über NestedScrollConnection
als auch als untergeordneter Container über NestedScrollDispatcher
.
Das untergeordnete Element führt dann ein verschachteltes Scrollen für das übergeordnete Element aus, wenn sich das untergeordnete Element an der Grenze befindet. Diese Regel sorgt beispielsweise dafür, dass die Optionen „Compose“ (Pager
) und „Compose“ (LazyRow
) gut zusammen funktionieren. Wenn jedoch mit ViewPager2
oder RecyclerView
gescrollt wird, weil NestedScrollingParent3
nicht implementiert ist, ist kein kontinuierliches Scrollen von einem untergeordneten zum übergeordneten Element möglich.
Wenn Sie die verschachtelte Scrolling Interop API zwischen scrollbaren View
-Elementen und scrollbaren zusammensetzbaren Funktionen aktivieren möchten, die in beide Richtungen verschachtelt sind, können Sie in den folgenden Szenarien die verschachtelte Scrolling Interop API verwenden, um diese Probleme zu beheben.
Ein kooperierendes übergeordnetes Element View
mit einem untergeordneten ComposeView
Eine kooperative übergeordnete View
ist eine, die NestedScrollingParent3
bereits implementiert und daher Scroll-Deltas von einer kooperativen verschachtelten untergeordneten zusammensetzbaren Funktion erhalten kann. ComposeView
würde in diesem Fall als untergeordnetes Objekt fungieren und müsste (indirekt) NestedScrollingChild3
implementieren.
Ein Beispiel für ein kooperatives Elternteil ist androidx.coordinatorlayout.widget.CoordinatorLayout
.
Wenn Sie verschachtelte Scrolling-Interoperabilität zwischen übergeordneten View
-Containern und verschachtelten scrollbaren untergeordneten zusammensetzbaren Funktionen benötigen, können Sie rememberNestedScrollInteropConnection()
verwenden.
rememberNestedScrollInteropConnection()
ermöglicht und merkt sich die NestedScrollConnection
, die die verschachtelte Scroll-Interoperabilität zwischen einem übergeordneten View
-Element, das NestedScrollingParent3
implementiert, und einem untergeordneten Element vom Typ „Schreiben“ ermöglicht. Er sollte in Verbindung mit einem nestedScroll
-Modifikator verwendet werden. Da das verschachtelte Scrollen auf der Seite „Schreiben“ standardmäßig aktiviert ist, können Sie über diese Verbindung sowohl das verschachtelte Scrollen auf der Seite View
aktivieren als auch die erforderliche Kleblogik zwischen Views
und zusammensetzbaren Funktionen einfügen.
Ein häufiger Anwendungsfall ist die Verwendung von CoordinatorLayout
, CollapsingToolbarLayout
und einer untergeordneten zusammensetzbaren Funktion, wie in diesem Beispiel:
<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>
Richten Sie in der Aktivität oder dem Fragment die zusammensetzbare Funktion Ihres untergeordneten Elements und die erforderliche NestedScrollConnection
ein:
open class MainActivity : ComponentActivity() { @OptIn(ExperimentalComposeUiApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById<ComposeView>(R.id.compose_view).apply { setContent { val nestedScrollInterop = rememberNestedScrollInteropConnection() // Add the nested scroll connection to your top level @Composable element // using the nestedScroll modifier. LazyColumn(modifier = Modifier.nestedScroll(nestedScrollInterop)) { items(20) { item -> Box( modifier = Modifier .padding(16.dp) .height(56.dp) .fillMaxWidth() .background(Color.Gray), contentAlignment = Alignment.Center ) { Text(item.toString()) } } } } } } }
Eine zusammensetzbare übergeordnete Funktion mit einem untergeordneten AndroidView
In diesem Szenario wird die Implementierung der verschachtelten Scrolling-Interop API auf der Erstellungsseite behandelt, wenn Sie eine übergeordnete zusammensetzbare Funktion mit einem untergeordneten AndroidView
haben. Der AndroidView
implementiert NestedScrollDispatcher
, da er einem übergeordneten übergeordneten Element des Typs „Compose“ als untergeordnetes Element dient und NestedScrollingParent3
, da es als übergeordnetes Element für ein untergeordnetes View
-Scrollen fungiert. Das übergeordnete Element kann dann verschachtelte Scroll-Deltas von einem verschachtelten scrollbaren untergeordneten View
erhalten.
Das folgende Beispiel zeigt, wie Sie in diesem Szenario eine verschachtelte Scroll-Interop zusammen mit einer minimierbaren Symbolleiste zum Schreiben erstellen 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) {
// ...
}
}
// ...
}
Dieses Beispiel zeigt, 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 verschachtelte Scrolling-Interop API mit BottomSheetDialogFragment
verwendet wird, um ein Drag-and-drop-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
}
}
}
Durch rememberNestedScrollInteropConnection()
wird ein NestedScrollConnection
-Element in dem Element installiert, an das Sie es anhängen. NestedScrollConnection
überträgt die Deltas von der Erstellungsebene an die View
-Ebene. Dadurch kann das Element an verschachteltem Scrollen teilnehmen, das automatische Scrollen von Elementen wird jedoch nicht automatisch aktiviert. Bei zusammensetzbaren Funktionen, die nicht automatisch scrollbar sind, wie Box
oder Column
, werden Scroll-Deltas bei solchen Komponenten im verschachtelten Scroll-System nicht weitergegeben und die Deltas erreichen nicht den von rememberNestedScrollInteropConnection()
bereitgestellten NestedScrollConnection
. Daher erreichen diese Deltas nicht die übergeordnete Komponente View
. Um dieses Problem zu beheben, müssen Sie auch scrollbare Modifikatoren für diese Typen von verschachtelten zusammensetzbaren Funktionen festlegen. Weitere Informationen findest du im vorherigen Abschnitt zum verschachtelten Scrollen.
Ein nicht kooperierendes übergeordnetes Element View
mit einem untergeordneten ComposeView
Eine nicht kooperative Ansicht ist eine Ansicht, die nicht die erforderlichen NestedScrolling
-Schnittstellen auf der View
-Seite implementiert. Das bedeutet, dass die verschachtelte Scroll-Interoperabilität mit diesen Views
nicht von Anfang an funktioniert. Nicht kooperierende Views
sind RecyclerView
und ViewPager2
.
Empfehlungen für dich
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Touch-Gesten
CoordinatorLayout
zu Compose migrieren- Ansichten in „Compose“ verwenden