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 private 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 private 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.
@Composable private fun AutomaticNestedScroll() { 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 combináveis 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 combináveis 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 combináveis 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 combináveis roláveis, aninhados em ambas as direções, você pode usar a API
para mitigar esses problemas nos cenários a seguir.
Um View
pai colaborativo contendo um ComposeView
filho
Uma View
mãe colaborativa é aquela que já implementa a
NestedScrollingParent3
e, por isso, pode receber deltas de rolagem de um elemento combinável 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 combináveis 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 combináveis.
Um caso de uso frequente é a utilização de CoordinatorLayout
, CollapsingToolbarLayout
e um
elemento combinável, 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 combinável filho e
a NestedScrollConnection
necessária:
open class MainActivity : ComponentActivity() { @OptIn(ExperimentalComposeUiApi::class) 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 combinável pai que contém um AndroidView
filho
Esse cenário aborda a implementação da API de interoperabilidade de rolagem aninhada no
lado do Compose, quando há um elemento combinável 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 combináveis 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 combináveis
aninhados. Consulte a seção anterior sobre
Rolagem aninhada para
ver mais informações.
Um View
pai não colaborativo contendo um ComposeView
filho
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
.
Recomendados para você
- Observação: o texto do link aparece quando o JavaScript está desativado.
- Entender os gestos
- Migrar o
CoordinatorLayout
para o Compose - Como usar visualizações no Compose