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 e captura os deltas, mas não desloca o conteúdo
automaticamente. Em vez disso, ele é delegado ao usuário por meio de
ScrollableState
,
que é necessário para que esse modificador funcione corretamente.
Ao criar ScrollableState
, é necessário 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. 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
A rolagem aninhada é um sistema em que vários componentes de rolagem contidos uns nos outros trabalham juntos reagindo a um único gesto de rolagem e comunicando as diferenças de rolagem (mudanças).
O sistema de rolagem aninhada permite a coordenação entre componentes que são roláveis e vinculados hierarquicamente (geralmente compartilhando o mesmo pai). Esse sistema vincula contêineres de rolagem e permite a interação com as deltas de rolagem que estão sendo propagadas e compartilhadas.
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, 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.
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 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, 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 aninhada e não vão alcançar a
NestedScrollConnection
nem o componente pai. Para resolver isso, use nestedScroll
para conferir esse
suporte a outros componentes, inclusive aos personalizados.
Ciclo de rolagem aninhada
O ciclo de rolagem aninhada é o fluxo de deltas de rolagem que são enviados para cima e para baixo
na árvore de hierarquia por todos os componentes (ou nós) que fazem parte do sistema de rolagem
aninhada, por exemplo, usando componentes roláveis e modificadores ou
nestedScroll
.
Fases do ciclo de rolagem aninhada
Quando um evento de gatilho (por exemplo, um gesto) é detectado por um componente rolável, antes mesmo de a ação de rolagem ser acionada, as deltas geradas são enviadas para o sistema de rolagem aninhada e passam por três fases: pré-rolagem, consumo de nó e pós-rolagem.
Na primeira fase de pré-rolagem, o componente que recebeu as diferenças do evento de gatilho envia esses eventos para cima, pela árvore de hierarquia, até o elemento pai mais alto. Os eventos delta vão flutuar para baixo, o que significa que os deltas 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
nó possa consumi-lo.
Na fase de consumo de nó, o próprio nó vai usar qualquer delta que não tenha sido usado pelos pais. É quando o movimento de rolagem é concluído e fica visível.
Durante essa fase, a criança pode consumir todo ou parte do rolagem restante. O restante será enviado de volta para passar pela fase pós-rolagem.
Por fim, na fase pós-rolagem, tudo o que o nó não consumiu será enviado novamente para os ancestrais para consumo.
A fase pós-rolagem funciona de maneira semelhante à fase pré-rolagem, em que qualquer dos pais pode escolher consumir ou não.
Assim como na rolagem, quando um gesto de arrasto é concluído, a intenção do usuário pode ser traduzida em uma velocidade que é usada para deslizar (rolar usando uma animação) o contêiner rolável. O fling também faz parte do ciclo de rolagem aninhado, e as velocidades geradas pelo evento de arrasto passam por fases semelhantes: pré-fling, consumo de nó e pós-fling. A animação de arremesso está associada apenas ao gesto de toque e não será acionada por outros eventos, como a11y ou rolagem de hardware.
Participar do ciclo de rolagem aninhada
A participação no ciclo significa interceptar, consumir e informar o consumo de deltas ao longo da hierarquia. O Compose oferece um conjunto de ferramentas para influenciar o funcionamento do sistema de rolagem aninhada e como interagir diretamente com ele, por exemplo, quando você precisa fazer algo com as diferenças de rolagem antes de um componente rolável começar a rolar.
Se o ciclo de rolagem aninhado for um sistema que atua em uma cadeia de nós, o modificador
nestedScroll
é uma maneira de interceptar e inserir essas mudanças e
influenciar os dados (deltas de rolagem) que são propagados na cadeia. Esse
modificador pode ser colocado em qualquer lugar na hierarquia e se comunica com
instâncias de modificador de rolagem aninhadas na árvore para 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 aninhada e influenciar
o sistema de rolagem aninhada. Ele é composto por quatro métodos de callback, cada um
representando uma das fases de consumo: pré/pós-rolagem e pré/pós-fling:
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, a qualquer momento, você quiser parar de propagar 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. Usar um despachante e chamar os métodos dele
aciona o ciclo. Contêineres roláveis têm um despachante integrado que envia
deltas capturados durante os gestos para o sistema. Por esse motivo, a maioria dos casos de uso
de personalização de rolagem aninhada envolve o uso de NestedScrollConnection
em vez
de um despachante, para reagir a deltas já existentes em vez de enviar novos.
Consulte
NestedScrollDispatcherSample
para mais usos.
Redimensionar uma imagem ao rolar a tela
À medida que o usuário rola a tela, é possível criar um efeito visual dinâmico em que a imagem muda de tamanho com base na posição de rolagem.
Redimensionar uma imagem com base na posição de rolagem
Este snippet demonstra como redimensionar uma imagem em um LazyColumn
com base na
posição de rolagem vertical. A imagem encolhe à medida que o usuário rola para baixo e cresce
à medida que rola para cima, permanecendo dentro dos limites de tamanho mínimo e máximo definidos:
@Composable fun ImageResizeOnScrollExample( modifier: Modifier = Modifier, maxImageSize: Dp = 300.dp, minImageSize: Dp = 100.dp ) { var currentImageSize by remember { mutableStateOf(maxImageSize) } var imageScale by remember { mutableFloatStateOf(1f) } val nestedScrollConnection = remember { object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { // Calculate the change in image size based on scroll delta val delta = available.y val newImageSize = currentImageSize + delta.dp val previousImageSize = currentImageSize // Constrain the image size within the allowed bounds currentImageSize = newImageSize.coerceIn(minImageSize, maxImageSize) val consumed = currentImageSize - previousImageSize // Calculate the scale for the image imageScale = currentImageSize / maxImageSize // Return the consumed scroll amount return Offset(0f, consumed.value) } } } Box(Modifier.nestedScroll(nestedScrollConnection)) { LazyColumn( Modifier .fillMaxWidth() .padding(15.dp) .offset { IntOffset(0, currentImageSize.roundToPx()) } ) { // Placeholder list items items(100, key = { it }) { Text( text = "Item: $it", style = MaterialTheme.typography.bodyLarge ) } } Image( painter = ColorPainter(Color.Red), contentDescription = "Red color image", Modifier .size(maxImageSize) .align(Alignment.TopCenter) .graphicsLayer { scaleX = imageScale scaleY = imageScale // Center the image vertically as it scales translationY = -(maxImageSize.toPx() - currentImageSize.toPx()) / 2f } ) } }
Pontos principais sobre o código
- Esse código usa um
NestedScrollConnection
para interceptar eventos de rolagem. - O
onPreScroll
calcula a mudança no tamanho da imagem com base na delta de rolagem. - A variável de estado
currentImageSize
armazena o tamanho atual da imagem, restringido entreminImageSize
emaxImageSize. imageScale
, derivado decurrentImageSize
. - Os deslocamentos de
LazyColumn
com base nocurrentImageSize
. - O
Image
usa um modificadorgraphicsLayer
para aplicar a escala calculada. - O
translationY
dentro dographicsLayer
garante que a imagem permaneça centralizada verticalmente à medida que é dimensionada.
Resultado
O snippet anterior resulta em um efeito de imagem de escalonamento ao rolar:
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 o limite inicial ou final, esperando
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.
Uma View
mãe colaborativa que contém uma ComposeView
filha
Uma View
mãe colaborativa é aquela que já implementa
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 é
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, é possível usar essa conexão para ativar a rolagem aninhada no lado da View
e adicionar
a lógica de união necessária entre Views
e elementos combináveis.
Um caso de uso frequente é usar 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() { 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 elemento pai de composição
vai poder receber deltas de rolagem aninhados de 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
}
}
}
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 mais informações.
Uma View
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
.
Recomendados para você
- Observação: o texto do link aparece quando o JavaScript está desativado.
- Noções básicas sobre gestos
- Migrar
CoordinatorLayout
para o Compose - Como usar visualizações no Compose