André Alves de Lima

Talking about Software Development and more…

Aplicações Android com Xamarin – Parte 4 de N – Customizando o controle ListView

No último artigo da minha “saga” com o desenvolvimento de uma aplicação Android utilizando Xamarin Native, eu acabei esbarrando na dificuldade que temos para apresentarmos dados em um controle ListView. Como você pode conferir no artigo anterior, se quisermos simplesmente listar strings na ListView, até que nós conseguimos com uma certa facilidade. Porém, se quisermos exibir dados mais estruturados, aí o bicho pega!

Nessa semana eu dei uma estudada nesse tema e consegui evoluir um pouco com a implementação do ListView. Até que ficou legal, mas deu um certo trabalho. Existem conceitos muito importantes que temos que entender ao trabalharmos com ListView no Android, e é justamente esses conceitos que eu quero apresentar no artigo de hoje.

Acesse os artigos anteriores

Antes de continuar com a leitura deste artigo, acesse os artigos anteriores dessa série utilizando os links abaixo:

Parte 1: Prototipando a aplicação
Parte 2: Ambiente Xamarin Android e Hello World
Parte 3: Classes de modelo e primeira tela

Layouts nativos do ListView

Na seção anterior, nós listamos uns gastos aleatórios em uma ListView utilizando o layout padrão, que tem o nome de “SimpleListItem1“. Esse layout possibilita a listagem de strings no controle ListView, através de um ArrayAdapter. Porém, esse não é o único layout padrão que temos à nossa disposição no Android. Se dermos uma olhada na documentação do Xamarin Android Native, veremos que existem uma diversidade de outros modelos de layout padrão:

Como você pode perceber, os layouts padrão são uma mão na roda quando precisamos exibir até duas informações por registro. Porém, no nosso caso, nós queremos exibir três informações por registro (data, estabelecimento e valor). Nesse caso, nós teremos que construir um layout customizado.

Gerando mais dados aleatórios

Antes de prosseguirmos com a customização do nosso ListView, vamos gerar uma massa maior de dados aleatórios. Até o momento nós temos somente dois gastos no ListView, e isso dificulta um pouco a compreensão de como o ListView se comportará quando tivermos mais dados cadastrados na aplicação:

Como nós ainda não estamos armazenando os dados no dispositivo (esse será o tema para um próximo artigo), nós geraremos dados aleatórios em memória. Vamos trabalhar com 10 estabelecimentos e 3 possíveis datas, em um total de 15 gastos que serão listados na nossa ListView. O novo código do nosso método “CarregarGastos” deve ficar assim:

        private List<Models.Gasto> CarregarGastos()
        {
            var estabelecimentos = new List<Models.Estabelecimento>();
            for (int c = 1; c <= 10; c++)
            {
                estabelecimentos.Add(new Models.Estabelecimento() { Id = c, Nome = string.Format("Estabelecimento {0}", c) });
            }

            var random = new Random();
            var gastos = new List<Models.Gasto>();
            for (int c = 1; c <= 15; c++)
            {
                var data = DateTime.Now.AddDays(random.Next(0, 3));
                var estabelecimento = estabelecimentos[random.Next(0, estabelecimentos.Count - 1)];
                var valor = random.Next(1, 50);
                gastos.Add(new Models.Gasto() { Id = c, Data = data, Estabelecimento = estabelecimento, Valor = valor });
            }

            return gastos;
        }

E este é o resultado da nossa ListView com uma quantidade maior de dados utilizando o layout padrão:

Agora que já temos uma quantidade maior de dados sendo exibidos na nossa ListView, fica mais fácil a criação e testes de layouts customizados para exibirmos esses dados.

Criando layouts customizados para o ListView

A criação de layouts customizados para ListViews no Android pode ser dividida em duas etapas. A primeira etapa é a criação do layout da linha, ou seja, a definição visual de como cada linha deve ser desenhada. Esse layout deve ser definido como um arquivo de extensão axml dentro da pasta “Resources/layout“.

Dito isso, vamos criar um novo layout clicando com o botão direito na pasta “layout“, escolhendo a opção “Add, new item“, marcando o item “Android Layout” e dando o nome de “ListItemRow.axml” para o layout que será criado:

