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
private fun ScrollBoxes() {
    Column(
        modifier = Modifier
            .background(Color.LightGray)
            .size(100.dp)
            .verticalScroll(rememberScrollState())
    ) {
        repeat(10) {
            Text("Item $it", modifier = Modifier.padding(2.dp))
        }
    }
}

Uma lista vertical simples respondendo
a gestos de rolagem

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())
    }
}

Um elemento da interface que detecta o pressionamento da tela e mostra o valor numérico para
a localização
dele

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)
                    )
                }
            }
        }
    }
}

Dois elementos da interface de rolagem vertical aninhados, respondendo a gestos dentro e
fora do elemento
interno

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.

Fases do ciclo de rolagem
aninhado

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.

Fase de pré-rolagem: envio para cima

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.

Fase de pré-rolagem: bolhas
para baixo

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.

Fase de consumo do nó

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.

Fase pós-rolagem: envio para cima

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.

Fase pós-rolagem: bolhas
para baixo

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.