Przewijanie zagnieżdżone to system, w którym kilka komponentów przewijania zawartych w sobie współpracuje ze sobą, reagując na jeden gest przewijania i przekazując sobie informacje o zmianach przewijania.
System przewijania zagnieżdżonego umożliwia koordynację między komponentami, które można przewijać i które są połączone hierarchicznie (najczęściej przez wspólny element nadrzędny). Ten system łączy kontenery przewijania i umożliwia interakcję z zmianami przewijania, które są propagowane i udostępniane między nimi.
Compose udostępnia kilka sposobów obsługi przewijania zagnieżdżonego między elementami kompozycyjnymi. Typowym przykładem przewijania zagnieżdżonego jest lista w innej liście, a bardziej złożonym przypadkiem jest zwijany pasek narzędzi.
Automatyczne przewijanie zagnieżdżone
Proste przewijanie zagnieżdżone nie wymaga żadnych działań z Twojej strony. Gesty, które inicjują przewijanie, są automatycznie propagowane z elementów podrzędnych do nadrzędnych. Gdy element podrzędny nie może już przewijać, gest jest obsługiwany przez jego element nadrzędny.
Automatyczne przewijanie zagnieżdżone jest obsługiwane i udostępniane od razu przez niektóre komponenty i modyfikatory Compose:
verticalScroll,
horizontalScroll,
scrollable,
Lazy interfejsy API i TextField. Oznacza to, że gdy użytkownik przewija wewnętrzny element podrzędny komponentów zagnieżdżonych, poprzednie modyfikatory propagują zmiany przewijania do elementów nadrzędnych, które obsługują przewijanie zagnieżdżone.
Poniższy przykład pokazuje elementy z zastosowanym modyfikatorem
verticalScroll
w kontenerze, do którego również zastosowano 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) ) } } } } }
Używanie modyfikatora nestedScroll
Jeśli chcesz utworzyć zaawansowane skoordynowane przewijanie między kilkoma elementami,
modyfikator
nestedScroll
zapewnia większą elastyczność dzięki zdefiniowaniu hierarchii przewijania zagnieżdżonego. Jak wspomnieliśmy w poprzedniej sekcji, niektóre komponenty mają wbudowaną obsługę przewijania zagnieżdżonego. W przypadku elementów kompozycyjnych, których nie można przewijać automatycznie, takich jak Box czy Column, zmiany przewijania w takich komponentach nie będą propagowane w systemie przewijania zagnieżdżonego, a zmiany nie dotrą do NestedScrollConnection ani do komponentu nadrzędnego. Aby rozwiązać ten problem, możesz użyć nestedScroll, aby zapewnić taką obsługę innym komponentom, w tym komponentom niestandardowym.
Cykl przewijania zagnieżdżonego
Cykl przewijania zagnieżdżonego to przepływ zmian przewijania, które są wysyłane w górę i w dół drzewa hierarchii przez wszystkie komponenty (lub węzły) należące do systemu przewijania zagnieżdżonego, na przykład za pomocą komponentów i modyfikatorów przewijania lub nestedScroll.
Fazy cyklu przewijania zagnieżdżonego
Gdy komponent przewijania wykryje zdarzenie wyzwalające (np. gest), zanim jeszcze zostanie wywołane rzeczywiste przewijanie, wygenerowane zmiany są wysyłane do systemu przewijania zagnieżdżonego i przechodzą przez 3 fazy: przewijanie wstępne, zużycie węzła i przewijanie końcowe.
W pierwszej fazie, czyli przewijania wstępnego, komponent, który otrzymał zmiany zdarzenia wyzwalającego, będzie wysyłać te zdarzenia w górę drzewa hierarchii do elementu nadrzędnego najwyższego poziomu. Zdarzenia zmiany będą się następnie rozprzestrzeniać w dół, co oznacza, że zmiany będą propagowane od elementu nadrzędnego najwyższego poziomu do elementu podrzędnego, który rozpoczął cykl przewijania zagnieżdżonego.
Dzięki temu elementy nadrzędne przewijania zagnieżdżonego (elementy kompozycyjne używające nestedScroll lub modyfikatorów przewijania) mogą coś zrobić ze zmianą, zanim węzeł sam ją zużyje.
W fazie zużycia węzła sam węzeł użyje wszystkich zmian, które nie zostały użyte przez jego elementy nadrzędne. Wtedy właśnie następuje przewijanie i jest ono widoczne.
W tej fazie element podrzędny może zużyć cały lub część pozostałego przewijania. Wszystko, co zostanie, zostanie wysłane z powrotem w górę, aby przejść do fazy przewijania końcowego.
Wreszcie w fazie przewijania końcowego wszystko, czego węzeł nie zużył, zostanie ponownie wysłane do jego przodków w celu zużycia.
Faza przewijania końcowego działa podobnie jak faza przewijania wstępnego – każdy z elementów nadrzędnych może zdecydować, czy chce zużyć zmianę.
Podobnie jak w przypadku przewijania, gdy gest przeciągnięcia się kończy, intencja użytkownika może zostać przekształcona w prędkość, która jest używana do przewijania (przewijania za pomocą animacji) kontenera przewijania. Przewijanie jest również częścią cyklu przewijania zagnieżdżonego, a prędkości generowane przez zdarzenie przeciągnięcia przechodzą przez podobne fazy: przewijanie wstępne, zużycie węzła i przewijanie końcowe. Pamiętaj, że animacja przewijania jest powiązana tylko z gestem dotykowym i nie będzie wywoływana przez inne zdarzenia, takie jak przewijanie sprzętowe lub przewijanie za pomocą funkcji ułatwień dostępu.
Udział w cyklu przewijania zagnieżdżonego
Udział w cyklu oznacza przechwytywanie, zużywanie i zgłaszanie zużycia zmian w hierarchii. Compose udostępnia zestaw narzędzi, które pozwalają wpływać na działanie systemu przewijania zagnieżdżonego i bezpośrednio z nim współpracować, na przykład gdy trzeba coś zrobić ze zmianami przewijania, zanim komponent przewijania zacznie się przewijać.
Jeśli cykl przewijania zagnieżdżonego jest systemem działającym na łańcuchu węzłów, the
nestedScroll
modifier is a way of intercepting and inserting into these changes, and
influencing the data (scroll deltas) that are propagated in the chain. Ten modyfikator można umieścić w dowolnym miejscu hierarchii. Komunikuje się on z instancjami modyfikatora przewijania zagnieżdżonego w górę drzewa, dzięki czemu może udostępniać informacje za pomocą tego kanału. Elementami składowymi tego modyfikatora są NestedScrollConnection i NestedScrollDispatcher.
NestedScrollConnection
umożliwia reagowanie na fazy cyklu przewijania zagnieżdżonego i wpływanie na system przewijania zagnieżdżonego. Składa się z 4 metod wywołania zwrotnego, z których każda reprezentuje jedną z faz zużycia: przewijanie wstępne/końcowe i przewijanie wstępne/końcowe.
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 } }
Każde wywołanie zwrotne zawiera też informacje o propagowanej zmianie: zmiana available dla danej fazy i zmiana consumed zużyta w poprzednich fazach. Jeśli w dowolnym momencie chcesz przestać propagować zmiany w górę hierarchii, możesz użyć do tego połączenia przewijania zagnieżdżonego:
val disabledNestedScrollConnection = remember { object : NestedScrollConnection { override fun onPostScroll( consumed: Offset, available: Offset, source: NestedScrollSource ): Offset { return if (source == NestedScrollSource.SideEffect) { available } else { Offset.Zero } } } }
Wszystkie wywołania zwrotne zawierają informacje o
NestedScrollSource
typie.
NestedScrollDispatcher
inicjuje cykl przewijania zagnieżdżonego. Użycie dyspozytora i wywołanie jego metod powoduje uruchomienie cyklu. Kontenery przewijania mają wbudowany dyspozytor, który wysyła do systemu zmiany przechwycone podczas gestów. Z tego powodu większość przypadków użycia dostosowywania przewijania zagnieżdżonego polega na użyciu NestedScrollConnection zamiast dyspozytora, aby reagować na już istniejące zmiany, a nie wysyłać nowe.
Więcej informacji o użyciu znajdziesz w
NestedScrollDispatcherSample.
Zmienianie rozmiaru obrazu podczas przewijania
Gdy użytkownik przewija, możesz utworzyć dynamiczny efekt wizualny, w którym rozmiar obrazu zmienia się w zależności od pozycji przewijania.
Zmienianie rozmiaru obrazu na podstawie pozycji przewijania
Ten fragment kodu pokazuje, jak zmienić rozmiar obrazu w LazyColumn na podstawie
pozycji przewijania pionowego. Obraz zmniejsza się, gdy użytkownik przewija w dół, i powiększa się, gdy przewija w górę, pozostając w zdefiniowanych granicach minimalnego i maksymalnego rozmiaru:
@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 } ) } }
Najważniejsze informacje o kodzie
- Ten kod używa
NestedScrollConnectiondo przechwytywania zdarzeń przewijania. onPreScrolloblicza zmianę rozmiaru obrazu na podstawie zmiany przewijania.- Zmienna stanu
currentImageSizeprzechowuje bieżący rozmiar obrazu, ograniczony przezminImageSizeimaxImageSize. imageScalepochodzi zcurrentImageSize. LazyColumnjest przesuwany na podstawiecurrentImageSize.Imageużywa modyfikatoragraphicsLayerdo zastosowania obliczonej skali.translationYwgraphicsLayerzapewnia, że obraz pozostaje wyśrodkowany pionowo podczas skalowania.
Wynik
Powyższy fragment kodu powoduje efekt skalowania obrazu podczas przewijania:
Współpraca przewijania zagnieżdżonego
Jeśli spróbujesz zagnieździć elementy View, które można przewijać, w elementach kompozycyjnych, które można przewijać, lub odwrotnie, możesz napotkać problemy. Najbardziej zauważalne problemy wystąpią, gdy przewiniesz element podrzędny i dotrzesz do jego początku lub końca, a element nadrzędny przejmie przewijanie. Jednak to oczekiwane zachowanie może nie wystąpić lub nie działać zgodnie z oczekiwaniami.
Ten problem wynika z oczekiwań wbudowanych w elementy kompozycyjne, które można przewijać.
Elementy kompozycyjne, które można przewijać, mają regułę "przewijania zagnieżdżonego domyślnie", co oznacza, że
każdy kontener przewijania musi uczestniczyć w łańcuchu przewijania zagnieżdżonego zarówno jako element
nadrzędny za pomocą
NestedScrollConnection,
jak i jako element podrzędny za pomocą
NestedScrollDispatcher.
Gdy element podrzędny znajduje się na granicy, będzie on sterować przewijaniem zagnieżdżonym elementu nadrzędnego. Na przykład ta reguła umożliwia prawidłowe współdziałanie Pager i LazyRow w Compose. Jednak w przypadku przewijania interoperacyjnego za pomocą ViewPager2 lub RecyclerView ciągłe przewijanie z elementu podrzędnego do nadrzędnego nie jest możliwe, ponieważ nie implementują one NestedScrollingParent3.
Aby włączyć interfejs API współpracy przewijania zagnieżdżonego między elementami View, które można przewijać, a elementami kompozycyjnymi, które można przewijać, zagnieżdżonymi w obu kierunkach, możesz użyć interfejsu API współpracy przewijania zagnieżdżonego, aby rozwiązać te problemy w tych scenariuszach.
Współpracujący element nadrzędny View zawierający element podrzędny ComposeView
Współpracujący element nadrzędny View to element, który implementuje już
NestedScrollingParent3
, a zatem może odbierać zmiany przewijania z współpracującego zagnieżdżonego
elementu kompozycyjnego podrzędnego. ComposeView będzie działać jako element podrzędny w tym przypadku i będzie
musiał (pośrednio) implementować
NestedScrollingChild3.
Przykładem współpracującego elementu nadrzędnego jest androidx.coordinatorlayout.widget.CoordinatorLayout.
Jeśli potrzebujesz współpracy przewijania zagnieżdżonego między kontenerami nadrzędnymi View, które można przewijać, a zagnieżdżonymi elementami kompozycyjnymi podrzędnymi, które można przewijać, możesz użyć
rememberNestedScrollInteropConnection().
rememberNestedScrollInteropConnection()
umożliwia i zapamiętuje
NestedScrollConnection
, które umożliwia współpracę przewijania zagnieżdżonego między elementem nadrzędnym View implementującym
NestedScrollingParent3
a elementem podrzędnym Compose. Należy go używać w połączeniu z a
nestedScroll
modyfikatorem. Ponieważ przewijanie zagnieżdżone jest domyślnie włączone po stronie Compose, możesz
użyć tego połączenia, aby włączyć przewijanie zagnieżdżone po stronie View i dodać
niezbędną logikę łączącą między Views a elementami kompozycyjnymi.
Częstym przypadkiem użycia jest użycie CoordinatorLayout, CollapsingToolbarLayout i elementu kompozycyjnego podrzędnego, jak pokazano 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 aktywności lub fragmencie musisz skonfigurować element kompozycyjny podrzędny i
wymagane
NestedScrollConnection:
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()) } } } } } } }
Element kompozycyjny nadrzędny zawierający element podrzędny AndroidView
Ten scenariusz obejmuje implementację interfejsu API współpracy przewijania zagnieżdżonego po stronie Compose – gdy masz element kompozycyjny nadrzędny zawierający element podrzędny AndroidView. AndroidView implementuje
NestedScrollDispatcher,
ponieważ działa jako element podrzędny elementu nadrzędnego przewijania Compose, a także
NestedScrollingParent3
, ponieważ działa jako element nadrzędny elementu podrzędnego przewijania View. Element nadrzędny Compose będzie wtedy mógł odbierać zmiany przewijania zagnieżdżonego z zagnieżdżonego elementu podrzędnego View, który można przewijać.
Poniższy przykład pokazuje, jak w tym scenariuszu można osiągnąć współpracę przewijania zagnieżdżonego, wraz ze zwijanym paskiem narzędzi Compose:
@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) { // ... } } // ... }
Ten przykład pokazuje, jak używać 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(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 współpracy przewijania zagnieżdżonego jest używany z
BottomSheetDialogFragment
aby osiągnąć prawidłowe zachowanie 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 przesyłanie zmian z poziomu Compose do poziomu View. Umożliwia to elementowi uczestniczenie w przewijaniu zagnieżdżonym, ale nie włącza automatycznie przewijania elementów. W przypadku elementów kompozycyjnych, których nie można przewijać automatycznie, takich jak Box czy Column, zmiany przewijania w takich komponentach nie będą propagowane w systemie przewijania zagnieżdżonego, a zmiany nie dotrą do NestedScrollConnection udostępnianego przez rememberNestedScrollInteropConnection(). Dlatego te zmiany nie dotrą do elementu nadrzędnego View. Aby rozwiązać ten problem, upewnij się, że do tych typów zagnieżdżonych elementów kompozycyjnych ustawisz też modyfikatory przewijania. Więcej informacji znajdziesz w poprzedniej sekcji dotyczącej przewijania zagnieżdżonego w celu uzyskania bardziej szczegółowych
informacji.
Niewspółpracujący element nadrzędny View zawierający element podrzędny ComposeView
Niewspółpracujący element to element, który nie implementuje niezbędnych interfejsów
NestedScrolling po stronie View. Oznacza to, że
współpraca przewijania zagnieżdżonego z tymi elementami Views nie działa od razu. Niewspółpracujące Views to RecyclerView i ViewPager2.
Dodatkowe materiały
- Tworzenie przycisku umożliwiającego przewijanie z przyciąganiem
- Tworzenie efektu przewijania paralaksy
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy język JavaScript jest wyłączony.
- Omówienie gestów
- Migracja
CoordinatorLayoutdo Compose - Używanie widoków w Compose