Como mencionei anteriormente, dentro desse layout, nós vamos definir o conteúdo que cada uma das linhas do nosso ListView deverá exibir. Vamos começar definindo um layout linear horizontal (LinearLayout) e, dentro desse layout, vamos colocar três TextViews (um para cada propriedade do nosso gasto – valor, estabelecimento e data):

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:padding="8dp">
    <LinearLayout
        android:id="@+id/Text"
        android:orientation="horizontal"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingLeft="10dip">
        <TextView
            android:id="@+id/Valor"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="14dip" />
        <TextView
            android:id="@+id/Estabelecimento"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="14dip"
            android:paddingLeft="14dip" />
        <TextView
            android:id="@+id/Data"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="14dip"
            android:paddingLeft="14dip" />
    </LinearLayout>
</RelativeLayout>

Perceba que aqui nós temos toda a flexibilidade do mundo para definirmos como cada linha vai se comportar. Poderíamos criar um layout extremamente complexo (com imagens, por exemplo), mas vamos deixa-lo simples assim para começarmos a entender o conceito. Mais para frente nesse artigo nós customizaremos um pouco mais o layout das linhas.

Entendendo os Adapters do ListView

Uma vez criado o layout das linhas, nós temos que criar a classe que será responsável por fazer o carregamento dos dados em cada uma das linhas. No Android, essa classe se chama “Adapter“. Todas as implementações de adapters customizados no Android devem herdar de “BaseAdapter“, ou melhor, do “BaseAdapter” genérico (BaseAdapter<>).

Se observarmos a documentação dessa classe, nós notaremos que ela é abstrata, contendo quatro itens abstratos: a propriedade Count e os métodos GetItem, GetItemId e GetView. Isso quer dizer que, ao criarmos uma nova classe que herda de BaseAdapter, esses são os quatro itens que deveremos implementar para que o mecanismo do Android consiga utilizá-lo efetivamente.

Com isso em mente, vamos adicionar uma nova classe no nosso projeto, dando o nome de “ListViewAdapter“. Veja como é que deve ficar o código dessa classe (calma, já vou explicar a implementação em detalhes):

    public class ListViewAdapter : BaseAdapter<Models.Gasto>
    {
        List<Models.Gasto> items;
        Activity context;
        public ListViewAdapter(Activity context, List<Models.Gasto> items) : base()
        {
            this.context = context;
            this.items = items;
        }
        public override long GetItemId(int position)
        {
            return position;
        }
        public override Models.Gasto this[int position]
        {
            get { return items[position]; }
        }
        public override int Count
        {
            get { return items.Count; }
        }
        public override View GetView(int position, View convertView, ViewGroup parent)
        {
            var item = items[position];
            View view = convertView;
            if (view == null)
                view = context.LayoutInflater.Inflate(Resource.Layout.ListItemRow, null);
            view.FindViewById<TextView>(Resource.Id.Valor).Text = string.Format("{0:c}", item.Valor);
            view.FindViewById<TextView>(Resource.Id.Estabelecimento).Text = item.Estabelecimento.Nome;
            view.FindViewById<TextView>(Resource.Id.Data).Text = string.Format("{0:d}", item.Data);
            return view;
        }
    }

Primeiro, veja que nós herdamos de “BaseAdapter<Models.Gasto>“, ou seja, esse será um adapter que mostrará instâncias da classe “Gasto” (que criamos no artigo anterior). Logo de cara nós criamos uma lista interna de gastos (chamada “items“) e um atributo para mantermos uma relação com a Activity (tela) que criou esse adapter. Os valores para esses atributos são configurados no construtor da classe.

Em seguida, temos a implementação dos métodos “GetItemId“, “GetItem” (propriedade “this” indexada) e da propriedade “Count“. Essas implementações são muito tranquilas. O método “GetItemId” retorna simplesmente a posição que ele recebeu (na verdade, nem sei porque esse método precisa ser sobrescrito). A propriedade indexada “this” retorna o item correspondente à “position” na nossa lista interna. Por fim, a propriedade “Count” retorna a quantidade de itens da nossa lista interna de gastos.

O grande segredo está na implementação do método “GetView“. Esse é o método que o Android chamará para desenhar cada uma das linhas do ListView. Nesse método o Android passará por parâmetro a posição que está sendo desenhada (parâmetro “position“), uma View que será preenchida caso o Android queira reaproveitar um layout criado anteriormente a fim de economizar memória (parâmetro “convertView“) e um terceiro parâmetro chamado “parent” que eu não sei para que serve.

