Permitir que os usuários arrastem fisicamente componentes em uma tela é fundamental para apps Android modernos. Se você estiver criando um controle deslizante personalizado, um componente de deslizar para dispensar ou uma tela interativa, o Jetpack Compose oferece um conjunto abrangente de APIs para processar eventos de arrastar com facilidade.
As APIs de arrastar do Compose abstraem a complexidade dos eventos de toque brutos.
Em vez de calcular manualmente as coordenadas de toque e gerenciar os ciclos de vida de gestos com pointerInput, você pode usar modificadores integrados que se vinculam diretamente aos sistemas de gerenciamento de estado e animação do Compose.
Esta página descreve como e quando usar cada API de arrastar.
Escolher a API de arrastar correta
Há várias APIs que podem ser usadas para implementar conteúdo arrastável.
- Escolha componentes padrão do Material (como
SwipeToDismissBoxou gavetas de navegação) quando: você precisar do comportamento padrão de deslizar ou arrastar do Material 3 (por exemplo, excluir um e-mail, arquivar uma tarefa ou abrir um menu lateral / páginas inferiores). Eles oferecem comportamentos integrados sem exigir gerenciamento de estado ou layout personalizado. - Escolha
Modifier.draggablequando: você só precisar de deltas de arrastar brutos (por exemplo, arrastar um controle deslizante personalizado). Para mais informações, consulte Arrastar ao longo de um único eixo com arrastável.- Escolha
Modifier.draggable2Dquando: você precisar de um componente para se mover continuamente nos eixos X e Y simultaneamente (por exemplo, fazer uma panorâmica em um mapa ou criar um botão de ação flutuante que o usuário possa arrastar para qualquer lugar na tela). Para mais informações, consulte Arrastar em duas dimensões comModifier.draggable2D.
- Escolha
- Escolha
Modifier.anchoredDraggablequando: você estiver criando componentes interativos personalizados que precisam se ajustar entre duas ou mais posições discretas (por exemplo, uma página inferior, uma gaveta de navegação ou uma ação de deslizar para revelar). Para mais informações, consulte Arrastar componentes entre pontos de ancoragem comanchoredDraggable.
Arrastar ao longo de um único eixo com draggable
O modificador draggable é o ponto de entrada de alto nível para gestos de arrastar em
uma única orientação e informa a distância da ação de arrastar em pixels.
É importante observar que esse modificador é semelhante a scrollable, porque ele detecta apenas o gesto. Você precisa manter o estado e representá-lo na tela
por exemplo, movendo o elemento pelo offset modificador:
@Composable private fun DraggableText() { var offsetX by remember { mutableFloatStateOf(0f) } Text( modifier = Modifier .offset { IntOffset(offsetX.roundToInt(), 0) } .draggable( orientation = Orientation.Horizontal, state = rememberDraggableState { delta -> offsetX += delta } ), text = "Drag me!" ) }
Se você precisar controlar todo o gesto de arrastar, considere usar o detector de gestos de arrastar
em vez disso, pelo pointerInput modificador.
@Composable private fun DraggableTextLowLevel() { Box(modifier = Modifier.fillMaxSize()) { var offsetX by remember { mutableFloatStateOf(0f) } var offsetY by remember { mutableFloatStateOf(0f) } Box( Modifier .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) } .background(Color.Blue) .size(50.dp) .pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consume() offsetX += dragAmount.x offsetY += dragAmount.y } } ) } }
Arrastar componentes entre pontos de ancoragem com anchoredDraggable
O modificador anchoredDraggable permite arrastar um componente entre um conjunto de
posições predefinidas chamadas de pontos de ancoragem. Quando o usuário libera o gesto de arrastar, o componente se instala na âncora mais próxima. anchoredDraggable é útil para criar componentes como páginas inferiores personalizadas, controles deslizantes complexos ou painéis de gaveta.
Superfície da API
A API AnchoredDraggable consiste nas seguintes partes principais:
- Âncoras (
DraggableAnchors): um builder que define o mapeamento entre seus estados personalizados (por exemplo, uma classe enum paraStart, Center, End) e os valores de deslocamento de pixel correspondentes. - Estado (
AnchoredDraggableState<T>): um detentor de estado que gerencia a posição atual do componente (deslocamento), a âncora em que ele está instalado e o progresso de arrastar. Ele precisa ser criado usandorememberSaveableeAnchoredDraggableState.Saver<T>para preservar o estado em mudanças de configuração - Modificador de gesto (
Modifier.anchoredDraggable): um modificador que detecta gestos de arrastar ao longo de umaOrientation, atualiza o estado com o delta de arrastar e processa o gesto de deslizar para animar o componente até a âncora mais próxima após a liberação.
Exemplo: criar um componente SwipeToDismiss personalizado
Este exemplo demonstra como criar um componente SwipeToDismiss personalizado (um banner branco) que o usuário pode deslizar para a esquerda para dispensar ou arrastar para a direita para ver um efeito de rolagem esticada.
Definir valores de âncora. Defina um enum que represente as posições discretas em que o conteúdo arrastável pode ser instalado:
private enum class AnchoredDraggableSwipeDismissValue { DismissedStart, Resting }
Instanciar e salvar
AnchoredDraggableState. UserememberSaveablecomAnchoredDraggableState.Saver()para persistir a posição atual do componente em toda a recriação de atividades e mudanças de configuração (por exemplo, rotação da tela ou mudanças de tema):val state = rememberSaveable(saver = AnchoredDraggableState.Saver()) { AnchoredDraggableState(initialValue = AnchoredDraggableSwipeDismissValue.Resting) }
Aplicar os modificadores
layout,offsetedraggable. Para tornar o componente arrastável e garantir que os pontos de ancoragem se adaptem ao tamanho dele, aplique os seguintes modificadores ao conteúdo que precisa se mover visualmente:Modifier.layout: as posições de ancoragem geralmente dependem do tamanho de um componente, que não é conhecido durante a composição inicial. Use o modificadorlayoutpara medir o componente, calcular o mapaDraggableAnchorsdinamicamente com base na largura dele e atualizar o estado usandostate.updateAnchors().
Modifier.anchoredDraggable: conecta-se ao estado, envia deltas de arrastar na orientação especificada e processa o gesto de deslizar para animar o componente até a âncora mais próxima quando liberado. Adicione esse modificador ao componente que precisa responder a gestos de arrastar e transmita ostatee aorientationde arrastar (e, opcionalmente,overscrollEffect).Modifier.offset: lê o deslocamento destate.requireOffset()dentro da lambda de deslocamento. A leitura do deslocamento nessa lambda garante que o valor seja lido apenas durante a fase de posicionamento, evitando recomposições desnecessárias durante o arrastar. Aplique esse modificador ao conteúdo que precisa se mover visualmente.
Box( modifier = Modifier .fillMaxWidth() // Read state.requireOffset() inside the offset lambda to defer reading to the placement phase .offset { IntOffset(state.requireOffset().roundToInt(), 0) } .layout { measurable, constraints -> val placeable = measurable.measure(constraints) val anchors = DraggableAnchors { AnchoredDraggableSwipeDismissValue.DismissedStart at -placeable.width.toFloat() AnchoredDraggableSwipeDismissValue.Resting at 0f } state.updateAnchors(anchors) layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } } // Pass overscrollEffect to anchoredDraggable so deltas beyond the bounds (min and max anchor) are dispatched to overscrollEffect, and render the visual effect with Modifier.overscroll. .overscroll(overscrollEffect) .anchoredDraggable( state, Orientation.Horizontal, overscrollEffect = overscrollEffect ) ) { dismissContent() }
[Opcional] Integrar a rolagem esticada. A API
overscrollEffectimplementa um efeito visual que aparece quando as bordas de um contêiner de rolagem são alcançadas com uma rolagem ou deslize. Você pode fornecer feedback visual quando um usuário arrasta além dos limites de ancoragem definidos com um efeito de rolagem esticada (normalmente um alongamento ou brilho).- Enviar: transmita uma instância
overscrollEffectpara oanchoredDraggablemodificador. Isso envia deltas de arrastar para o estadoAnchoredDraggableou para o efeito de rolagem esticada, dependendo se o gesto está dentro ou fora dos limites da âncora. - Renderizar: aplique o
Modifier.overscrollcom a mesmaOverscrollEffectinstância ao componente que precisa renderizar o efeito visualmente.
- Enviar: transmita uma instância
O snippet a seguir mostra como adicionar um efeito de alongamento padrão:
val overscrollEffect = rememberOverscrollEffect() Box( modifier.fillMaxWidth() ) { Box(Modifier.fillMaxWidth()) { background() } Box( modifier = Modifier .fillMaxWidth() // Read state.requireOffset() inside the offset lambda to defer reading to the placement phase .offset { IntOffset(state.requireOffset().roundToInt(), 0) } .layout { measurable, constraints -> val placeable = measurable.measure(constraints) val anchors = DraggableAnchors { AnchoredDraggableSwipeDismissValue.DismissedStart at -placeable.width.toFloat() AnchoredDraggableSwipeDismissValue.Resting at 0f } state.updateAnchors(anchors) layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } } // Pass overscrollEffect to anchoredDraggable so deltas beyond the bounds (min and max anchor) are dispatched to overscrollEffect, and render the visual effect with Modifier.overscroll. .overscroll(overscrollEffect) .anchoredDraggable( state, Orientation.Horizontal, overscrollEffect = overscrollEffect ) ) { dismissContent() } }
Resultado
O snippet de código anterior resulta em um banner branco, com o texto "Drag me to dismiss!", que o usuário pode deslizar para a esquerda para dispensar (ou arrastar para a direita para ver o efeito de alongamento de rolagem):
Exemplo: criar uma gaveta de navegação personalizada orientada por gestos
Este exemplo demonstra como criar uma gaveta de navegação personalizada orientada por gestos. À medida que o usuário arrasta horizontalmente, a tela de primeiro plano (ScreenContents) desliza para a direita, é reduzida de 1.0f para 0.8f e arredonda os cantos para revelar a gaveta de navegação subjacente.
Definir estados e posições de ancoragem. Neste exemplo, as âncoras são os estados aberto e fechado da gaveta. Defina um enum para os estados
OpeneClosede mapeie-os para deslocamentos de pixel usandoDraggableAnchors:enum class DrawerState { Open, Closed } val anchors = DraggableAnchors { DrawerState.Open at drawerWidth DrawerState.Closed at 0f } val state = remember { AnchoredDraggableState( initialValue = DrawerState.Closed, anchors = anchors, ) }
Animar
anchoredDraggableStateao clicar em um item. ColoqueHomeScreenDrawerna parte de baixo daBoxeScreenContentsna parte de cima. A gaveta permanece estacionária, enquanto a camada superior é traduzida e dimensionada sobre ela.Quando um usuário seleciona um item de gaveta, atualize o estado da tela ativa e dispense automaticamente a gaveta usando
state.animateTo(DrawerState.Closed):@Composable private fun AnchoredDraggableDrawerOnClickSample( modifier: Modifier = Modifier, state: AnchoredDraggableState<DrawerState>, coroutineScope: CoroutineScope ) { var screenState by remember { mutableStateOf(ScreenState.Home) } Box(modifier = modifier.fillMaxSize()) { HomeScreenDrawer( selectedScreen = screenState, onScreenSelected = { screen -> screenState = screen // Dismiss drawer when a destination is selected coroutineScope.launch { state.animateTo(DrawerState.Closed) } } ) // ScreenContents(...) } }
Use
state.requireOffset()para mudar várias propriedades visuais. Em vez de apenas deslocar o conteúdo, você pode mudar várias propriedades comModifier.graphicsLayerpara animar simultaneamente a tradução, a escala e o raio do canto com base no progresso de arrastar:- Use
state.requireOffset()para receber o deslocamento de arrastar atual. - Use
lerp()(interpolação linear) para calcular a escala proporcional (1fpara0.8f) e o raio do canto (0.dppara32.dp) em relação ao deslocamento e à largura da gaveta. Modifier.graphicsLayerexecuta mudanças na fase de desenho, garantindo animações de alto desempenho sem acionar recomposições durante o arrastar.
@Composable private fun AnchoredDraggableDrawerGraphicsLayerSample( state: AnchoredDraggableState<DrawerState>, drawerWidth: Float, selectedScreen: ScreenState, onDrawerClicked: () -> Unit ) { ScreenContents( selectedScreen = selectedScreen, onDrawerClicked = onDrawerClicked, modifier = Modifier .graphicsLayer { this.translationX = state.requireOffset() val scale = lerp(1f, 0.8f, state.requireOffset() / drawerWidth) this.scaleX = scale this.scaleY = scale val cornerRadius = lerp(0f, 32f, state.requireOffset() / drawerWidth) this.shape = RoundedCornerShape(cornerRadius.dp) this.clip = true } .anchoredDraggable(state, Orientation.Horizontal) ) }
- Use
Acionar animações programáticas com
animateTo. Controle a gaveta de maneira programática (por exemplo, ao tocar no ícone de menu ou selecionar um item de gaveta) iniciandostate.animateTo()em um escopo de corrotina:fun toggleDrawerState( state: AnchoredDraggableState<DrawerState>, coroutineScope: CoroutineScope ) { coroutineScope.launch { if (state.currentValue == DrawerState.Open) { state.animateTo(DrawerState.Closed) } else { state.animateTo(DrawerState.Open) } } }
Resultado
Arrastar em duas dimensões com Modifier.draggable2D
Use o modificador draggable2D para mover elementos individuais da interface nos eixos X e Y simultaneamente.
Arrastar um elemento combinável
Este exemplo mostra o caso de uso mais comum para draggable2D: permitir que um usuário pegue um elemento da interface e o reposicione em qualquer lugar dentro de um contêiner pai.
@Composable private fun DraggableComposableElement() { // 1. Track the position of the floating window var offset by remember { mutableStateOf(Offset.Zero) } Box(modifier = Modifier.fillMaxSize().background(Color(0xFFF5F5F5))) { Box( modifier = Modifier // 2. Apply the offset to the box's position .offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) } // ... // 3. Attach the 2D drag logic .draggable2D( state = rememberDraggable2DState { delta -> // 4. Update the position based on the movement delta offset += delta } ), contentAlignment = Alignment.Center ) { Text("Video Preview", color = Color.White, fontSize = 12.sp) } } }
O snippet de código anterior inclui o seguinte:
- Rastreia a posição da caixa usando um estado
offset. - Usa o modificador
offsetpara mudar a posição do componente com base nos deltas de arrastar. - Como não há suporte para deslizar, a caixa para de se mover no instante em que o usuário levanta o dedo.
Arrastar um combinável filho com base na área de arrastar do pai
Este exemplo demonstra como usar draggable2D para criar uma área de entrada 2D em que um botão seletor é restrito a uma superfície específica. Ao contrário do exemplo de elemento arrastável, que move o componente em si, essa implementação usa os deltas 2D para mover um "seletor" combinável filho em um seletor de cores:
@Composable private fun ExampleColorSelector( // ... ) { // 1. Maintain the 2D position of the selector in state. var selectorOffset by remember { mutableStateOf(Offset.Zero) } // 2. Track the size of the background container. var containerSize by remember { mutableStateOf(IntSize.Zero) } Box( modifier = Modifier .size(300.dp, 200.dp) // Capture the actual pixel dimensions of the container when it's laid out. .onSizeChanged { containerSize = it } .clip(RoundedCornerShape(12.dp)) .background( brush = remember(hue) { // Create a simple gradient representing Saturation and Value for the given Hue. Brush.linearGradient(listOf(Color.White, Color.hsv(hue, 1f, 1f))) } ) ) { Box( modifier = Modifier .size(24.dp) .graphicsLayer { // Center the selector on the finger by subtracting half its size. translationX = selectorOffset.x - (24.dp.toPx() / 2) translationY = selectorOffset.y - (24.dp.toPx() / 2) } // ... // 3. Configure 2D touch dragging. .draggable2D( state = rememberDraggable2DState { delta -> // 4. Calculate the new position and clamp it to the container bounds val newX = (selectorOffset.x + delta.x) .coerceIn(0f, containerSize.width.toFloat()) val newY = (selectorOffset.y + delta.y) .coerceIn(0f, containerSize.height.toFloat()) selectorOffset = Offset(newX, newY) } ) ) } }
O snippet anterior inclui o seguinte:
- Ele usa o modificador
onSizeChangedpara capturar as dimensões reais do contêiner de gradiente. O seletor sabe exatamente onde estão as bordas. - Dentro da
graphicsLayer, ele ajustatranslationXetranslationYpara garantir que o seletor permaneça centralizado durante o arrastar.
Recomendados para você
- Observação: o texto do link aparece quando o JavaScript está desativado.
- Entender gestos
- Exemplo de animação avançada: gestos {:#gesture-and-animation}
- Animações com base no valor