Przewiń

Modyfikatory przewijania

Modyfikatory verticalScroll i horizontalScroll zapewniają najprostszy sposób na przewijanie elementu przez użytkownika, gdy granice jego zawartości przekraczają granicę maksymalnego rozmiaru. Korzystając z modyfikatorów verticalScroll i horizontalScroll, nie musisz tłumaczyć ani przesuwać treści.

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

Prosta pionowa lista reagująca na gesty przewijania

Element ScrollState umożliwia zmianę pozycji przewijania lub pobranie jej bieżącego stanu. Aby utworzyć go z parametrami domyślnymi, użyj parametru 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))
        }
    }
}

Modyfikator przewijany

Modyfikator scrollable różni się od modyfikatorów przewijania tym, że scrollable wykrywa gesty przewijania, ale nie przesuwa ich zawartości. Aby ten modyfikator działał prawidłowo, wymagany jest element ScrollableState. Tworząc ScrollableState, musisz podać funkcję consumeScrollDelta, która będzie wywoływana po każdym kroku przewijania (poprzez wprowadzanie gestami, płynne przewijanie lub przesuwanie palcem) z delta w pikselach. Aby zapewnić prawidłowe rozpowszechnianie zdarzenia w przypadku elementów zagnieżdżonych z modyfikatorem scrollable, funkcja musi zwracać pokonywaną odległość przewijania.

Ten fragment kodu wykrywa gesty i wyświetla wartość liczbową przesunięcia, ale nie zwraca żadnych elementów:

@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())
    }
}

Element interfejsu wykrywający naciśnięcie palca i wyświetlający wartość liczbową lokalizacji palca.

Zagnieżdżone przewijanie

Tworzenie obsługuje przewijanie zagnieżdżone,w którym wiele elementów reaguje na pojedynczy gest przewijania. Typowym przykładem przewijania zagnieżdżonego jest lista wewnątrz innej listy, a w bardziej skomplikowanym przypadku – zwijający się pasek narzędzi.

Automatyczne przewijanie zagnieżdżone

Proste przewijanie zagnieżdżone nie wymaga ze strony użytkownika żadnego działania. Gesty inicjujące przewijanie są automatycznie przekazywane z dziedziny dziecka do rodziców. Gdy dziecko nie może już przewinąć strony, gest jest obsługiwany przez element nadrzędny.

Automatyczne zagnieżdżone przewijanie jest obsługiwane i dostępne od razu przez niektóre komponenty i modyfikatory komponentu Compose: verticalScroll, horizontalScroll, scrollable, Lazy API i TextField. Oznacza to, że gdy użytkownik przewinie wewnętrzne elementy podrzędne zagnieżdżonych komponentów, poprzednie modyfikatory przekazują delta przewijania do elementów nadrzędnych, które obsługują przewijanie zagnieżdżone.

Poniższy przykład pokazuje elementy z zastosowanym modyfikatorem verticalScroll wewnątrz kontenera, do którego zastosowano też modyfikator 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)
                    )
                }
            }
        }
    }
}

2 zagnieżdżone elementy interfejsu umożliwiające przewijanie w pionie, reagujące na gesty wewnątrz i na zewnątrz elementu wewnętrznego

Korzystanie z modyfikatora nestedScroll

Jeśli chcesz utworzyć zaawansowane skoordynowane przewijanie między wieloma elementami, modyfikator nestedScroll zapewnia większą elastyczność, ponieważ definiuje zagnieżdżoną hierarchię przewijania. Jak wspomnieliśmy w poprzedniej sekcji, niektóre komponenty mają wbudowaną obsługę przewijania zagnieżdżonego. Jednak w przypadku elementów kompozycyjnych, których nie da się przewijać automatycznie, np. Box czy Column, delta takich komponentów nie będą rozpowszechniane w zagnieżdżonym systemie przewijania, a delta nie będą docierać do komponentu NestedScrollConnection ani elementu nadrzędnego. Aby rozwiązać ten problem, możesz użyć nestedScroll, aby udzielić takiej pomocy innym komponentom, w tym komponentom niestandardowym.

Zagnieżdżona interoperacyjność z przewijaniem (od opcji Tworzenie 1.2.0)

Gdy próbujesz zagnieździć przewijane elementy View w komponentach z możliwością przewijania lub na odwrót, możesz napotkać problemy. Najczęściej dzieje się tak, gdy przewiniesz element podrzędny do osiągnięcia jego początku lub końca i oczekujesz, że rodzic przejmie przewijanie. Może ono jednak nie działać lub nie działać zgodnie z oczekiwaniami.

Ten problem jest wynikiem oczekiwań związanych z przewijanymi elementami kompozycyjnymi. Elementy kompozycyjne z możliwością przewijania mają regułę „zagnieżdżone przewijanie domyślne”, co oznacza, że każdy kontener, który można przewijać, musi należeć do zagnieżdżonego łańcucha przewijania – zarówno jako element nadrzędny za pomocą NestedScrollConnection, jak i jako element podrzędny w ramach NestedScrollDispatcher. Gdy element podrzędny jest na granicy, element podrzędny generuje zagnieżdżony przewijany element nadrzędny. Na przykład ta reguła umożliwia współdziałanie funkcji Utwórz Pager i Utwórz LazyRow. Jeśli jednak przewijanie współdziała z elementami ViewPager2 lub RecyclerView, które nie implementują NestedScrollingParent3, ciągłe przewijanie z elementu podrzędnego do elementu nadrzędnego nie jest możliwe.