A implementação desse método parece complicada, mas na verdade é bem tranquila. Primeiro nós pegamos o item que deverá ser desenhado (que é o gasto correspondente à posição indicada na nossa lista interna). Em seguida, nós checamos se o Android já passou uma view através do parâmetro “convertView“. Caso positivo, nós utilizamos essa View que o Android passou, senão, nós criamos uma View nova utilizando o método “Inflate” e passando o layout que criamos anteriormente (ListItemRow).

Nota: aqui vale a pena uma explicação um pouco mais detalhada. Nas primeiras chamadas do método “GetView”, o Android nunca passará uma View no parâmetro “convertView”. Ou seja, nós criaremos o layout de cada uma das linhas que serão exibidas inicialmente no ListView. Porém, ao fazermos um scroll do ListView, o Android passará as Views criadas anteriormente para que nós possamos simplesmente substituir os valores nos controles. Isso resulta em uma economia de memória e processamento, uma vez que as Views não precisarão ser criadas novamente, mas sim, serão reaproveitadas do carregamento inicial.

Por fim, uma vez criada a View, nós setamos os valores de cada um dos TextViews, utilizando o método FindViewById com o respectivo ID do TextView (que foi definido no arquivo ListItemRow). Uma vez definidos os valores, nós retornamos a View.

Com o nosso Adapter em mãos, nós podemos utilizá-lo na hora de definirmos o Adapter do nosso ListView (no método OnCreate da MainActivity):

            var listViewGastos = FindViewById<ListView>(Resource.Id.listViewGastos);
            listViewGastos.Adapter = new ListViewAdapter(this, gastos);

O resultado em tempo de execução será este:

Mais customizações no layout do ListView

Agora que nós estamos utilizando um layout customizado para os itens do nosso ListView (o layout ListItemRow), nós podemos começar a brincar com algumas customizações. Por exemplo, vamos configurar uma cor de fundo para o nosso layout (elemento “background” no RelativeLayout) e vamos estabelecer um tamanho de fonte um pouco maior para o valor do gasto (elemento “textSize” do TextView):

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:background="#3e4444"
    android:padding="8dp">
    <LinearLayout
        android:id="@+id/Text"
        android:orientation="horizontal"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingLeft="10dip">
        <TextView
            android:id="@+id/Valor"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="18dip" />
        <TextView
            android:id="@+id/Estabelecimento"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="14dip"
            android:paddingLeft="14dip" />
        <TextView
            android:id="@+id/Data"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="14dip"
            android:paddingLeft="14dip" />
    </LinearLayout>
</RelativeLayout>

Para irmos um pouco além, vamos definir proporções para as “colunas” do nosso ListView, de forma que o nome do estabelecimento ocupe duas vezes mais espaço do que o valor e a data (elemento “layout_weight” dos TextViews – você encontra mais informações sobre essa implementação na documentação do Xamarin):

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:background="#3e4444"
    android:padding="8dp">
    <LinearLayout
        android:id="@+id/Text"
        android:orientation="horizontal"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:paddingLeft="10dip">
      <TextView
          android:id="@+id/Valor"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:textSize="18dip"
          android:layout_weight="1" />
        <TextView
            android:id="@+id/Estabelecimento"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="14dip"
            android:layout_weight="2" />
        <TextView
            android:id="@+id/Data"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="14dip"
            android:layout_weight="1" />
    </LinearLayout>
</RelativeLayout>

Viu como temos toda a flexibilidade que precisamos para definirmos o layout que quisermos? A partir daqui nós poderíamos alterar a implementação para que as linhas fiquem da maneira que precisássemos na nossa aplicação.

Criando um ListView agrupado

Se dermos uma olhada na primeira parte dessa série sobre Xamarin Android, onde definimos o protótipo da aplicação, podemos notar que os dados estão agrupados por data. Porém, até agora nós estamos somente listando os dados no nosso ListView, sem nenhum agrupamento. Como podemos fazer para agruparmos o ListView no Xamarin Android Native?

