W Jetpack Compose scrollable2D i draggable2D to modyfikatory niskiego poziomu przeznaczone do obsługi danych wejściowych ze wskaźnika w 2 wymiarach. Standardowe modyfikatory 1D scrollable i draggable są ograniczone do jednej orientacji, natomiast warianty 2D śledzą ruch jednocześnie na osiach X i Y.
Na przykład istniejący modyfikator scrollable jest używany do przewijania i przesuwania w jednej orientacji, a scrollable2d – do przewijania i przesuwania w 2D. Dzięki temu możesz tworzyć bardziej złożone układy, które poruszają się we wszystkich kierunkach, np. arkusze kalkulacyjne lub przeglądarki obrazów. Modyfikator scrollable2d obsługuje też zagnieżdżone przewijanie w scenariuszach 2D.
Wybierz scrollable2D lub draggable2D.
Wybór odpowiedniego interfejsu API zależy od elementów interfejsu, które chcesz przenieść, oraz preferowanego zachowania fizycznego tych elementów.
Modifier.scrollable2D: użyj tego modyfikatora w kontenerze, aby przenieść zawartość do jego wnętrza. Możesz go używać na przykład w przypadku Map, arkuszy kalkulacyjnych lub widoków Zdjęcia, gdzie zawartość kontenera musi być przewijana zarówno w poziomie, jak i w pionie. Ma wbudowaną obsługę rzutu, dzięki czemu treść nadal się porusza po przesunięciu palcem, i jest skoordynowany z innymi komponentami przewijania na stronie.
Modifier.draggable2D: użyj tego modyfikatora, aby przenieść sam komponent. Jest to lekki modyfikator, więc ruch zatrzymuje się dokładnie wtedy, gdy użytkownik przestaje przesuwać palcem. Nie obejmuje obsługi rzucania.
Jeśli chcesz, aby komponent można było przeciągać, ale nie potrzebujesz obsługi rzutu ani przewijania zagnieżdżonego, użyj draggable2D.
Wdrażanie modyfikatorów 2D
W sekcjach poniżej znajdziesz przykłady użycia modyfikatorów 2D.
Wdrożenie: Modifier.scrollable2D
Używaj tego modyfikatora w przypadku kontenerów, w których użytkownik musi przesuwać zawartość we wszystkich kierunkach.
Przechwytywanie danych o ruchu 2D
Ten przykład pokazuje, jak przechwytywać nieprzetworzone dane o ruchu 2D i wyświetlać przesunięcie X,Y:
@Composable private fun Scrollable2DSample() { // 1. Manually track the total distance the user has moved in both X and Y directions var offset by remember { mutableStateOf(Offset.Zero) } Box( modifier = Modifier .fillMaxSize() // ... contentAlignment = Alignment.Center ) { Box( modifier = Modifier .size(200.dp) // 2. Attach the 2D scroll logic to capture XY movement deltas .scrollable2D( state = rememberScrollable2DState { delta -> // 3. Update the cumulative offset state with the new movement delta offset += delta // Return the delta to indicate the entire movement was handled by this box delta } ) // ... contentAlignment = Alignment.Center ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { // 4. Display the current X and Y values from the offset state in real-time Text( text = "X: ${offset.x.roundToInt()}", // ... ) Spacer(modifier = Modifier.height(8.dp)) Text( text = "Y: ${offset.y.roundToInt()}", // ... ) } } } }
Poprzedni fragment kodu wykonuje te czynności:
- Używa
offsetjako stanu, który przechowuje łączną odległość przewiniętą przez użytkownika. - Wewnątrz
rememberScrollable2DStatezdefiniowana jest funkcja lambda, która obsługuje każdą zmianę wygenerowaną przez palec użytkownika. Kodoffset.value += deltaaktualizuje stan ręczny, podając nową pozycję. - Komponenty
Textwyświetlają bieżące wartości X i Y stanuoffset, które są aktualizowane w czasie rzeczywistym, gdy użytkownik przeciąga element.
Przesuwanie dużego widocznego obszaru
Ten przykład pokazuje, jak używać przechwyconych danych przewijanych w 2D i stosować translationX i translationY do treści, które są większe niż ich kontener nadrzędny:
@Composable private fun Panning2DImage() { // Manually track the total distance the user has moved in both X and Y directions val offset = remember { mutableStateOf(Offset.Zero) } // Define how gestures are captured. The lambda is called for every finger movement val scrollState = rememberScrollable2DState { delta -> offset.value += delta delta } // The Viewport (Container): A fixed-size box that acts as a window into the larger content Box( modifier = Modifier .size(600.dp, 400.dp) // The visible area dimensions // ... // Hide any parts of the large content that sit outside this container's boundaries .clipToBounds() // Apply the 2D scroll modifier to intercept touch and fling gestures in all directions .scrollable2D(state = scrollState), contentAlignment = Alignment.Center, ) { // The Content: An image given a much larger size than the container viewport Image( painter = painterResource(R.drawable.cheese_5), contentDescription = null, modifier = Modifier .requiredSize(1200.dp, 800.dp) // Manual Scroll Effect: Since scrollable2D doesn't move content automatically, // we use graphicsLayer to shift the drawing position based on the tracked offset. .graphicsLayer { translationX = offset.value.x translationY = offset.value.y }, contentScale = ContentScale.FillBounds ) } }
Modifier.scrollable2D.Modifier.scrollable2D.Powyższy fragment kodu zawiera te elementy:
- Kontener ma stały rozmiar (
600x400dp), a treść ma znacznie większy rozmiar (1200x800dp), aby uniknąć zmiany rozmiaru na rozmiar kontenera nadrzędnego. - Modyfikator
clipToBounds()w kontenerze sprawia, że każda część dużej zawartości, która znajduje się poza polem600x400, jest ukryta. - W przeciwieństwie do komponentów wysokiego poziomu, takich jak
LazyColumn,scrollable2Dnie przenosi treści automatycznie. Zamiast tego musisz zastosować śledzony elementoffsetdo treści, używając przekształceńgraphicsLayerlub przesunięć układu. - Wewnątrz bloku
graphicsLayerklockitranslationX = offset.value.xitranslationY = offset.value.yprzesuwają pozycję rysowania obrazu lub tekstu na podstawie ruchu palca, tworząc efekt wizualny przewijania.
Implementowanie przewijania zagnieżdżonego za pomocą scrollable2D
Ten przykład pokazuje, jak komponent dwukierunkowy można zintegrować ze standardowym jednowymiarowym elementem nadrzędnym, takim jak pionowy kanał wiadomości.
Podczas wdrażania zagnieżdżonego przewijania pamiętaj o tych kwestiach:
- Funkcja lambda dla
rememberScrollable2DStatepowinna zwracać tylko wykorzystaną deltę, aby lista nadrzędna mogła przejąć kontrolę w naturalny sposób, gdy lista podrzędna osiągnie limit. - Gdy użytkownik wykonuje ruch po przekątnej, udostępniana jest prędkość 2D. Jeśli podczas animacji element podrzędny osiągnie granicę, pozostały pęd jest przekazywany do elementu nadrzędnego, aby kontynuować przewijanie w naturalny sposób.
@Composable private fun NestedScrollable2DSample() { var offset by remember { mutableStateOf(Offset.Zero) } val maxScrollDp = 250.dp val maxScrollPx = with(LocalDensity.current) { maxScrollDp.toPx() } Column( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) .background(Color(0xFFF5F5F5)), horizontalAlignment = Alignment.CenterHorizontally ) { Text( "Scroll down to find the 2D Box", modifier = Modifier.padding(top = 100.dp, bottom = 500.dp), style = TextStyle(fontSize = 18.sp, color = Color.Gray) ) // The Child: A 2D scrollable box with nested scroll coordination Box( modifier = Modifier .size(250.dp) .scrollable2D( state = rememberScrollable2DState { delta -> val oldOffset = offset // Calculate new potential offset and clamp it to our boundaries val newX = (oldOffset.x + delta.x).coerceIn(-maxScrollPx, maxScrollPx) val newY = (oldOffset.y + delta.y).coerceIn(-maxScrollPx, maxScrollPx) val newOffset = Offset(newX, newY) // Calculate exactly how much was consumed by the child val consumed = newOffset - oldOffset offset = newOffset // IMPORTANT: Return ONLY the consumed delta. // The remaining (unconsumed) delta propagates to the parent Column. consumed } ) // ... contentAlignment = Alignment.Center ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { val density = LocalDensity.current Text("2D Panning Zone", color = Color.White.copy(alpha = 0.7f), fontSize = 12.sp) Spacer(Modifier.height(8.dp)) Text("X: ${with(density) { offset.x.toDp().value.roundToInt() }}dp", color = Color.White, fontWeight = FontWeight.Bold) Text("Y: ${with(density) { offset.y.toDp().value.roundToInt() }}dp", color = Color.White, fontWeight = FontWeight.Bold) } } Text( "Once the Purple Box hits Y: 250 or -250,\nthis parent list will take over the vertical scroll.", textAlign = TextAlign.Center, modifier = Modifier.padding(top = 40.dp, bottom = 800.dp), style = TextStyle(fontSize = 14.sp, color = Color.Gray) ) } }
W powyższym fragmencie kodu:
- Komponent 2D może wykorzystywać ruch w osi X do przesuwania wewnętrznego, a jednocześnie wysyłać ruch w osi Y do listy nadrzędnej po osiągnięciu własnych granic pionowych.
- Zamiast zatrzymywać użytkownika na powierzchni 2D, system oblicza wykorzystaną deltę i przekazuje pozostałą część w górę hierarchii. Dzięki temu użytkownik może kontynuować przewijanie pozostałej części strony bez odrywania palca.
Wdrożenie: Modifier.draggable2D
Informacje o tym, jak używać Modifier.draggable2D, znajdziesz w artykule Przeciąganie w 2 wymiarach za pomocą Modifier.draggable2D.