Aby włączyć zagnieżdżony interfejs API interoperacyjności przewijania między elementami View, które można przewijać oraz przewijanymi elementami kompozycyjnymi, zagnieżdżonymi w obu kierunkach, możesz zapobiec tym problemom w poniższych scenariuszach, używając zagnieżdżonego interfejsu API interoperacyjności przewijania.

Współpracujący element nadrzędny View zawierający element podrzędny ComposeView

Współpracujący element nadrzędny View to taki, który implementuje już NestedScrollingParent3, więc może otrzymywać wartości delta przewijania z współpracującego zagnieżdżonego elementu kompozycyjnego. ComposeView będzie działać jako element podrzędny i (pośrednio) wdrożyć NestedScrollingChild3. Przykładem współpracującego elementu nadrzędnego jest androidx.coordinatorlayout.widget.CoordinatorLayout.

Jeśli potrzebujesz zagnieżdżonego przewijania między możliwymi do przewijania kontenerami nadrzędnymi View a zagnieżdżonymi podrzędnymi elementami kompozycyjnymi z możliwością przewijania, możesz użyć funkcji rememberNestedScrollInteropConnection().

rememberNestedScrollInteropConnection() dopuszcza i zapamiętuje element NestedScrollConnection, który umożliwia współdziałanie z zagnieżdżonym przewijaniem między elementem nadrzędnym View, który implementuje element NestedScrollingParent3 oraz elementem podrzędnym tworzenia. Tej opcji należy używać w połączeniu z modyfikatorem nestedScroll. Ponieważ przewijanie zagnieżdżone jest domyślnie włączone po stronie tworzenia wiadomości, możesz użyć tego połączenia, aby włączyć zarówno zagnieżdżone przewijanie po stronie elementu View, jak i odpowiednią logikę klejową między elementami Views i kompozycyjnymi.

Częstym przypadkiem użycia jest użycie właściwości CoordinatorLayout, CollapsingToolbarLayout i podrzędnego elementu kompozycyjnego, co widać w tym przykładzie:

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

W sekcji Aktywność lub Fragment musisz skonfigurować podrzędną funkcję kompozycyjną i wymagany NestedScrollConnection:

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

Nadrzędny element kompozycyjny zawierający element podrzędny AndroidView

Ten scenariusz obejmuje implementację zagnieżdżonego interfejsu API interoperacyjności przewijania po stronie tworzenia – gdy istnieje nadrzędny obiekt kompozycyjny zawierający element podrzędny AndroidView. Element AndroidView implementuje właściwość NestedScrollDispatcher, ponieważ działa jako element podrzędny wobec przewijającego elementu nadrzędnego w komponencie Compose, a także NestedScrollingParent3, ponieważ pełni rolę elementu nadrzędnego względem przewijanego elementu podrzędnego View. Element nadrzędny tworzenia będzie wówczas mógł otrzymywać zagnieżdżone delta przewijania z zagnieżdżonego elementu podrzędnego View, który można przewijać.

Poniższy przykład pokazuje, jak w tym scenariuszu uzyskać zagnieżdżone przewijanie wraz z paskiem narzędzi zwijania widoku:

@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) {
            // ...
        }
    }
    // ...
}

Ten przykład pokazuje, jak korzystać z interfejsu API z modyfikatorem 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)
                    }
            }
        )
    }
}

Na koniec ten przykład pokazuje, jak interfejs API zagnieżdżonego przewijania Interop API jest używany w BottomSheetDialogFragment do zapewnienia skutecznego przeciągania i zamykania:

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

Pamiętaj, że rememberNestedScrollInteropConnection() zainstaluje NestedScrollConnection w elemencie, do którego go dołączysz. NestedScrollConnection odpowiada za przenoszenie delt z poziomu tworzenia na poziom View. Dzięki temu element może uczestniczyć w zagnieżdżonym przewijaniu, ale nie umożliwia automatycznego przewijania elementów. W przypadku elementów kompozycyjnych, które nie są przewijane automatycznie, np. Box lub Column, delta takich komponentów nie będą rozpowszechniane w zagnieżdżonym systemie przewijania, a delta nie będą docierać do elementów NestedScrollConnection w funkcji rememberNestedScrollInteropConnection(), dlatego te delta nie dotrą do nadrzędnego komponentu View. Aby rozwiązać ten problem, pamiętaj, aby ustawić modyfikatory przewijane na te typy zagnieżdżonych funkcji kompozycyjnych. Więcej informacji znajdziesz w poprzedniej sekcji poświęconej przewijaniu zagnieżdżonym.

Niedziałający element nadrzędny View zawierający element podrzędny ComposeView

Widok nieobsługiwany to widok, w którym po stronie View nie są zaimplementowane niezbędne interfejsy NestedScrolling. Pamiętaj, że oznacza to, że zagnieżdżone przewijanie z tymi elementami Views nie działa od razu. Views, które nie obsługują współpracy, to RecyclerView i ViewPager2.