Procurando por uma maneira de agrupar dados no ListView do Xamarin Android, encontrei diversos tutoriais mostrando como fazer isso com Xamarin Forms. Porém, para Xamarin Native só consegui encontrar um artigo perdido que abordava esse tema. Infelizmente, o ListView do Android não tem um recurso nativo de agrupamento. Dessa forma, teremos que fazer o agrupamento “na mão“.

A primeira coisa que precisaremos é fazer a transformação da nossa lista de gastos em uma estrutura que contenha registros tanto para o cabeçalho do agrupamento (a data) quanto para os detalhes (os gastos). Para armazenarmos essa estrutura, criaremos uma nova classe chamada “ListViewItem“:

    public class ListViewItem
    {
        public int IdGasto { get; set; }
        public DateTime Data { get; set; }
        public string NomeEstabelecimento { get; set; }
        public decimal Valor { get; set; }
        public bool Header { get; set; }
    }

Carregaremos uma lista com instâncias dessa classe utilizando a seguinte sistemática: para cada data, teremos um registro na lista contendo somente a informação da data e a propriedade “Header” configurada como “true“. Depois de cada registro de header, nós teremos os gastos em si (que terão todas as propriedades configuradas, além da propriedade “Header” configurada como “false“). Veja como fica o código para fazer a transformação da nossa lista de gastos em uma lista de “ListViewItems” (que devemos adicionar na nossa classe “MainActivity“):

        private List<ListViewItem> PrepararListViewItems(List<Models.Gasto> gastos)
        {
            var listViewItems = new List<ListViewItem>();

            if (gastos.Any())
            {
                var gastosOrdenados = gastos.OrderBy(g => g.Data).ToList();
                var primeiroGasto = gastosOrdenados.First();

                listViewItems.Add(new ListViewItem() { Header = true, Data = primeiroGasto.Data });
                listViewItems.Add(new ListViewItem() { IdGasto = primeiroGasto.Id, Data = primeiroGasto.Data, NomeEstabelecimento = primeiroGasto.Estabelecimento.Nome, Valor = primeiroGasto.Valor });

                var gastoAnterior = primeiroGasto;
                for (int c = 1; c <= gastosOrdenados.Count - 1; c++)
                {
                    var gastoAtual = gastosOrdenados;

                    if (gastoAtual.Data.Date != gastoAnterior.Data.Date)
                    {
                        listViewItems.Add(new ListViewItem() { Header = true, Data = gastoAtual.Data });
                    }

                    listViewItems.Add(new ListViewItem() { IdGasto = gastoAtual.Id, Data = gastoAtual.Data, NomeEstabelecimento = gastoAtual.Estabelecimento.Nome, Valor = gastoAtual.Valor });

                    gastoAnterior = gastoAtual;
                }
            }

            return listViewItems;
        }

Parece complicado, mas não é. Se você não tiver entendido, não se preocupe. Mais para a frente nesse próprio artigo nós veremos uma maneira mais simples de resolver esse problema.

Uma vez criada a lista que alimentará o nosso ListView agrupado, o próximo passo é ajustarmos o seu layout. Para criarmos um ListView agrupado no Xamarin Android Native, nós precisamos de dois layouts. Um layout será utilizado para os cabeçalhos de grupo (no nosso caso, as datas) e o outro layout será utilizado para as linhas de detalhe do grupo. Como nós já temos um layout para as linhas de detalhe (o arquivo ListItemRow.axml), vamos ajustá-lo, de forma que nós não tenhamos mais a data do gasto:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:background="#3e4444"
    android:padding="8dp">
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:paddingLeft="10dip">
        <TextView
            android:id="@+id/Valor"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="18dip"
            android:layout_weight="1" />
        <TextView
            android:id="@+id/NomeEstabelecimento"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="14dip"
            android:layout_weight="2" />
    </LinearLayout>
</RelativeLayout>

Em seguida, vamos criar um novo layout para o cabeçalho do agrupamento. Dentro da pasta layout, adicione um novo Android Layout dando no nome de “ListItemGroupHeaderRow“:

Nesse layout, nós só teremos um TextView, que servirá para exibirmos a data. A única customização que eu fiz nesse TextView foi a configuração da fonte (tamanho 20dip, cor holo_blue_light):

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:background="#3e4444"
    android:padding="8dp">
    <TextView
        android:id="@+id/Data"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="20dip"
        android:textColor="@android:color/holo_blue_light" />
</RelativeLayout>

