In Jetpack Compose, scrollable2D e draggable2D sono
modificatori di basso livello progettati per gestire l'input del puntatore in due dimensioni. Mentre
i modificatori 1D standard scrollable e draggable sono
limitati a un singolo orientamento, le varianti 2D tengono traccia del movimento simultaneamente su entrambi
gli assi X e Y.
Ad esempio, il modificatore scrollable esistente viene utilizzato per lo scorrimento e lo scorrimento rapido a orientamento singolo, mentre scrollable2d viene utilizzato per lo scorrimento e lo scorrimento rapido
in 2D. In questo modo, puoi creare layout più complessi che si muovono in tutte le direzioni, come fogli di lavoro o visualizzatori di immagini. Il modificatore scrollable2d supporta anche lo scorrimento nidificato negli scenari 2D.
Scegliere scrollable2D o draggable2D
La scelta dell'API giusta dipende dagli elementi dell'interfaccia utente che vuoi spostare e dal comportamento fisico preferito per questi elementi.
Modifier.scrollable2D: utilizza questo modificatore su un contenitore per spostare i contenuti al suo interno. Ad esempio, utilizzalo con mappe, fogli di lavoro o visualizzazione Foto, dove i contenuti del contenitore devono scorrere sia in orizzontale che in verticale. Include il supporto integrato per lo scorrimento rapido, in modo che i contenuti continuino a muoversi dopo uno scorrimento, e si coordina con altri componenti di scorrimento nella pagina.
Modifier.draggable2D: utilizza questo modificatore per spostare un componente stesso. È un modificatore leggero, quindi il movimento si interrompe esattamente quando il dito dell'utente si ferma. Non include il supporto per lo scorrimento rapido.
Se vuoi rendere un componente trascinabile, ma non hai bisogno del supporto per lo scorrimento rapido o nidificato, utilizza draggable2D.
Implementare modificatori 2D
Le sezioni seguenti forniscono esempi per mostrare come utilizzare i modificatori 2D.
Implementare Modifier.scrollable2D
Utilizza questo modificatore per i contenitori in cui l'utente deve spostare i contenuti in tutte le direzioni.
Acquisire dati di movimento 2D
Questo esempio mostra come acquisire dati di movimento 2D non elaborati e visualizzare l'offset 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()}", // ... ) } } } }
Lo snippet precedente esegue le seguenti operazioni:
- Utilizza
offsetcome stato che contiene la distanza totale percorsa dall'utente. - All'interno di
rememberScrollable2DState, viene definita una funzione lambda per gestire ogni delta, generato dal dito dell'utente. Il codiceoffset.value += deltaaggiorna lo stato manuale con la nuova posizione. - I componenti
Textvisualizzano i valori X e Y correnti dello statooffset, che si aggiornano in tempo reale mentre l'utente trascina.
Eseguire la panoramica di un'ampia area visibile
Questo esempio mostra come utilizzare i dati scorrevoli 2D acquisiti e applicare una translationX e una translationY ai contenuti più grandi del contenitore principale:
@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.Lo snippet precedente include quanto segue:
- Il contenitore è impostato su una dimensione fissa (
600x400dp), mentre ai contenuti viene assegnata una dimensione molto più grande (1200x800dp) per evitare che vengano ridimensionati in base alla dimensione del contenitore principale. - Il modificatore
clipToBounds()sul contenitore garantisce che qualsiasi parte dei contenuti di grandi dimensioni che si trova all'esterno della casella600x400sia nascosta alla visualizzazione. - A differenza dei componenti di alto livello come
LazyColumn,scrollable2Dnon sposta automaticamente i contenuti. Devi invece applicare l'offsetmonitorato ai contenuti, utilizzando le trasformazionigraphicsLayero gli offset di layout. - All'interno del blocco
graphicsLayer,translationX = offset.value.xetranslationY = offset.value.yspostano la posizione di disegno dell'immagine o del testo in base al movimento del dito, creando l'effetto visivo dello scorrimento.
Implementare lo scorrimento nidificato con scrollable2D
Questo esempio mostra come un componente bidirezionale può essere integrato in un componente principale unidimensionale standard, come un feed di notizie verticale.
Quando implementi lo scorrimento nidificato, tieni presente quanto segue:
- La lambda per
rememberScrollable2DStatedeve restituire solo il delta consumato, per consentire alla lista principale di assumere il controllo in modo naturale quando il componente secondario raggiunge il suo limite. - Quando un utente esegue uno scorrimento rapido diagonale, la velocità 2D viene condivisa. Se il componente secondario raggiunge un limite durante l'animazione, lo slancio rimanente viene propagato al componente principale per continuare lo scorrimento in modo naturale.
@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) ) } }
Nello snippet precedente:
- Il componente 2D può consumare il movimento dell'asse X per eseguire la panoramica internamente, inviando contemporaneamente il movimento dell'asse Y all'elenco principale una volta raggiunti i limiti verticali del componente secondario.
- Anziché intrappolare l'utente all'interno della superficie 2D, il sistema calcola il delta consumato e passa il resto alla gerarchia. In questo modo, l'utente può continuare a scorrere il resto della pagina senza sollevare il dito.
Implementare Modifier.draggable2D
Per informazioni su come utilizzare Modifier.draggable2D, consulta Trascinare in due
dimensioni con Modifier.draggable2D.