Scrollen

Scroll-Modifikatoren

Die Modifikatoren verticalScroll und horizontalScroll sind die einfachste Möglichkeit, dem Nutzer das Scrollen durch ein Element zu ermöglichen, wenn die Grenzen seines Inhalts die maximale Größe überschreiten. Mit den Modifikatoren verticalScroll und horizontalScroll müssen Sie den Inhalt nicht übersetzen oder 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))
        }
    }
}

Eine einfache vertikale Liste, die auf Scroll-Gesten reagiert

Mit ScrollState kannst du die Scrollposition ändern oder den aktuellen Status abrufen. Wenn Sie sie mit Standardparametern erstellen möchten, 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 von den Scroll-Modifikatoren dadurch, dass scrollable die Scroll-Gesten erkennt, aber ihren Inhalt nicht versetzt. Damit dieser Modifikator korrekt funktioniert, ist ein ScrollableState erforderlich. Beim Erstellen von ScrollableState musst du eine consumeScrollDelta-Funktion bereitstellen, die bei jedem Scrollschritt (durch Gesteneingabe, optimiertes Scrollen oder Ziehen) mit dem Delta in Pixeln aufgerufen wird. Diese Funktion muss die verbrauchte Scrollstrecke zurückgeben, damit das Ereignis in Fällen korrekt weitergegeben wird, in denen verschachtelte Elemente mit dem Modifikator scrollable vorhanden sind.

Das folgende Snippet erkennt die Gesten und zeigt einen numerischen Wert für einen Offset an, verschiebt aber 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())
    }
}

Ein UI-Element, das den Fingerdruck erkennt und den numerischen Wert für die Position des Fingers anzeigt

Verschachteltes Scrollen

Die Funktion „Compose“ unterstützt das verschachtelte Scrollen,bei dem mehrere Elemente auf eine einzelne Scroll-Geste reagieren. Ein typisches Beispiel für verschachteltes Scrollen ist eine Liste innerhalb einer anderen Liste. Ein komplexerer Fall ist eine minimierbare Symbolleiste.

Automatisches verschachteltes Scrollen

Einfaches verschachteltes Scrollen erfordert von Ihrer Seite keine Maßnahmen. Gesten, die einen Scrollvorgang starten, werden automatisch von den untergeordneten Elementen an die übergeordneten Elemente weitergegeben. Wenn das Kind also nicht weiter scrollen kann, wird die Geste vom übergeordneten Element ausgeführt.

Automatisches 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 in einem inneren untergeordneten Element verschachtelter Komponenten scrollt, geben die vorherigen Modifikatoren die Scroll-Deltas an die übergeordneten Elemente weiter, die verschachteltes Scrollen unterstützen.

Im folgenden Beispiel sehen Sie Elemente mit dem Modifizierer verticalScroll in einem Container, auf den auch der Modifizierer verticalScroll 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)
                    )
                }
            }
        }
    }
}

Zwei verschachtelte UI-Elemente mit vertikalem Scrollen, die auf Gesten innerhalb und außerhalb des inneren Elements reagieren

Den nestedScroll-Modifikator verwenden

Wenn Sie ein erweitertes koordiniertes Scrollen zwischen mehreren Elementen erstellen müssen, bietet der Modifizierer nestedScroll mehr Flexibilität, da Sie eine verschachtelte Scroll-Hierarchie definieren. Wie im vorherigen Abschnitt erwähnt, haben einige Komponenten eine integrierte Scrollunterstützung. Bei zusammensetzbaren Funktionen, die nicht automatisch gescrollt werden können, z. B. Box oder Column, werden Scroll-Deltas für diese Komponenten jedoch im verschachtelten Scrollsystem nicht weitergegeben und die Deltas erreichen weder NestedScrollConnection noch die übergeordnete Komponente. Sie können dieses Problem beheben, indem Sie nestedScroll verwenden, um diese Unterstützung auf andere Komponenten, einschließlich benutzerdefinierter Komponenten, zu übertragen.

Verschachtelte Scroll-Interoperabilität (ab Compose 1.2.0)

Wenn Sie versuchen, scrollbare View-Elemente in scrollbaren zusammensetzbaren Funktionen zu verschachteln oder umgekehrt, können Probleme auftreten. Die spärlichsten treten auf, wenn Sie das untergeordnete Element scrollen, seine Start- oder Endgrenzen erreichen und erwarten, dass das übergeordnete Element den Scrollvorgang übernimmt. Dieses erwartete Verhalten tritt jedoch möglicherweise nicht ein oder funktioniert nicht wie erwartet.