O próximo passo é ajustarmos o nosso adapter. Você lembra que anteriormente nós tínhamos um adapter de gastos (BaseAdapter<Models.Gasto>)? Pois é, agora o nosso adapter será um BaseAdapter<ListViewItem>, que é justamente a classe que criamos logo acima. A única diferença desse adapter estará na implementação do método “GetView“:

    public class ListViewAdapter : BaseAdapter<ListViewItem>
    {
        List<ListViewItem> items;
        Activity context;
        public ListViewAdapter(Activity context, List<ListViewItem> items) : base()
        {
            this.context = context;
            this.items = items;
        }
        public override long GetItemId(int position)
        {
            return position;
        }
        public override ListViewItem this[int position]
        {
            get { return items[position]; }
        }
        public override int Count
        {
            get { return items.Count; }
        }
        public override View GetView(int position, View convertView, ViewGroup parent)
        {
            var item = items[position];
            View view = convertView;
            if (item.Header)
            {
                view = context.LayoutInflater.Inflate(Resource.Layout.ListItemGroupHeaderRow, null);
                view.FindViewById<TextView>(Resource.Id.Data).Text = string.Format("{0:d}", item.Data);
            }
            else
            {
                view = context.LayoutInflater.Inflate(Resource.Layout.ListItemRow, null);
                view.FindViewById<TextView>(Resource.Id.Valor).Text = string.Format("{0:c}", item.Valor);
                view.FindViewById<TextView>(Resource.Id.NomeEstabelecimento).Text = item.NomeEstabelecimento;
            }

            return view;
        }
    }

Note que agora nós temos que tomar uma decisão antes de retornarmos a View. Dependendo do valor da propriedade “Header“, nós retornaremos a View correspondente à linha de cabeçalho (quando “Header” for “true“) ou a View correspondente à linha de detalhe (quando “Header” for “false“). Além disso, nós só preencheremos os valores correspondentes de cada nível (no cabeçalho nós preenchemos a data e no detalhe nós preenchemos o valor e estabelecimento).

Por fim, temos que mudar o código onde nós configuramos o adapter da ListView. Nós precisamos primeiramente preparar a lista de “ListViewItems” (utilizando o método “PrepararListViewItems” que criamos anteriormente). Em seguida, nós passamos essa lista para o construtor do nosso adapter, que por sua vez, será utilizado na propriedade “Adapter” do ListView:

            var listViewGastos = FindViewById<ListView>(Resource.Id.listViewGastos);
            var listViewItems = PrepararListViewItems(gastos);
            listViewGastos.Adapter = new ListViewAdapter(this, listViewItems);

Veja só o resultado dessas alterações:

Alterando a cultura da aplicação

Uma coisa que estava me irritando um pouco é a questão do idioma. Note na imagem acima que os dados monetários estão sendo exibidos no formato em inglês americano e a data também está sendo exibida no formato americano. Como é que podemos mudar isso? Simples! Da mesma forma que configuramos a cultura da thread em aplicações desktop, nós podemos configurar a cultura nas aplicações Xamarin Android também!

Para alterarmos a cultura em uma aplicação Xamarin Android, basta configurarmos as propriedades “CurrentCulture” e “CurrentUICulture” no método “OnCreate” da Activity principal da aplicação (no nosso caso, a “MainActivity“). Por exemplo, para alterarmos a cultura para português do Brasil, basta adicionarmos o seguinte código no início do método “OnCreate“:

            var culture = new System.Globalization.CultureInfo("pt-BR");
            System.Threading.Thread.CurrentThread.CurrentCulture = culture;
            System.Threading.Thread.CurrentThread.CurrentUICulture = culture;

O resultado será este:

No meu caso, como eu vou utilizar essa aplicação na Alemanha (com moeda em formato Euros), eu configurei o código da cultura como “de-DE” e obtive o seguinte resultado:

Utilizando um ExpandableListView

A princípio, eu achei que esse artigo iria terminar por aqui. Porém, eu fiquei um pouco incomodado com a gambiarra que tivemos que fazer para agruparmos o ListView. Eu achei que pudesse ter um jeito mais fácil de implementar essa funcionalidade. Além disso, do jeito que implementamos o método “GetView” no exemplo acima, nós estaremos sempre criando Views novas, ao invés de utilizarmos o reaproveitamento de Views do Android ao fazermos um scroll do ListView. Isso pode acabar trazendo problemas de performance e memória em listas grandes (não será o nosso caso, mas acho válido já aprender uma maneira mais otimizada para implementarmos o agrupamento).

