No Jetpack Compose, scrollable2D e draggable2D são
modificadores de baixo nível projetados para processar entradas de ponteiro em duas dimensões. Enquanto
os modificadores 1D padrão scrollable e draggable são
restritos a uma única orientação, as variantes 2D rastreiam o movimento nos eixos
X e Y simultaneamente.
Por exemplo, o modificador scrollable atual é usado para rolagem e deslizamento
em uma única orientação, enquanto scrollable2d é usado para rolagem e deslizamento
em 2D. Isso permite criar layouts mais complexos que se movem em todas
as direções, como planilhas ou visualizadores de imagens. O modificador scrollable2d
também oferece suporte à rolagem aninhada em cenários 2D.
Escolha scrollable2D ou draggable2D.
A escolha da API certa depende dos elementos da interface que você quer mover e do comportamento físico preferido para esses elementos.
Modifier.scrollable2D: use esse modificador em um contêiner para mover o conteúdo dentro dele. Por exemplo, use-o com mapas, planilhas ou visualizadores de fotos, em que o conteúdo do contêiner precisa ser rolado nas direções horizontal e vertical. Ele inclui suporte integrado para rolagem rápida, para que o conteúdo continue se movendo após um deslize, e se coordena com outros componentes de rolagem na página.
Modifier.draggable2D: use esse modificador para mover um componente. É um modificador leve, então o movimento para exatamente quando o dedo do usuário para. Ele não inclui suporte para gestos de deslizar.
Se você quiser tornar um componente arrastável, mas não precisar de suporte para rolagem rápida ou
aninhada, use draggable2D.
Implementar modificadores 2D
As seções a seguir mostram exemplos de como usar os modificadores 2D.
Implemente o Modifier.scrollable2D.
Use esse modificador para contêineres em que o usuário precisa mover o conteúdo em todas as direções.
Capturar dados de movimento em 2D
Este exemplo mostra como capturar dados brutos de movimento 2D e exibir o deslocamento 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()}", // ... ) } } } }
O snippet anterior faz o seguinte:
- Usa
offsetcomo um estado que contém a distância total que o usuário rolou. - Em
rememberScrollable2DState, uma função lambda é definida para processar cada delta gerado pelo dedo do usuário. O códigooffset.value += deltaatualiza o estado manual com a nova posição. - Os componentes
Textmostram os valores X e Y atuais desse estadooffset, que são atualizados em tempo real conforme o usuário arrasta.
Mover uma janela de visualização grande
Este exemplo mostra como usar dados roláveis 2D capturados e aplicar um
translationX e um translationY ao conteúdo maior que o contêiner
pai:
@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.O snippet anterior inclui o seguinte:
- O contêiner é definido com um tamanho fixo (
600x400dp), enquanto o conteúdo recebe um tamanho muito maior (1200x800dp) para evitar que seja redimensionado para o tamanho do pai. - O modificador
clipToBounds()no contêiner garante que qualquer parte do conteúdo grande que esteja fora da caixa600x400fique oculta. - Ao contrário de componentes de alto nível, como
LazyColumn, oscrollable2Dnão move o conteúdo automaticamente. Em vez disso, é necessário aplicar ooffsetrastreado ao conteúdo, usando transformaçõesgraphicsLayerou deslocamentos de layout. - Dentro do bloco
graphicsLayer,translationX = offset.value.xetranslationY = offset.value.ymudam a posição de desenho da imagem ou do texto com base no movimento do dedo, criando o efeito visual de rolagem.
Implementar a rolagem aninhada com scrollable2D
Este exemplo demonstra como um componente bidirecional pode ser integrado a um pai unidimensional padrão, como um feed de notícias vertical.
Considere os seguintes pontos ao implementar a rolagem aninhada:
- A lambda para
rememberScrollable2DStateprecisa retornar apenas o delta consumido para permitir que a lista mãe assuma o controle naturalmente quando a filha atingir o limite. - Quando um usuário faz um gesto de deslizar diagonal, a velocidade 2D é compartilhada. Se o filho atingir um limite durante a animação, o momento restante será propagado para o pai para continuar a rolagem naturalmente.
@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) ) } }
No snippet anterior:
- O componente 2D pode consumir o movimento do eixo X para deslocar internamente e, ao mesmo tempo, enviar o movimento do eixo Y para a lista pai quando os limites verticais do filho forem atingidos.
- Em vez de prender o usuário na superfície 2D, o sistema calcula o delta consumido e transmite o restante para cima na hierarquia. Isso garante que o usuário possa continuar rolando o restante da página sem levantar o dedo.
Implemente o Modifier.draggable2D.
Para saber como usar o Modifier.draggable2D, consulte Arrastar em duas
dimensões com o Modifier.draggable2D.