Dieses Problem ist auf die Erwartungen an scrollbare zusammensetzbare Funktionen zurückzuführen. Für scrollbare zusammensetzbare Funktionen gilt eine „standardmäßige verschachtelte Scrolling“-Regel. Das bedeutet, dass jeder scrollbare Container Teil der verschachtelten Scrollkette sein muss, sowohl als übergeordneter Container über NestedScrollConnection als auch als untergeordneter Container über NestedScrollDispatcher. Das untergeordnete Element würde dann einen verschachtelten Scrollvorgang für das übergeordnete Element ausführen, wenn sich das untergeordnete Element am Grenzwert befindet. Diese Regel sorgt beispielsweise dafür, dass die Funktionen „Schreiben“ Pager und „Schreiben“ LazyRow gut zusammen funktionieren. Wenn jedoch mit ViewPager2 oder RecyclerView Interoperabilität gescrollt wird, da NestedScrollingParent3 dort nicht implementiert ist, ist das kontinuierliche Scrollen von den untergeordneten zu übergeordneten Elementen nicht 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 die verschachtelte Scrolling Interop API verwenden, um diese Probleme in den folgenden Szenarien zu minimieren.

Ein kooperierendes übergeordnetes Element (View) mit einem untergeordneten ComposeView

Eine kooperierende übergeordnete View ist eine, die NestedScrollingParent3 bereits implementiert und daher Scroll-Deltas von einer kooperierenden verschachtelten untergeordneten zusammensetzbaren Funktion empfangen kann. ComposeView würde in diesem Fall als untergeordnetes Netzwerk fungieren und NestedScrollingChild3 (indirekt) implementieren. Ein Beispiel für ein kooperierendes übergeordnetes Element ist androidx.coordinatorlayout.widget.CoordinatorLayout.

Wenn Sie verschachtelte Scroll-Interoperabilität zwischen scrollbaren übergeordneten View-Containern und verschachtelten scrollbaren untergeordneten zusammensetzbaren Funktionen benötigen, können Sie rememberNestedScrollInteropConnection() verwenden.

rememberNestedScrollInteropConnection() lässt und speichert den NestedScrollConnection, der verschachtelte Scroll-Interoperabilität zwischen einem übergeordneten View-Element, das NestedScrollingParent3 implementiert, und einer untergeordneten „Compose“-Datei ermöglicht. Dieser sollte in Verbindung mit einem nestedScroll-Modifikator verwendet werden. Da das verschachtelte Scrollen auf der Seite „Compose“ standardmäßig aktiviert ist, können Sie über diese Verbindung sowohl verschachteltes Scrollen auf der View-Seite aktivieren und die erforderliche Klebelogik 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>

Du musst in deiner Aktivität oder deinem Fragment die untergeordnete zusammensetzbare Funktion und das erforderliche NestedScrollConnection einrichten:

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 übergeordnete zusammensetzbare Funktion, die das untergeordnete AndroidView enthält

In diesem Szenario wird die Implementierung der verschachtelten Scroll-Interop API auf der Erstellungsseite beschrieben, wenn Sie eine übergeordnete zusammensetzbare Funktion haben, die ein untergeordnetes AndroidView-Element enthält. Mit AndroidView wird NestedScrollDispatcher implementiert, da es dem übergeordneten Element „Compose“ als untergeordnetes Element fungiert. Außerdem wird NestedScrollingParent3 als übergeordnetes Element für ein untergeordnetes Element des Typs View verwendet, das dem Bildlauf untergeordnet ist. Das übergeordnete Element „Compose“ kann dann verschachtelte Scrolldeltas von einem verschachtelten scrollbaren untergeordneten View empfangen.

Das folgende Beispiel zeigt, wie Sie in diesem Szenario verschachtelte Scroll-Interop zusammen mit einer minimierbaren Symbolleiste "Compose" 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)
                    }
            }
        )
    }
}

Abschließend wird in diesem Beispiel gezeigt, wie die verschachtelte Scroll-Interop API mit BottomSheetDialogFragment verwendet wird, um ein erfolgreiches Ziehen und Schließen zu erreichen:

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
        }
    }
}

Mit rememberNestedScrollInteropConnection() wird ein NestedScrollConnection-Element in dem Element installiert, an das Sie es anhängen. NestedScrollConnection ist für die Übertragung der Deltas von der Erstellungsebene in die Ebene View verantwortlich. Dadurch kann das Element am verschachtelten Scrollen teilnehmen, aber nicht automatisch das Scrollen von Elementen. Bei zusammensetzbaren Funktionen, die nicht automatisch gescrollt werden können, z. B. Box oder Column, werden Scroll-Deltas dieser Komponenten im verschachtelten Scrollsystem nicht weitergegeben. Sie erreichen außerdem nicht den von rememberNestedScrollInteropConnection() bereitgestellten NestedScrollConnection. Daher erreichen diese Deltas nicht die übergeordnete View-Komponente. Um dieses Problem zu beheben, müssen Sie auch scrollbare Modifikatoren auf diese Arten verschachtelter zusammensetzbarer Funktionen festlegen. Weitere Informationen finden Sie im vorherigen Abschnitt zum verschachtelten Scrollen.

Ein nicht kooperierendes übergeordnetes Element (View) mit einem untergeordneten ComposeView

In einer nicht kooperativen Ansicht werden die erforderlichen NestedScrolling-Schnittstellen auf der View-Seite nicht implementiert. Das bedeutet, dass die Interoperabilität mit verschachteltem Scrollen mit diesen Views nicht standardmäßig funktioniert. Die nicht kooperierenden Views sind RecyclerView und ViewPager2.