Quando eu estava preparando as imagens dos layouts nativos do ListView (exibidos no início do artigo), eu percebi a existência do layout “SimpleExpandableListItem“. Depois de pesquisar melhor esse termo, descobri que existe, sim, uma maneira mais fácil de implementarmos um ListView agrupado no Xamarin Android Native. Basta utilizarmos um ExpandableListView, ao invés do ListView comum! Agora que eu aprendi como utilizá-lo, vou ensinar aqui os meus aprendizados com esse controle também.

A primeira coisa que temos que fazer é alterarmos o tipo do controle no layout da tela “Main“. Ao invés de “ListView“, nós declararemos um “ExpandableListView“:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:minWidth="25px"
    android:minHeight="25px">
    <ExpandableListView
        android:minWidth="25px"
        android:minHeight="25px"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/listViewGastos" />
</LinearLayout>

Em seguida, nós ajustaremos a maluquice que criamos anteriormente com a classe “ListViewItem“, separando agora em duas classes. A classe “ListViewItem” representará os filhos do agrupamento e a (nova) classe “ListViewGroup” representará os cabeçalhos do agrupamento. Cada “ListViewGroup” terá uma coleção de “ListViewItems“. Aqui vai a estrutura dessas duas classes:

    public class ListViewItem
    {
        public int IdGasto { get; set; }
        public string NomeEstabelecimento { get; set; }
        public decimal Valor { get; set; }
    }
    public class ListViewGroup
    {
        public DateTime Data { get; set; }
        public List<ListViewItem> ListViewItems { get; set; }

        public ListViewGroup()
        {
            ListViewItems = new List<ListViewItem>();
        }
    }

A criação da coleção com os “ListViewGroups” tomando como base a nossa lista de gastos ficou agora mais simples:

        private List<ListViewGroup> PrepararListViewGroups(List<Models.Gasto> gastos)
        {
            var listViewGroups = new List<ListViewGroup>();

            if (gastos.Any())
            {
                var gastosOrdenados = gastos.OrderBy(g => g.Data);

                Models.Gasto gastoAnterior = null;
                ListViewGroup grupoAtual = null;
                foreach (var gasto in gastosOrdenados)
                {
                    if (gastoAnterior == null || gasto.Data.Date != gastoAnterior.Data.Date)
                    {
                        grupoAtual = new ListViewGroup() { Data = gasto.Data };
                        listViewGroups.Add(grupoAtual);
                    }

                    grupoAtual.ListViewItems.Add(new ListViewItem() { IdGasto = gasto.Id, NomeEstabelecimento = gasto.Estabelecimento.Nome, Valor = gasto.Valor });
                    gastoAnterior = gasto;
                }
            }

            return listViewGroups;
        }

Se utilizarmos LINQ para agruparmos os nossos gastos, esse código acaba ficando ainda mais simples:

        private List<ListViewGroup> PrepararListViewGroups(List<Models.Gasto> gastos)
        {
            var listViewGroups = new List<ListViewGroup>();

            if (gastos.Any())
            {
                var gastosAgrupados = from gasto in gastos
                                      group gasto by gasto.Data.Date into grupoDeGastos
                                      select new
                                      {
                                          Data = grupoDeGastos.Key,
                                          Gastos = grupoDeGastos
                                      };

                foreach (var gastoAgrupado in gastosAgrupados)
                {
                    var listViewGroup = new ListViewGroup() { Data = gastoAgrupado.Data };
                    listViewGroups.Add(listViewGroup);

                    foreach (var gasto in gastoAgrupado.Gastos)
                    {
                        listViewGroup.ListViewItems.Add(new ListViewItem() { IdGasto = gasto.Id, NomeEstabelecimento = gasto.Estabelecimento.Nome, Valor = gasto.Valor });
                    }
                }
            }

            return listViewGroups;
        }

