Przewijanie zagnieżdżone to system, w którym wiele komponentów przewijania zawartych w sobie nawzajem współpracuje ze sobą, reagując na pojedynczy gest przewijania i przekazując sobie informacje o zmianach przewijania.
System zagnieżdżonego przewijania 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 różnicami przewijania, które są propagowane i udostępniane między nimi.
Compose udostępnia wiele sposobów obsługi zagnieżdżonego przewijania między komponentami. Typowym przykładem zagnieżdżonego przewijania jest lista w innej liście, a bardziej złożonym przypadkiem jest zwijany pasek narzędzi.
Automatyczne zagnieżdżone przewijanie
Proste zagnieżdżone przewijanie nie wymaga od Ciebie żadnych działań. Gesty, które inicjują przewijanie, są automatycznie przekazywane z elementów podrzędnych do nadrzędnych. Oznacza to, że gdy element podrzędny nie może już przewijać, gest jest obsługiwany przez jego element nadrzędny.
Automatyczne zagnieżdżone przewijanie jest obsługiwane i dostępne od razu w przypadku niektórych komponentów i modyfikatorów Compose: verticalScroll, horizontalScroll, scrollable, interfejsy API Lazy i TextField. Oznacza to, że gdy użytkownik przewija wewnętrzny element podrzędny zagnieżdżonych komponentów, poprzednie modyfikatory propagują delty przewijania do elementów nadrzędnych, które obsługują zagnieżdżone przewijanie.
Poniższy przykład przedstawia 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 wieloma elementami, modyfikator nestedScroll
zapewni Ci większą elastyczność dzięki zdefiniowaniu zagnieżdżonej hierarchii przewijania. Jak wspomnieliśmy w poprzedniej sekcji, niektóre komponenty mają wbudowaną obsługę zagnieżdżonego przewijania. Jednak w przypadku komponentów kompozycyjnych, które nie są automatycznie przewijane, np. Box lub Column, delty przewijania w takich komponentach nie będą propagowane w systemie przewijania zagnieżdżonego i nie dotrą do komponentu 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 zagnieżdżonego przewijania
Zagnieżdżony cykl przewijania to przepływ wartości delta przewijania, które są wysyłane w górę i w dół drzewa hierarchii przez wszystkie komponenty (lub węzły) należące do zagnieżdżonego systemu przewijania, np. za pomocą komponentów i modyfikatorów z możliwością przewijania lub nestedScroll.
Fazy zagnieżdżonego cyklu przewijania
Gdy komponent z możliwością przewijania wykryje zdarzenie wywołujące (np. gest), zanim jeszcze zostanie wywołana rzeczywista czynność przewijania, wygenerowane wartości delta są wysyłane do zagnieżdżonego systemu przewijania i przechodzą przez 3 fazy: przed przewijaniem, wykorzystanie węzła i po przewijaniu.
W pierwszej fazie przed przewinięciem komponent, który otrzymał zdarzenie wywołujące, będzie wysyłać te zdarzenia w górę drzewa hierarchii do najwyższego elementu nadrzędnego. Zdarzenia delta będą się wtedy rozprzestrzeniać w dół, co oznacza, że będą przekazywane od nadrzędnego elementu głównego do elementu podrzędnego, który rozpoczął zagnieżdżony cykl przewijania.
Dzięki temu zagnieżdżone elementy przewijane (komponenty kompozycyjne korzystające z modyfikatorów nestedScroll lub scrollable) mogą wykonać pewne działania z wartością delta, zanim węzeł będzie mógł ją wykorzystać.
W fazie zużycia węzła sam węzeł wykorzysta wszystkie wartości delta, które nie zostały użyte przez jego węzły nadrzędne. Wtedy ruch przewijania jest faktycznie wykonywany i widoczny.
W tym czasie dziecko może przewinąć pozostałą część strony lub nie. Pozostałe elementy zostaną przesłane z powrotem, aby przejść etap po przewinięciu.
W fazie po przewinięciu wszystko, co nie zostało wykorzystane przez węzeł, zostanie ponownie przesłane do jego elementów nadrzędnych.
Faza po przewinięciu działa podobnie jak faza przed przewinięciem, w której każdy z elementów nadrzędnych może zdecydować, czy chce wykorzystać zdarzenie.
Podobnie jak w przypadku przewijania, gdy gest przeciągnięcia się zakończy, intencja użytkownika może zostać przełożona na szybkość, która jest używana do przesunięcia (przewijania za pomocą animacji) kontenera z możliwością przewijania. Szybkie przesunięcie jest też częścią zagnieżdżonego cyklu przewijania, a prędkości generowane przez zdarzenie przeciągania przechodzą przez podobne fazy: przed szybkim przesunięciem, wykorzystanie węzła i po szybkim przesunięciu. Pamiętaj, że animacja przesunięcia jest powiązana tylko z gestem dotykowym i nie jest wywoływana przez inne zdarzenia, takie jak przewijanie za pomocą funkcji ułatwień dostępu lub sprzętu.
Uczestniczenie w cyklu zagnieżdżonego przewijania
Udział w cyklu polega na przechwytywaniu, wykorzystywaniu i zgłaszaniu wykorzystania różnic wzdłuż hierarchii. Compose udostępnia zestaw narzędzi, które pozwalają wpływać na działanie zagnieżdżonego systemu przewijania i bezpośrednio z nim wchodzić w interakcje. Możesz na przykład wykonać pewne działania na różnicach przewijania, zanim komponent z możliwością przewijania zacznie się przewijać.
Jeśli zagnieżdżony cykl przewijania jest systemem działającym na łańcuchu węzłów, modyfikator nestedScroll umożliwia przechwytywanie i wstawianie zmian oraz wpływanie na dane (przyrosty przewijania) propagowane w łańcuchu. Ten modyfikator może być umieszczony w dowolnym miejscu w hierarchii i komunikuje się z zagnieżdżonymi instancjami modyfikatora przewijania 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 zagnieżdżonego cyklu przewijania i wpływanie na
zagnieżdżony system przewijania. Składa się z 4 metod wywołania zwrotnego, z których każda reprezentuje jedną z faz konsumpcji: przed/po przewinięciu i przed/po przesunięciu:
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 wartości delta:available wartość delta dla danego etapu i consumed wartość delta wykorzystana na poprzednich etapach. Jeśli w dowolnym momencie chcesz przestać propagować zmiany w górę hierarchii, możesz użyć do tego zagnieżdżonego połączenia przewijania:
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.
NestedScrollDispatcher
inicjuje zagnieżdżony cykl przewijania. Użycie dyspozytora i wywołanie jego metod
uruchamia cykl. Kontenery z możliwością przewijania mają wbudowany moduł wysyłający, który przekazuje do systemu różnice zarejestrowane podczas gestów. Dlatego w większości przypadków dostosowywania zagnieżdżonego przewijania używa się funkcji NestedScrollConnection zamiast dyspozytora, aby reagować na istniejące już delty, a nie wysyłać nowych.
Więcej informacji znajdziesz w sekcji NestedScrollDispatcherSample.
Zmienianie rozmiaru obrazu podczas przewijania
Podczas przewijania przez użytkownika 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 elemencie LazyColumn na podstawie pozycji przewijania w pionie. Obraz zmniejsza się, gdy użytkownik przewija stronę w dół, a powiększa, gdy przewija ją w górę, przy czym pozostaje w określonych 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 wartości delta przewijania.- Zmienna stanu
currentImageSizeprzechowuje bieżący rozmiar obrazu, który mieści się w zakresie odminImageSizedomaxImageSize. imageScalei jest wyliczany na podstawie zmiennejcurrentImageSize. - Wartości przesunięcia
LazyColumnsą oparte na wartościcurrentImageSize. - W przypadku
Imageużywany jest modyfikatorgraphicsLayer, aby zastosować obliczoną skalę. - Symbol
translationYwgraphicsLayerzapewnia, że obraz pozostanie wyśrodkowany w pionie podczas skalowania.
Wynik
Powyższy fragment kodu powoduje efekt skalowania obrazu podczas przewijania:
Interakcja z zagnieżdżonym przewijaniem
Jeśli spróbujesz zagnieździć elementy z możliwością przewijania View w komponentach z możliwością przewijania lub odwrotnie, możesz napotkać problemy. Najbardziej zauważalne przypadki wystąpią, gdy przewiniesz element podrzędny do początku lub końca i oczekujesz, że element nadrzędny przejmie przewijanie. Jednak to oczekiwane zachowanie może nie wystąpić lub może nie działać zgodnie z oczekiwaniami.
Ten problem wynika z oczekiwań wbudowanych w komponenty kompozycyjne z możliwością przewijania.
Komponenty z możliwością przewijania mają regułę „nested-scroll-by-default”, co oznacza, że każdy kontener z możliwością 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 osiągnie granicę, będzie powodować zagnieżdżone przewijanie elementu nadrzędnego. Na przykład ta reguła umożliwia prawidłowe współdziałanie Compose Pager i Compose LazyRow. Jednak w przypadku przewijania w ramach interoperacyjności za pomocą ViewPager2 lub RecyclerView, które nie implementują NestedScrollingParent3, ciągłe przewijanie z elementu podrzędnego do nadrzędnego nie jest możliwe.
Aby włączyć interfejs API współdziałania z zagnieżdżonym przewijaniem między elementami View z możliwością przewijania a komponentami z możliwością przewijania zagnieżdżonymi w obu kierunkach, możesz użyć interfejsu API współdziałania z zagnieżdżonym przewijaniem, aby rozwiązać te problemy w tych scenariuszach.
Współpracujący rodzic View zawierający dziecko ComposeView
Współpracujący element nadrzędny View to element, który implementuje już
NestedScrollingParent3
i dlatego może otrzymywać delty przewijania ze współpracującego zagnieżdżonego elementu podrzędnego
kompozycji. ComposeView zachowywałby się w tym przypadku jak dziecko i musiałby (pośrednio) zaimplementować NestedScrollingChild3.
Przykładem współpracującego rodzica jest
androidx.coordinatorlayout.widget.CoordinatorLayout.
Jeśli potrzebujesz interoperacyjności zagnieżdżonego przewijania między przewijanymi Viewkontenerami nadrzędnymi
a zagnieżdżonymi przewijanymi komponentami podrzędnymi, możesz użyć rememberNestedScrollInteropConnection().
rememberNestedScrollInteropConnection()
umożliwia i zapamiętuje
NestedScrollConnection
umożliwiające zagnieżdżone przewijanie między elementem nadrzędnym View, który implementuje
NestedScrollingParent3
a elementem podrzędnym Compose. Należy go używać w połączeniu z modyfikatorem nestedScroll. Przewijanie zagnieżdżone jest domyślnie włączone po stronie Compose, więc możesz użyć tego połączenia, aby włączyć przewijanie zagnieżdżone po stronie View i dodać niezbędną logikę łączącą Views z komponentami kompozycyjnymi.
Częstym przypadkiem użycia jest użycie funkcji CoordinatorLayout, CollapsingToolbarLayout i komponentu podrzędnego, co widać na 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 wymagany 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()) } } } } } } }
Funkcja kompozycyjna nadrzędna zawierająca funkcję kompozycyjną podrzędną AndroidView
Ten scenariusz obejmuje implementację zagnieżdżonego interfejsu API do przewijania na stronie Compose – gdy masz element kompozycyjny nadrzędny zawierający element podrzędny AndroidView. Element AndroidView implementuje interfejs NestedScrollDispatcher, ponieważ jest elementem podrzędnym przewijanego elementu nadrzędnego Compose, a także interfejs NestedScrollingParent3, ponieważ jest elementem nadrzędnym przewijanego elementu podrzędnego View. Komponent nadrzędny będzie wtedy mógł otrzymywać zagnieżdżone delty przewijania z zagnieżdżonego komponentu podrzędnego z możliwością przewijaniaView.
Poniższy przykład pokazuje, jak w tym scenariuszu można osiągnąć współdziałanie zagnieżdżonego przewijania wraz z rozwijanym 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(
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(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)
}
}
)
}
}
Ten przykład pokazuje, jak interfejs API współdziałania z zagnieżdżonym przewijaniem jest używany z BottomSheetDialogFragment, aby uzyskać prawidłowe działanie funkcji 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 na poziom View. Umożliwia to elementowi uczestniczenie w przewijaniu zagnieżdżonym, ale nie włącza automatycznego przewijania elementów. W przypadku komponentów kompozycyjnych, które nie są przewijane automatycznie, np. Box lub Column, różnice przewijania w takich komponentach nie będą propagowane w systemie przewijania zagnieżdżonego, a różnice nie dotrą do elementu NestedScrollConnection udostępnianego przez rememberNestedScrollInteropConnection(), dlatego nie dotrą do komponentu nadrzędnego View. Aby rozwiązać ten problem, ustaw też modyfikatory z możliwością przewijania dla tych typów zagnieżdżonych funkcji kompozycyjnych. Więcej informacji znajdziesz w poprzedniej sekcji dotyczącej zagnieżdżonego przewijania.
Rodzic, który nie współpracuje View, zawierający dziecko ComposeView
Widok nieobsługujący to taki, który nie implementuje niezbędnych interfejsów po stronie View.NestedScrolling Oznacza to, że zagnieżdżone przewijanie nie działa od razu z tymi Views. Nie współpracujące Views to RecyclerView i ViewPager2.
Dodatkowe materiały
- Tworzenie przycisku włączającego przewijanie z przyciąganiem
- Tworzenie efektu przewijania z paralaksą
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy język JavaScript jest wyłączony.
- Informacje o gestach
- Migracja z
CoordinatorLayoutdo Compose - Używanie widoków w Compose