O Compose fornece diversas APIs para ajudar a detectar gestos originados de interações do usuário. As APIs abrangem uma grande variedade de casos de uso:
Algumas delas são de alto nível e foram projetadas para abranger os gestos mais usados. Por exemplo, o modificador
clickable
facilita a detecção de cliques e também fornece recursos de acessibilidade e exibe indicadores visuais quando tocado (como ondulações).Também existem detectores de gestos menos usados, que oferecem mais flexibilidade em um nível inferior, como
PointerInputScope.detectTapGestures
ouPointerInputScope.detectDragGestures
, mas não incluem os recursos complementares.
Tocar e pressionar
O modificador
clickable
permite que os apps detectem cliques no elemento em que ele é aplicado.
@Composable
fun ClickableSample() {
val count = remember { mutableStateOf(0) }
// content that you want to make clickable
Text(
text = count.value.toString(),
modifier = Modifier.clickable { count.value += 1 }
)
}
Quando for necessário ter mais flexibilidade, forneça um detector de gestos de toque
usando o modificador pointerInput
:
Modifier.pointerInput(Unit) {
detectTapGestures(
onPress = { /* Called when the gesture starts */ },
onDoubleTap = { /* Called on Double Tap */ },
onLongPress = { /* Called on Long Press */ },
onTap = { /* Called on Tap */ }
)
}
Rolagem
Modificadores de rolagem
Os modificadores
verticalScroll
e
horizontalScroll
oferecem a forma mais simples de permitir que o usuário role um elemento quando
os limites do conteúdo são maiores que as restrições de tamanho máximo. Com
os modificadores verticalScroll
e horizontalScroll
, não é necessário
transladar nem deslocar o conteúdo.
@Composable
fun ScrollBoxes() {
Column(
modifier = Modifier
.background(Color.LightGray)
.size(100.dp)
.verticalScroll(rememberScrollState())
) {
repeat(10) {
Text("Item $it", modifier = Modifier.padding(2.dp))
}
}
}
O
ScrollState
permite mudar a posição de rolagem ou descobrir o estado atual. Para criá-lo
com parâmetros padrão, use
rememberScrollState()
.
@Composable
private fun ScrollBoxesSmooth() {
// Smoothly scroll 100px on first composition
val state = rememberScrollState()
LaunchedEffect(Unit) { state.animateScrollTo(100) }
Column(
modifier = Modifier
.background(Color.LightGray)
.size(100.dp)
.padding(horizontal = 8.dp)
.verticalScroll(state)
) {
repeat(10) {
Text("Item $it", modifier = Modifier.padding(2.dp))
}
}
}
Modificador scrollable
O modificador
scrollable
é diferente dos modificadores de rolagem, porque scrollable
detecta os
gestos de rolagem, mas não desloca o conteúdo. Um
ScrollableState
é necessário para que esse modificador funcione corretamente.
Ao criar ScrollableState
, é necessário fornecer uma
função consumeScrollDelta
, que vai ser invocada em cada etapa de rolagem
(por entrada de gestos, rolagem suave ou deslizamento rápido)
com o delta em pixels.
Essa função precisa retornar a quantidade de distância de rolagem consumida. Isso é
para garantir que o evento seja propagado corretamente nos casos em que há elementos
aninhados com o modificador scrollable
.
O snippet a seguir detecta os gestos e exibe um valor numérico para o deslocamento, mas não desloca os elementos:
@Composable
fun ScrollableSample() {
// actual composable state
var offset by remember { mutableStateOf(0f) }
Box(
Modifier
.size(150.dp)
.scrollable(
orientation = Orientation.Vertical,
// Scrollable state: describes how to consume
// scrolling delta and update offset
state = rememberScrollableState { delta ->
offset += delta
delta
}
)
.background(Color.LightGray),
contentAlignment = Alignment.Center
) {
Text(offset.toString())
}
}
Rolagem aninhada
O Compose é compatível com a rolagem aninhada, em que vários elementos reagem a um único gesto de rolagem. Um exemplo típico de rolagem aninhada é uma lista dentro de outra, e um caso mais complexo é uma barra de ferramentas recolhível (em inglês).
Rolagem aninhada automática
Nenhuma ação é necessária para a rolagem aninhada simples. Os gestos que iniciam uma ação de rolagem são propagados automaticamente para os pais. Assim, quando o elemento filho não consegue rolar mais, o gesto é processado pelo pai.
Há suporte para a rolagem aninhada automática e ela é fornecida de imediato por alguns
componentes e modificadores do Compose: verticalScroll
, horizontalScroll
,
scrollable
, APIs Lazy
e TextField
. Isso significa que, quando o usuário
rola um filho interno de componentes aninhados, os modificadores anteriores propagam
os deltas de rolagem para os pais que têm suporte à rolagem aninhada.
O exemplo a seguir mostra elementos com um modificador verticalScroll
aplicado em um contêiner que também tem um modificador verticalScroll
aplicado a ele.
val gradient = Brush.verticalGradient(0f to Color.Gray, 1000f to Color.White)
Box(
modifier = Modifier
.background(Color.LightGray)
.verticalScroll(rememberScrollState())
.padding(32.dp)
) {
Column {
repeat(6) {
Box(
modifier = Modifier
.height(128.dp)
.verticalScroll(rememberScrollState())
) {
Text(
"Scroll here",
modifier = Modifier
.border(12.dp, Color.DarkGray)
.background(brush = gradient)
.padding(24.dp)
.height(150.dp)
)
}
}
}
}
Como usar o modificador nestedScroll
Caso você precise criar uma rolagem coordenada avançada entre vários elementos,
o
modificador nestedScroll
oferece mais flexibilidade, definindo uma hierarquia de rolagem aninhada.
Como mencionado na seção anterior, alguns componentes têm suporte integrado
à rolagem aninhada. No entanto, no caso de elementos de composição que não podem ser rolados
automaticamente, como Box
ou Column
, os deltas de rolagem
não serão propagados no sistema de rolagem aninhado e não vão alcançar a
NestedScrollConnection
nem o componente pai. Se quiser resolver isso, use
nestedScroll
para conferir esse suporte a outros componentes, inclusive aos componentes
personalizados.
Interoperabilidade de rolagem aninhada (a partir do Compose 1.2.0)
Ao tentar aninhar elementos View
roláveis em
elementos de composição roláveis, ou vice-versa, talvez você encontre problemas.
Os mais perceptíveis aconteceriam ao rolar o filho e atingir o limite inicial
ou final, esperando que o pai assuma a rolagem. Esse
comportamento pode não acontecer ou não funcionar como previsto.
Esse problema é resultado das expectativas criadas em elementos de composição roláveis.
Esses elementos têm uma regra "nested-scroll-by-default", que significa que
qualquer contêiner rolável precisa participar da cadeia de rolagem aninhada, ambos como
um pai pela
NestedScrollConnection
e como um filho pelo
NestedScrollDispatcher
.
Quando o filho estivesse no limite, ele geraria uma rolagem aninhada
para o pai. Por exemplo, essa regra permite que Pager
e LazyRow
do Compose
funcionem bem juntos. No entanto, quando a rolagem de interoperabilidade ocorre
com a ViewPager2
ou a RecyclerView
, como elas não implementam a
NestedScrollingParent3
,
a rolagem contínua de filho para pai não pode ser feita.
Para ativar a API de interoperabilidade de rolagem aninhada entre elementos View
roláveis e
elementos de composição roláveis, aninhados em ambas as direções, você pode usar a API
para mitigar esses problemas nos cenários a seguir.
Uma visualização mãe colaborativa que contém uma ComposeView filha
Uma View
mãe colaborativa é aquela que já implementa a
NestedScrollingParent3
e, por isso, pode receber deltas de rolagem de um elemento de composição filho
que é colaborativo e aninhado. A ComposeView
atuaria como uma filha nesse caso e
precisaria implementar (indiretamente)
a NestedScrollingChild3
.
Um exemplo de um pai colaborativo é o
androidx.coordinatorlayout.widget.CoordinatorLayout
.
Caso você precise de interoperabilidade de rolagem aninhada entre contêineres pai
roláveis de View
e elementos de composição filhos roláveis e aninhados, use
rememberNestedScrollInteropConnection()
.
A função rememberNestedScrollInteropConnection()
permite e se lembra da
NestedScrollConnection
,
que ativa a interoperabilidade de rolagem aninhada entre uma View
mãe que implementa a NestedScrollingParent3
e um filho de composição. Ela precisa ser usada em conjunto com um
modificador
nestedScroll
. Como a rolagem aninhada é ativada por padrão no lado do Compose,
você pode usar essa conexão para ativar a rolagem aninhada no lado da View
e
adicionar a conexão necessária entre Views
e elementos de composição.
Um caso de uso frequente é a utilização de CoordinatorLayout
, CollapsingToolbarLayout
e um
elemento de composição, como mostrado neste exemplo:
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="100dp"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<!--...-->
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
Na atividade ou no fragmento, você precisa configurar o elemento de composição filho e
a NestedScrollConnection
necessária:
open class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<ComposeView>(R.id.compose_view).apply {
setContent {
val nestedScrollInterop = rememberNestedScrollInteropConnection()
// Add the nested scroll connection to your top level @Composable element
// using the nestedScroll modifier.
LazyColumn(modifier = Modifier.nestedScroll(nestedScrollInterop)) {
items(20) { item ->
Box(
modifier = Modifier
.padding(16.dp)
.height(56.dp)
.fillMaxWidth()
.background(Color.Gray),
contentAlignment = Alignment.Center
) {
Text(item.toString())
}
}
}
}
}
}
}
Um elemento de composição pai que contém uma AndroidView filha
Esse cenário aborda a implementação da API de interoperabilidade de rolagem aninhada no
lado do Compose, quando há um elemento de composição pai contendo uma
AndroidView
filha. A AndroidView
implementa o
NestedScrollDispatcher
,
que atua como filha para um pai de rolagem do Compose, e a
NestedScrollingParent3
,
que atua como uma View
de rolagem filha. O pai de composição
vai poder receber deltas de rolagem aninhados em uma View
filha de
rolagem aninhada.
O exemplo a seguir mostra como alcançar a interoperabilidade de rolagem aninhada nesse cenário, junto com uma barra de ferramentas recolhível do Compose:
@Composable
private fun NestedScrollInteropComposeParentWithAndroidChildExample() {
val toolbarHeightPx = with(LocalDensity.current) { ToolbarHeight.roundToPx().toFloat() }
val toolbarOffsetHeightPx = remember { mutableStateOf(0f) }
// Sets up the nested scroll connection between the Box composable parent
// and the child AndroidView containing the RecyclerView
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
// Updates the toolbar offset based on the scroll to enable
// collapsible behaviour
val delta = available.y
val newOffset = toolbarOffsetHeightPx.value + delta
toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f)
return Offset.Zero
}
}
}
Box(
Modifier
.fillMaxSize()
.nestedScroll(nestedScrollConnection)
) {
TopAppBar(
modifier = Modifier
.height(ToolbarHeight)
.offset { IntOffset(x = 0, y = toolbarOffsetHeightPx.value.roundToInt()) }
)
AndroidView(
{ context ->
LayoutInflater.from(context)
.inflate(R.layout.view_in_compose_nested_scroll_interop, null).apply {
with(findViewById<RecyclerView>(R.id.main_list)) {
layoutManager = LinearLayoutManager(context, VERTICAL, false)
adapter = NestedScrollInteropAdapter()
}
}.also {
// Nested scrolling interop is enabled when
// nested scroll is enabled for the root View
ViewCompat.setNestedScrollingEnabled(it, true)
}
},
// ...
)
}
}
private class NestedScrollInteropAdapter :
Adapter<NestedScrollInteropAdapter.NestedScrollInteropViewHolder>() {
val items = (1..10).map { it.toString() }
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): NestedScrollInteropViewHolder {
return NestedScrollInteropViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.list_item, parent, false)
)
}
override fun onBindViewHolder(holder: NestedScrollInteropViewHolder, position: Int) {
// ...
}
class NestedScrollInteropViewHolder(view: View) : ViewHolder(view) {
fun bind(item: String) {
// ...
}
}
// ...
}
O exemplo a seguir mostra como usar a API com um modificador scrollable
:
@Composable
fun ViewInComposeNestedScrollInteropExample() {
Box(
Modifier
.fillMaxSize()
.scrollable(rememberScrollableState {
// View component deltas should be reflected in Compose
// components that participate in nested scrolling
it
}, Orientation.Vertical)
) {
AndroidView(
{ context ->
LayoutInflater.from(context)
.inflate(android.R.layout.list_item, null)
.apply {
// Nested scrolling interop is enabled when
// nested scroll is enabled for the root View
ViewCompat.setNestedScrollingEnabled(this, true)
}
}
)
}
}
Por fim, este exemplo mostra como a API de interoperabilidade de rolagem aninhada é usada com a classe
BottomSheetDialogFragment
para possibilitar um comportamento de arrastar e dispensar:
class BottomSheetFragment : BottomSheetDialogFragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val rootView: View = inflater.inflate(R.layout.fragment_bottom_sheet, container, false)
rootView.findViewById<ComposeView>(R.id.compose_view).apply {
setContent {
val nestedScrollInterop = rememberNestedScrollInteropConnection()
LazyColumn(
Modifier
.nestedScroll(nestedScrollInterop)
.fillMaxSize()
) {
item {
Text(text = "Bottom sheet title")
}
items(10) {
Text(
text = "List item number $it",
modifier = Modifier.fillMaxWidth()
)
}
}
}
return rootView
}
}
}
Observe que a função rememberNestedScrollInteropConnection()
vai instalar uma
NestedScrollConnection
no elemento ao qual ela será anexada. A NestedScrollConnection
é responsável por
transmitir os deltas do nível de composição para o nível da View
. Isso permite
que o elemento participe da rolagem aninhada, mas não ativa
a rolagem automática de elementos. No caso de elementos de composição que não podem ser rolados
automaticamente, como Box
ou Column
, os deltas de rolagem
não serão propagados no sistema de rolagem aninhado. Além disso, os deltas não vão conseguir alcançar a
NestedScrollConnection
fornecida pela rememberNestedScrollInteropConnection()
.
Portanto, esses deltas não vão alcançar o componente View
mãe. Para resolver
isso, defina também modificadores roláveis para esses tipos de elementos de composição
aninhados. Consulte a seção anterior sobre
Rolagem aninhada para
ver mais informações.
Uma visualização mãe não colaborativa que contém uma ComposeView filha
Uma visualização não colaborativa é aquela que não implementa as interfaces
NestedScrolling
necessárias no lado da View
. Isso significa que
a interoperabilidade de rolagem aninhada nessas Views
não funciona
imediatamente. As Views
não colaborativas são a RecyclerView
e a ViewPager2
.
Arrastar
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. É necessário manter o estado e representá-lo na tela,
por exemplo, movendo o elemento com o
modificador
offset
:
var offsetX by remember { mutableStateOf(0f) }
Text(
modifier = Modifier
.offset { IntOffset(offsetX.roundToInt(), 0) }
.draggable(
orientation = Orientation.Horizontal,
state = rememberDraggableState { delta ->
offsetX += delta
}
),
text = "Drag me!"
)
Caso você precise controlar todo o gesto de arrastar, considere usar o detector de gestos
de arrastar, com o
modificador
pointerInput
.
Box(modifier = Modifier.fillMaxSize()) {
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
Box(
Modifier
.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
.background(Color.Blue)
.size(50.dp)
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consumeAllChanges()
offsetX += dragAmount.x
offsetY += dragAmount.y
}
}
)
}
Deslizar
O modificador
swipeable
permite arrastar elementos que, quando soltos, são animados em direção a dois ou
mais pontos de fixação definidos na orientação. Um uso comum para isso é
a implementação do padrão "deslizar para dispensar".
É importante ressaltar que esse modificador não move o elemento,
apenas detecta o gesto. É necessário manter o estado e representá-lo na tela,
por exemplo, movendo o elemento com o
modificador
offset
.
O estado deslizante é obrigatório no modificador swipeable
e pode ser criado
e lembrado com
rememberSwipeableState()
.
Esse estado também fornece um conjunto de métodos úteis para inserir animações de forma programática
nos pontos fixos (consulte
snapTo
,
animateTo
,
performFling
e
performDrag
)
e as propriedades para observar o progresso da ação de arrastar.
O gesto de deslizar pode ser configurado para ter diferentes tipos de limite, como
FixedThreshold(Dp)
e
FractionalThreshold(Float)
.
Esses limites podem ser diferentes para cada combinação de ponto de partida e chegada dos pontos fixos.
Para ter mais flexibilidade, você pode configurar resistance
ao deslizar para além dos
limites. Também é possível configurar velocityThreshold
, que fará a animação do gesto de deslizar para o
próximo estado, mesmo que os thresholds
não tenham sido alcançados.
@Composable
fun SwipeableSample() {
val width = 96.dp
val squareSize = 48.dp
val swipeableState = rememberSwipeableState(0)
val sizePx = with(LocalDensity.current) { squareSize.toPx() }
val anchors = mapOf(0f to 0, sizePx to 1) // Maps anchor points (in px) to states
Box(
modifier = Modifier
.width(width)
.swipeable(
state = swipeableState,
anchors = anchors,
thresholds = { _, _ -> FractionalThreshold(0.3f) },
orientation = Orientation.Horizontal
)
.background(Color.LightGray)
) {
Box(
Modifier
.offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }
.size(squareSize)
.background(Color.DarkGray)
)
}
}
Multitoque: panorâmica, zoom, rotação
Para detectar gestos multitoque usados para colocar na panorâmica, aplicar zoom e girar,
use o modificador transformable
. Ele não transforma os elementos por si
só, apenas detecta os gestos.
@Composable
fun TransformableSample() {
// set up all transformation states
var scale by remember { mutableStateOf(1f) }
var rotation by remember { mutableStateOf(0f) }
var offset by remember { mutableStateOf(Offset.Zero) }
val state = rememberTransformableState { zoomChange, offsetChange, rotationChange ->
scale *= zoomChange
rotation += rotationChange
offset += offsetChange
}
Box(
Modifier
// apply other transformations like rotation and zoom
// on the pizza slice emoji
.graphicsLayer(
scaleX = scale,
scaleY = scale,
rotationZ = rotation,
translationX = offset.x,
translationY = offset.y
)
// add transformable to listen to multitouch transformation events
// after offset
.transformable(state = state)
.background(Color.Blue)
.fillMaxSize()
)
}
Caso você precise combinar zoom, panorâmica e rotação com outros gestos,
use
o detector
PointerInputScope.detectTransformGestures
.