A próxima alteração será no nosso Adapter. Ao invés de trabalharmos com um BaseAdapter, nós utilizaremos agora um BaseExpandableListAdapter. Essa classe divide a criação das Views mestre e detalhe em duas etapas. Veja só como é que fica o código:

    public class ListViewAdapter : BaseExpandableListAdapter
    {
        List<ListViewGroup> grupos;
        Activity context;
        public ListViewAdapter(Activity context, List<ListViewGroup> grupos) : base()
        {
            this.context = context;
            this.grupos = grupos;
        }

        public override int GroupCount
        {
            get
            {
                return grupos.Count;
            }
        }

        public override bool HasStableIds
        {
            get
            {
                return true;
            }
        }

        public override Java.Lang.Object GetChild(int groupPosition, int childPosition)
        {
            throw new NotImplementedException();
        }

        public override long GetChildId(int groupPosition, int childPosition)
        {
            return childPosition;
        }

        public override int GetChildrenCount(int groupPosition)
        {
            return grupos[groupPosition].ListViewItems.Count;
        }

        public override View GetChildView(int groupPosition, int childPosition, bool isLastChild, View convertView, ViewGroup parent)
        {
            View view = convertView;
            var item = grupos[groupPosition].ListViewItems[childPosition];

            if (view == null)
            {
                view = context.LayoutInflater.Inflate(Resource.Layout.ListItemRow, null);
            }
            view.FindViewById<TextView>(Resource.Id.Valor).Text = string.Format("{0:c}", item.Valor);
            view.FindViewById<TextView>(Resource.Id.NomeEstabelecimento).Text = item.NomeEstabelecimento;

            return view;
        }

        public override Java.Lang.Object GetGroup(int groupPosition)
        {
            throw new NotImplementedException();
        }

        public override long GetGroupId(int groupPosition)
        {
            return groupPosition;
        }

        public override View GetGroupView(int groupPosition, bool isExpanded, View convertView, ViewGroup parent)
        {
            View view = convertView;
            var item = grupos[groupPosition];

            if (view == null)
            {
                view = context.LayoutInflater.Inflate(Resource.Layout.ListItemGroupHeaderRow, null);
            }
            view.FindViewById<TextView>(Resource.Id.Data).Text = string.Format("{0:d}", item.Data);

            return view;
        }

        public override bool IsChildSelectable(int groupPosition, int childPosition)
        {
            return true;
        }
    }

As propriedades e métodos importantes nessa implementação são:

GroupCount: retornamos a quantidade de agrupamentos, ou seja, a quantidade de “ListItemGroups” da nossa coleção
GetChildrenCount: retornamos a quantidade de filhos de um determinado grupo
GetChildView: onde criamos (ou reaproveitamos) a View de detalhes e preenchemos os valores nos campos correspondentes
GetGroupView: onde criamos (ou reaproveitamos) a View de cabeçalho e preenchemos a data no TextView correspondente

Além disso, temos que implementar também as seguintes propriedades com valores padrão:

HasStableIds: retornando sempre “true” (não sei a utilidade dessa propriedade)
GetChildId: retornando o valor de “childPosition
GetGroupId: retornando o valor de “groupPosition
IsChildSelectable: retornando sempre “true“, para habilitar a seleção (clique) nos itens de detalhe

Por fim, temos que ajustar o método onde configuramos o adapter para o nosso ListView. Agora nós não podemos mais configurar através da propriedade “Adapter” do ListView, mas sim, através do método “SetAdapter” do ExpandableListView:

            var listViewGastos = FindViewById<ExpandableListView>(Resource.Id.listViewGastos);
            var listViewGroups = PrepararListViewGroups(gastos);
            var adapter = new ListViewAdapter(this, listViewGroups);
            listViewGastos.SetAdapter(adapter);

Um último detalhe que temos que ajustar antes de executarmos a nossa aplicação é o padding do TextView que exibirá a data. Se não configurarmos um padding à esquerda para esse TextView, o valor da data sairá em cima da “setinha” que faz a expansão e recolhimento do grupo. Por isso, vamos alterar o código do layout “ListItemGroupHeaderRow“, adicionando o elemento “paddingLeft” com o valor de 20dp:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:background="#3e4444"
    android:padding="8dp">
    <TextView
        android:id="@+id/Data"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="20dip"
        android:textColor="@android:color/holo_blue_light" 
        android:paddingLeft="20dp"/>
</RelativeLayout>

Ao executarmos a aplicação, este será o resultado:

