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, já que scrollable
detecta os
gestos de rolagem e captura os deltas, mas não desloca o conteúdo
automaticamente. Em vez disso, ele é delegado ao usuário por
ScrollableState
,
que é necessário para que esse modificador funcione corretamente.
Ao criar ScrollableState
, você precisa fornecer uma função consumeScrollDelta
que 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 para garantir que o evento seja propagado
corretamente nos casos em que há elementos aninhados que tenham 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
A rolagem aninhada é um sistema em que vários componentes de rolagem internos trabalham juntos reagindo a um único gesto de rolagem e comunicando os deltas de rolagem (mudanças).
O sistema de rolagem aninhado permite a coordenação entre componentes que são roláveis e vinculados hierarquicamente (na maioria das vezes, compartilhando o mesmo pai). Esse sistema vincula contêineres de rolagem e permite a interação com os deltas de rolagem que estão sendo propagados e compartilhados.
O Compose oferece várias maneiras de processar a rolagem aninhada entre elementos combináveis. Um exemplo típico de rolagem aninhada é uma lista dentro de outra lista, e um caso mais complexo é uma barra de ferramentas recolhível.
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.
A rolagem aninhada automática tem suporte e é fornecida de imediato por alguns dos
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 abaixo mostra elementos com um modificador
verticalScroll
aplicado a eles dentro de 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, para elementos combináveis que não podem ser rolados automaticamente, como
Box
ou Column
, os deltas de rolagem não vão ser propagados no
sistema de rolagem aninhado e não vão alcançar o NestedScrollConnection
nem
o componente pai. Para resolver isso, use nestedScroll
para conferir esse
suporte a outros componentes, inclusive personalizados.
Ciclo de rolagem aninhado
O ciclo de rolagem aninhado é o fluxo de deltas de rolagem enviados para cima e para baixo
na árvore de hierarquia por todos os componentes (ou nós) que fazem parte do sistema
de rolagem aninhado, por exemplo, usando componentes e modificadores roláveis, ou
nestedScroll
.
Fases do ciclo de rolagem aninhada
Quando um evento acionador (por exemplo, um gesto) é detectado por um componente rolável, antes mesmo da ação de rolagem real ser acionada, os deltas gerados são enviados ao sistema de rolagem aninhado e passam por três fases: pré-rolagem, consumo de nós e pós-rolagem.
Na primeira fase, pré-rolagem, o componente que recebeu os deltas de eventos acionadores envia esses eventos para o pai superior pela árvore hierárquica. Os eventos delta vão surgir, o que significa que eles serão propagados do pai mais raiz para o filho que iniciou o ciclo de rolagem aninhado.
Isso dá aos pais de rolagem aninhados (elementos combináveis que usam nestedScroll
ou
modificadores roláveis) a oportunidade de fazer algo com o delta antes que o
próprio nó possa consumi-lo.
Na fase de consumo do nó, o próprio nó usará qualquer delta não usado pelos pais. É quando o movimento de rolagem é feito e fica visível.
Durante essa fase, o filho pode optar por consumir toda ou parte da rolagem restante. O que restar será enviado de volta para passar pela fase pós-rolagem.
Por fim, na fase pós-rolagem, tudo o que o próprio nó não consumiu será enviado novamente aos ancestrais para consumo.
A fase pós-rolagem funciona de maneira semelhante à fase de pré-rolagem, em que qualquer um dos pais pode optar por consumir ou não.
Da mesma forma que a rolagem, quando um gesto de arrastar termina, a intenção do usuário pode ser convertida em uma velocidade usada para deslizar (rolar usando uma animação) do contêiner rolável. A rolagem rápida também faz parte do ciclo de rolagem aninhado, e as velocidade geradas pelo evento de arrastar passam por fases semelhantes: pré-rolagem, consumo do nó e pós-deslizamento. A animação de deslize rápido é associada apenas ao gesto de toque e não é acionada por outros eventos, como acessibilidade ou rolagem de hardware.
Participar do ciclo de rolagem aninhada
A participação no ciclo significa interceptar, consumir e relatar o consumo de deltas ao longo da hierarquia. O Compose oferece um conjunto de ferramentas para influenciar como o sistema de rolagem aninhado funciona e como interagir diretamente com ele. Por exemplo, quando você precisa fazer algo com os deltas de rolagem antes que um componente rolável comece a rolar.
Se o ciclo de rolagem aninhado for um sistema que atua em uma cadeia de nós, o
modificador nestedScroll
será uma maneira de interceptar e inserir nessas mudanças, além de
influenciar os dados (deltas de rolagem) propagados na cadeia. Esse
modificador pode ser colocado em qualquer lugar da hierarquia e se comunica com
instâncias de modificador de rolagem aninhadas na árvore para que possa compartilhar informações
por esse canal. Os elementos básicos desse modificador são NestedScrollConnection
e NestedScrollDispatcher
.
NestedScrollConnection
oferece uma maneira de responder às fases do ciclo de rolagem aninhado e influenciar
o sistema de rolagem aninhado. Ele é composto por quatro métodos de callback, cada um
representando uma das fases de consumo: pré/pós-rolagem e pré/pós-rolagem:
val nestedScrollConnection = object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { println("Received onPreScroll callback.") return Offset.Zero } override fun onPostScroll( consumed: Offset, available: Offset, source: NestedScrollSource ): Offset { println("Received onPostScroll callback.") return Offset.Zero } }
Cada callback também fornece informações sobre o delta que está sendo propagado:
delta available
para essa fase específica e delta consumed
consumido nas
fases anteriores. Se, em algum momento, você quiser interromper a propagação de deltas na
hierarquia, use a conexão de rolagem aninhada para fazer isso:
val disabledNestedScrollConnection = remember { object : NestedScrollConnection { override fun onPostScroll( consumed: Offset, available: Offset, source: NestedScrollSource ): Offset { return if (source == NestedScrollSource.SideEffect) { available } else { Offset.Zero } } } }
Todos os callbacks fornecem informações sobre o
tipo
NestedScrollSource
.
NestedScrollDispatcher
inicializa o ciclo de rolagem aninhado. O uso de um agente e a chamada dos métodos
aciona o ciclo. Os contêineres roláveis têm um agente integrado que envia
deltas capturados durante os gestos para o sistema. Por esse motivo, a maioria dos casos de uso
de personalização da rolagem aninhada envolve o uso de NestedScrollConnection
em vez
de um dispatcher, para reagir a deltas já existentes em vez de enviar novos.
Consulte
NestedScrollDispatcherSample
para mais usos.
Interoperabilidade de rolagem aninhada
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 os limites inicial ou final e esperar
que o pai assuma a rolagem. No entanto, esse comportamento esperado
pode não acontecer ou não funcionar como esperado.
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 que contém um ComposeView
filho
Uma View
mãe colaborativa é aquela que já implementa a
NestedScrollingParent3
e, portanto, pode receber deltas de rolagem de um elemento combinável filho
que é colaborativo e aninhado. A ComposeView
funcionaria como uma filha nesse caso e
precisaria implementar (indiretamente) a
NestedScrollingChild3
.
Um exemplo de um pai colaborativo é
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 filho, 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 contendo um filho AndroidView
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 elemento pai do Compose
poderá receber deltas de rolagem aninhados de um View
filho rolável aninhado.
O exemplo abaixo 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
}
}
}
O
rememberNestedScrollInteropConnection()
vai instalar um
NestedScrollConnection
no elemento ao qual ele será anexado. 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. Para 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 alcançarão o
NestedScrollConnection
fornecido por rememberNestedScrollInteropConnection()
.
Portanto, esses deltas não alcançarão o componente pai View
. Para resolver isso,
defina modificadores roláveis para esses tipos de elementos combináveis
aninhados. Consulte a seção anterior sobre Rolagem
aninhada para ver informações
mais detalhadas.
Um View
pai não colaborativo que contém um(a) filho(a) ComposeView
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
CoordinatorLayout
para o Compose - Como usar visualizações no Compose