Como implementar o recurso Swipe-to-Action usando AnchoredDraggable no Jetpack Compose
Histórico
Nesta postagem do blog, vamos nos aprofundar em uma nova e empolgante ferramenta: o modificador AnchoredDraggable
. Veremos como usar esse modificador para criar algo muito legal: deslizar para a ação. É aquele recurso que você vê com frequência nos aplicativos modernos em que você desliza algo e ele revela ações como excluir, editar, compartilhar etc.
O AnchoredDraggable foi projetado para criar componentes que você pode arrastar entre estados específicos, exatamente como aquelas folhas de fundo modais que você já viu. A melhor parte? Essa API assume a função da API Swipeable do Material, oferecendo uma abordagem mais versátil.
Essa implementação é inspirada no flutter_slidable.
O que implementamos neste blog? Três variantes de swipe-to-action
Conteudo
Adicionar dependência
Vamos primeiro adicionar uma dependência necessária para usar a API arrastável Anchored.
implementation("androidx.compose.foundation:foundation:1.7.0-alpha05")
Modificador AnchoredDraggable
Antes de prosseguirmos com a implementação, vamos dar uma olhada rápida no modificador AnchoredDraggable.
AnchoredDraggableState
@ExperimentalFoundationApi class AnchoredDraggableState<T>( initialValue: T, internal val positionalThreshold: (totalDistance: Float) -> Float, internal val velocityThreshold: () -> Float, val animationSpec: AnimationSpec<Float>, internal val confirmValueChange: (newValue: T) -> Boolean = { true } )
O AnchoredDraggableState
ajuda a gerenciar e controlar elementos arrastáveis com pontos de ancoragem em seu aplicativo. Veja a seguir o que você precisa saber:
Posição inicial: Você pode definir a posição inicial de um elemento arrastável usando initialValue
.
Limites: Permite que você defina dois limites:
positionalThreshold
: Determina quando o elemento deve se ajustar a uma nova posição enquanto você o arrasta. É como se você dissesse: “Mova-o até aqui e depois se encaixe na âncora mais próxima”.velocityThreshold
: Define a velocidade na qual o elemento precisa estar se movendo quando você o solta para que ele se encaixe em uma nova posição, mesmo que não tenha atingido o limite posicional.
Animações: Você pode especificar como o elemento deve fazer uma transição suave entre os estados usando o animationSpec
.
Controle: Há um retorno de chamada opcional chamado confirmValueChange
que lhe dá controle sobre a ocorrência ou não de uma mudança de estado.
Modifier.anchoredDraggable
@ExperimentalFoundationApi fun <T> Modifier.anchoredDraggable( state: AnchoredDraggableState<T>, orientation: Orientation, enabled: Boolean = true, reverseDirection: Boolean = false, interactionSource: MutableInteractionSource? = null ) = draggable( state = state.draggableState, orientation = orientation, enabled = enabled, interactionSource = interactionSource, reverseDirection = reverseDirection, startDragImmediately = state.isAnimationRunning, onDragStopped = { velocity -> launch { state.settle(velocity) } } )
Parâmetros principais:
state
: Associa o elemento arrastável a umAnchoredDraggableState
, gerenciando sua posição e animação.orientation
(orientação): Especifica a orientação do arrasto como horizontal ou vertical.enabled
: Determina se o comportamento arrastável está ativo ou não.reverseDirection
: Parâmetro opcional para reverter a direção do arrasto.interactionSource
: Uma fonte opcional para eventos de interação.
Como funciona:
- Quando o usuário inicia um gesto de arrastar, a posição do composable é atualizada de acordo com o delta de arrastar, criando um efeito visualmente responsivo.
- Ao liberar o composable, ele é animado suavemente em direção ao ponto de ancoragem predefinido mais próximo.
- O
AnchoredDraggableState
mantém o controle da âncora atual, permitindo que você reaja às alterações de forma adequada.
Agora vamos iniciar a implementação do primeiro e mais simples Swipe-to-action. Para simplificar, demos nomes a todas as três variantes de acordo com seu movimento.
Para manter a implementação simples, não estou mostrando a composição de ações e conteúdo.
Usaremos o DraggableItem
composable comum em todos os exemplos.
enum class DragAnchors { Start, Center, End, } @OptIn(ExperimentalFoundationApi::class) @Composable fun DraggableItem( state: AnchoredDraggableState<DragAnchors>, content: @Composable BoxScope.() -> Unit, startAction: @Composable (BoxScope.() -> Unit)? = {}, endAction: @Composable (BoxScope.() -> Unit)? = {} ) { Box( modifier = Modifier .padding(16.dp) .fillMaxWidth() .height(100.dp) .clip(RectangleShape) ) { endAction?.let { endAction() } startAction?.let { startAction() } Box( modifier = Modifier .fillMaxWidth() .align(Alignment.CenterStart) .offset { IntOffset( x = -state .requireOffset() .roundToInt(), y = 0, ) } .anchoredDraggable(state, Orientation.Horizontal, reverseDirection = true), content = content ) } }
Parâmetros:
state
: Esse parâmetro é do tipo AnchoredDraggableState<DragAnchors> e controla como o item é arrastado, ancorado e animado.content
: Representa o conteúdo principal do item arrastável.startAction
eendAction
: Esses parâmetros são opcionais. Define os botões de deslizar para ação que aparecem quando o item é arrastado para a posição inicial ou final.
BehindMotionSwipe
Aqui implementaremos uma animação de deslizamento que revela a ação à medida que você arrasta. Primeiro, vamos definir o AnchoredDraggableState
val density = LocalDensity.current val defaultActionSize = 80.dp val endActionSizePx = with(density) { (defaultActionSize * 2).toPx() } val startActionSizePx = with(density) { defaultActionSize.toPx() } val state = remember { AnchoredDraggableState( initialValue = DragAnchors.Center, anchors = DraggableAnchors { DragAnchors.Start at -startActionSizePx DragAnchors.Center at 0f DragAnchors.End at endActionSizePx }, positionalThreshold = { distance: Float -> distance * 0.5f }, velocityThreshold = { with(density) { 100.dp.toPx() } }, animationSpec = tween(), ) }
DraggableAnchors { ... }
: Define os pontos de ancoragem para o elemento arrastável. Ele usa a DSL (linguagem específica de domínio) DraggableAnchors
para especificar três pontos de ancoragem:
- Âncora “Start” a uma distância negativa de
startActionSizePx
pixels do centro. - Âncora “Center” no centro (0 pixels do centro).
- Âncora “End” (fim) a
endActionSizePx
pixels do centro.
Agora, só precisamos usar isso para animar o DraggableItem
. Veja como
Box( modifier = Modifier .fillMaxWidth() .align(Alignment.CenterStart) .offset { IntOffset( x = -state .requireOffset() .roundToInt(), y = 0, ) } .anchoredDraggable(state, Orientation.Horizontal, reverseDirection = true), content = content )
E pronto. Muito simples, certo? Veja a seguir o que teremos com a implementação acima
O código-fonte completo do BehindMotionSwipe está disponível no GitHub.
ScrollMotionSwipe
Para ter um efeito deslizante à medida que arrastamos, precisamos animar a posição das ações.
Nosso AnchoredDraggableState seria o mesmo da implementação acima.
Vamos ver primeiro a ação inicial, que é a Save Action.
DraggableItem(state = state, startAction = { Box( modifier = Modifier .fillMaxHeight() .align(Alignment.CenterStart), ) { SaveAction( Modifier .width(defaultActionSize) .fillMaxHeight() .offset { IntOffset( ((-state .requireOffset() - actionSizePx)) .roundToInt(), 0 ) } ) } }, endAction = { ... }, content = { ... }
((-state.requireOffset() - actionSizePx)).roundToInt()
: Esse cálculo determina a coordenada X da SaveAction. Vamos decompô-lo:
state.requireOffset()
: Isso recupera o deslocamento atual do objeto de estado. O deslocamento representa a distância em que o elemento arrastável foi arrastado.- actionSizePx
: Subtrai oactionSizePx
do deslocamento. Isso é usado para ajustar a posição doSaveAction
em relação à posição do elemento arrastável.
Agora vamos ver a animação de deslizamento das ações finais.
endAction = { Row( modifier = Modifier .fillMaxHeight() .align(Alignment.CenterEnd) .offset { IntOffset( (-state .requireOffset() + endActionSizePx) .roundToInt(), 0 ) } ) { // Action composables } }
Aqui, o cálculo de -state.requireOffset() + endActionSizePx
determina a coordenada X da Row
e +
endActionSizePx
adiciona endActionSizePx
ao deslocamento. Esse ajuste é usado para posicionar a “ação final” em relação à posição do elemento arrastável.
Agora terminamos a configuração. Vamos ver o resultado.
DrawerMotionSwipe
Esse efeito de deslizamento é bastante semelhante ao anterior, com apenas um pequeno ajuste na posição horizontal de uma ação.
Nesse caso, em vez de alterar o deslocamento do composable pai (como feito na implementação anterior, em que era uma linha), ajustaremos o deslocamento de cada ação individual.
Vejamos primeiro a EditAction.
EditAction( Modifier .width(defaultActionSize) .fillMaxHeight() .offset { IntOffset( ((-state .requireOffset()) + actionSizePx) .roundToInt(), 0 ) } )
Nada de especial, apenas alterando o deslocamento horizontal da ação com base no estado arrastável.
DeleteAction( Modifier .width(defaultActionSize) .fillMaxHeight() .offset { IntOffset( ((-state .requireOffset() * 0.5f) + actionSizePx) .roundToInt(), 0 ) } )
Assim como a “EditAction”, a “DeleteAction” também é posicionada com base no deslocamento do estado atual. No entanto, há uma diferença distinta na forma como ele é calculado. A “DeleteAction” foi intencionalmente projetada para reduzir seu movimento horizontal pela metade em comparação com a “EditAction”, fazendo com que pareça uma gaveta que desliza suavemente para fora. E aqui está o resultado.
Conclusão
Ao implementar a funcionalidade de deslizar para revelar usando AnchoredDraggable, mostramos como a composibilidade e a abordagem declarativa do Compose podem simplificar interações complexas. Ao experimentar essa técnica, você encontrará inúmeras possibilidades de aprimorar as experiências do usuário.
Boa programação!
O código-fonte está disponível no GitHub.