Note que os grupos do ExpandableListView vêm recolhidos por padrão. Para que os grupos sejam automaticamente expandidos, temos que adicionar o seguinte código logo após a configuração do adapter:

            for (int group = 0; group <= adapter.GroupCount - 1; group++)
            {
                listViewGastos.ExpandGroup(group);
            }

Pronto! Execute a aplicação novamente e veja o resultado:

E com isso temos o nosso ListView agrupado sem gambiarras e otimizado de forma que a memória e o processamento não sejam desperdiçados ao fazermos um scroll no ListView.

Acertando o tamanho das colunas do ListView

Uma última coisa que estava me irritando é o fato que as informações das colunas do ListView não estavam sendo exibidas de maneira alinhada. Perceba na imagem acima que o estabelecimento na terceira linha de detalhe está um pouco desalinhado à esquerda (porque o valor do gasto só contém três posições). Como é que resolvemos isso?

Depois de assistir a um vídeo do João Ribeiro onde ele mostra a criação de ListViews personalizadas no Android, consegui finalmente entender o que estava errado na minha implementação. Primeiramente, eu não tinha dado um peso total para o meu LinearLayout dentro do arquivo “ListItemRow.axml” (elemento “weightSum“). Além disso, o “layout_width” dos TextViews estavam configurados como “wrap_content“, quando na verdade eles precisam estar configurados como “match_parent” para ter o efeito que eu estava querendo. E, por fim, os pesos para cada elemento do LinearLayout devem ser definidos da maneira inversa, ou seja, quanto maior o valor do peso, menor será a área destinada para o controle dentro do LinearLayout.

Veja o conteúdo do layout ListItemRow depois de ter aplicado todas essas alterações:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:background="#3e4444"
    android:padding="8dp">
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:paddingLeft="10dip"
        android:weightSum="3">
        <TextView
            android:id="@+id/Valor"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="18dip"
            android:layout_weight="2" />
        <TextView
            android:id="@+id/NomeEstabelecimento"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="14dip"
            android:layout_weight="1" />
    </LinearLayout>
</RelativeLayout>

E com isso, finalmente, nós teremos o layout alinhado corretamente, independentemente da quantidade de dígitos:

Acesse o código no GitHub

Normalmente eu disponibilizo os códigos de exemplo dos artigos e vídeos somente para as pessoas que se inscrevem na minha newsletter. Porém, como nessa série nós evoluiremos o projeto em cada artigo, achei mais interessante publicar o código no GitHub, onde todos poderão acompanhar a evolução do projeto quando novos artigos forem sendo publicados. Caso você se interesse, acesse o repositório desse projeto no meu GitHub.

Concluindo

Enquanto nós não trabalhamos com agrupamentos, a customização do controle ListView no Xamarin Android Native não é tão complicada. Basta criarmos o arquivo de layout em conjunto com o adapter correspondente e pronto, nosso ListView será exibido com sucesso.

Entretanto, a história muda completamente quando decidimos agrupar os dados exibidos na ListView. Como não existe uma maneira nativa de agruparmos os dados, nós temos que fazer toda a implementação “na mão“. Uma alternativa é utilizarmos um ExpandableListView, que facilita um pouco a nossa vida.

No artigo de hoje você viu como trabalhar com layouts customizados no ListView do Android, bem como o conceito de adapters. Além disso, nós vimos uma maneira de agruparmos os dados do ListView e, por fim, nós vimos como nós podemos utilizar um ExpandableListView como alternativa ao ListView quando precisamos agrupar os dados.

De bônus, você conferiu também como alterar a cultura da aplicação, de forma que os dados monetários e de data venham no formato esperado.

Espero que você tenha gostado e que você tenha entendido todos os conceitos apresentados no artigo. Se você ficar com alguma dúvida, tiver algo a acrescentar ou alguma crítica, é só utilizar a caixa de comentários!

Por fim, convido você a inscrever-se na minha newsletter. Ao fazer isso, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário logo abaixo.

Acesse os próximos artigos da série

Se você quiser acessar os próximos artigos dessa série, utilize os links abaixo:

Parte 5: Navegação entre telas

Até a próxima!

André Lima

Newsletter do André Lima

* indicates required



Powered by MailChimp

11 thoughts on “Aplicações Android com Xamarin – Parte 4 de N – Customizando o controle ListView

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *