Implementação de Paginação por Deslizamento no ListView do Android

Ao desenvolevr interfaces que consomem dados locais no Android, a combinação de SQLite, Cursor, CursorAdapter e ListView é uma abordagem clássica para armazenar, manipular e exibir informações. Para implementar um mecanismo de paginação acionado por gestos de deslizamento, é fundamental compreender como o componente de lista gerencia sua área de visualização e os eventos de toque.

Em vez de calcular a direção do movimento comparando coordenadas de toque ou analisando o valor do primeiro registro, uma estratégia mais robusta consiste em avaliar o estado de renderização do ListView após uma atualização. Podemos classificar a visualização atual em quatro cenários distintos:

  1. O conteúdo não preenche a tela inteira (banco de dados com poucos registros ou exibição da última página).
  2. O conteúdo excede a tela e a visualização está ancorada no início da lista.
  3. O conteúdo excede a tela e a visualização está em uma posição intermediária.
  4. O conteúdo excede a tela e a visualização está no final dos registros da página atual.

Identificado o estado visual, cruzamos essa informação com o estado do gesto de deslizamento para decidir se uma nova página deve ser carregada. A lógica de paginação do banco de dados é abstraída neste contexto.

Para monitorar a posição da lista, sobrescrevemos o método onScroll da interfcae AbsListView.OnScrollListener:

lista.setOnScrollListener(new AbsListView.OnScrollListener() {
    @Override
    public void onScroll(AbsListView view, int primeiroItemVisivel, int quantidadeItensVisiveis, int totalDeItens) {
        // primeiroItemVisivel: índice do primeiro item visível na tela (inicia em 0)
        // quantidadeItensVisiveis: total de itens visíveis no momento (inclui itens parcialmente exibidos)
        // totalDeItens: quantidade total de itens carregados no adapter
        
        if (quantidadeItensVisiveis == totalDeItens) {
            // Cenário 1: Os dados não preenchem a tela inteira
            estadoAtual = ESTADO_LISTA_CURTA;
        } else {
            if (primeiroItemVisivel == 0) {
                // Cenário 2: Exibindo o topo da lista
                estadoAtual = ESTADO_TOPO;
            } else if (primeiroItemVisivel + quantidadeItensVisiveis == totalDeItens) {
                // Cenário 4: Exibindo o final da lista
                estadoAtual = ESTADO_BASE;
            } else {
                // Cenário 3: Posição intermediária (não requer paginação imediata)
                estadoAtual = ESTADO_MEIO;
            }
        }
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int estadoRolagem) {
        // Implementação do estado de rolagem
    }
});

Para capturar a intenção de deslizamento do usuário, o uso de setOnTouchListener não é recomendado. Esse método é acionado repetidamente em intervalos curtos durante o movimento e também responde a cliques simples nos itens, tornando a lógica de controle excessivamente complexa e verbosa.

A alternativa ideal é utilizar o callback onScrollStateChanged, que consolida a interação em estados bem definidos, tratando o ato de deslizar como uma única ação contínua.

lista.setOnScrollListener(new AbsListView.OnScrollListener() {
    private int contadorDeslizamentos = 0;

    @Override
    public void onScrollStateChanged(AbsListView view, int estadoRolagem) {
        switch (estadoRolagem) {
            case AbsListView.OnScrollListener.SCROLL_STATE_IDLE:
                // A rolagem parou completamente
                contadorDeslizamentos++;
                textViewStatus.setText("Rolagem finalizada: " + contadorDeslizamentos);
                Log.d("LISTA_TAG", "Estado IDLE atingido. Contagem: " + contadorDeslizamentos);
                
                // Aqui pode-se verificar o 'estadoAtual' definido no onScroll 
                // para disparar o carregamento da próxima ou anterior página.
                break;
                
            case AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
                // O usuário tocou e começou a arrastar
                if (contadorDeslizamentos > 5) {
                    contadorDeslizamentos = 0; // Reset preventivo
                }
                Log.d("LISTA_TAG", "Estado TOUCH_SCROLL iniciado.");
                break;
                
            case AbsListView.OnScrollListener.SCROLL_STATE_FLING:
                // O usuário soltou o dedo e a lista continua rolando por inércia
                Log.d("LISTA_TAG", "Estado FLING (inércia) ativo.");
                break;
        }
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        // Lógica de cálculo de estado visual
    }
});

Durante os testes, observa-se um comportamento peculiar no Cenário 1 (lista curta): o contador de eventos de rolagem só é alterado se o gesto de deslizamento for iniciado diretamente sobre um item renderizado. Se o usuário deslizar o dedo sobre uma área vazia do ListView, nenhum evento é disparado.

Isso ocorre porque o onScrollStateChanged depende da interação com os filhos (children) do ListView. Esse detalhe também explica a discrepância comum entre os identificadores do banco de dados e os índices da interface: enquanto as chaves primárias no SQLite geralmente iniciam em 1, os índices de posição dos itens no ListView do Android são baseados em zero. Compreender essa distinção e a área de toque válida é crucial para evitar falhas de paginação quando a lista possui poucos elementos.

Tags: android listview sqlite cursoradapter onscrolllistener

Publicado em 8-31 06:53