segunda-feira, 28 de março de 2011

Criando um ListAdapter Customizado para o ListView do Android

Post atualizado em 06/09/2011.

O widget ListView é um componente poderoso e flexível do Android, normalmente utilizado para exibir uma simples lista de dados. Este post descreve como criar um ListAdapter para exibir várias informações e uma figura em cada linha do ListView.

Para demonstrar a criação do ListAdapter customizado foi criado um projeto para exibir em um ListView os estados do Brasil, juntamente com a abreviação, nome da capital, área em km2 e a imagem da bandeira do estado. O projeto rodando apresenta a seguinte tela:

device3

O projeto possui somente uma Activity que carrega o ListView no método OnCreate() demonstrado no arquivo Main.java.

public class Main extends ListActivity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        List<State> stateList = new ArrayList<State>();
        
        for (int i = 0; i < states.length; i++) {
            State state = new State();
            state.setState(states[i][0]);
            state.setAbbreviation(states[i][1]);
            state.setCapital(states[i][2]);
            state.setArea(Float.parseFloat(states[i][3]));
            state.setBanner(images[i]);
            
            stateList.add(state);
        }  
        
        setListAdapter(new StateAdapter(this, stateList));
    }
    
    private String[][] states = new String[][]{
           {"Acre", "AC", "Rio Branco", "152581.4"},
           {"Alagoas", "AL", "Maceió", "27767.7"},
           {"Amapá", "AP", "Macapá", "142814.6"},
           {"Amazonas", "AM", "Manaus", "1570745.7"},
           {"Bahia", "BA", "Salvador", "564692.7"},
           {"Ceará", "CE", "Fortaleza", "148825.6"},
           {"Distrito Federal", "DF", "Brasília", "5822.1"},
           {"Espírito Santo", "ES", "Vitória", "46077.5"},
           {"Goiás", "GO", "Goiânia", "340086.7"},
           {"Maranhão", "MA", "São Luís", "331983.3"},
           {"Mato Grosso", "MT", "Cuiabá", "903357.9"},
           {"Mato Grosso do Sul", "MS", "Campo Grande", "357125.0"},
           {"Minas Gerais", "MG", "Belo Horizonte", "586528.3"},
           {"Pará", "PA", "Belém", "1247689.5"},
           {"Paraíba", "PB", "João Pessoa", "56439.8"},
           {"Paraná", "PR", "Curitiba", "199314.9"},
           {"Pernambuco", "PE", "Recife", "98311.6"},
           {"Piauí", "PI", "Teresina", "251529.2"},
           {"Rio de Janeiro", "RJ", "Rio de Janeiro", "43696.1"},
           {"Rio Grande do Norte", "RN", "Natal", "52796.8"},
           {"Rio Grande do Sul", "RS", "Porto Alegre", "281748.5"},
           {"Rondônia", "RO", "Porto Velho", "237576.2"},
           {"Roraima", "RR", "Boa Vista", "224299.0"},
           {"Santa Catarina", "SC", "Florianópolis", "95346.2"},
           {"São Paulo", "SP", "São Paulo", "248209.4"},
           {"Sergipe", "SE", "Aracaju", "21910.3"},
           {"Tocantins", "TO", "Palmas", "277620.9"}
       };
    
    private int[] images = new int[]{
            R.drawable.acre,
            R.drawable.alagoas,
            R.drawable.amapa,
            R.drawable.amazonas,
            R.drawable.bahia,
            R.drawable.ceara,
            R.drawable.distritofederal,
            R.drawable.espiritosanto,
            R.drawable.goias,
            R.drawable.maranhao,
            R.drawable.matogrosso,
            R.drawable.matogrossosul,
            R.drawable.minasgerais,
            R.drawable.para,
            R.drawable.paraiba,
            R.drawable.parana,
            R.drawable.pernambuco,
            R.drawable.piaui,
            R.drawable.riojaneiro,
            R.drawable.riograndenorte,
            R.drawable.riograndesul,
            R.drawable.rondonia,
            R.drawable.roraima,
            R.drawable.santacatarina,
            R.drawable.saopaulo,
            R.drawable.sergipe,
            R.drawable.tocatins
    };
}

No método OnCreate() foi criado um ArrayList com as informações dos estados. Normalmente este tipo de informação é carregada de uma base de dados ou de alguma outra fonte de informação, mas como o objetivo é demonstrar a utilização do ListView, foi criado um array com as informações dos estados (states) e outro array com as imagens das bandeiras (images). As imagens foram obtidas dos arquivos colocados na pasta de resource res/drawable-mdpi.

Para montar o ArrayList foi utilizada a classe State, descrita no arquivo State.java:

public class State {
    private String state;
    private String abbreviation;
    private String capital;
    private float area;
    private int banner;
    
    public String getState() {
        return state;
    }
    public void setState(String state) {
        this.state = state;
    }
    public String getAbbreviation() {
        return abbreviation;
    }
    public void setAbbreviation(String shortening) {
        this.abbreviation = shortening;
    }
    public String getCapital() {
        return capital;
    }
    public void setCapital(String capital) {
        this.capital = capital;
    }
    public float getArea() {
        return area;
    }
    public void setArea(float area) {
        this.area = area;
    }
    public int getBanner() {
        return banner;
    }
    public void setBanner(int banner) {
        this.banner = banner;
    }
}

O cursor para as informações do ListView é montado utilizando o método setListAdapter(ListAdapter adapter) da classe ListActivity. No parâmetro adapter foi utilizada a classe StateAdapter que recebe como parâmetros o contexto da Activity e o ArrayList com as informações dos estados. O arquivo StateAdapter.java descreve os métodos que são necessários para montar o cursor do ListView. 

/** 
 * Adapter utilizado para exibir as informações dos Estados
 * no ListView.
 * @author Administrador
 *
 */
public class StateAdapter extends BaseAdapter {
    private Context context;
    private List<State> stateList;
    
    public StateAdapter(Context context, List<State> statelist){
        this.context = context;
        this.stateList = statelist;
    }
    
    @Override
    public int getCount() {
        return stateList.size();
    }
 
    @Override
    public Object getItem(int position) {
        return stateList.get(position);
    }
 
    @Override
    public long getItemId(int position) {
        return position;
    }
 
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        // Recupera o estado da posição atual
        State state = stateList.get(position);
        
        // Cria uma instância do layout XML para os objetos correspondentes
        // na View
        LayoutInflater inflater = (LayoutInflater)
            context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View view = inflater.inflate(R.layout.listview_states, null);
        
        // Estado - Abreviação
        TextView textState = (TextView)view.findViewById(R.id.textState);
        textState.setText(state.getState() + " - " + state.getAbbreviation());
        
        // Capital
        TextView textCapital = (TextView)view.findViewById(R.id.textCapital);
        textCapital.setText(state.getCapital());
        
        // Área
        TextView textArea = (TextView)view.findViewById(R.id.textArea);
        textArea.setText(String.valueOf(state.getArea()));
        
        // Bandeira
        ImageView img = (ImageView)view.findViewById(R.id.imageState);
        img.setImageResource(state.getBanner());
 
        return view;
    }
}

A classe StateAdapter extende a classe BaseAdapter que deve implementar os métodos:

  •  getCount(): retorna o número de itens.
  • getItem(int position): retorna o item de uma posição específica. 
  •  getItemId(int position): retorna o Id de um item de uma posição específica.
  • getView(int position, View convertView, ViewGroup parent): retorna a View com as informações posicionadas de acordo com o layout montado no arquivo listview_states.xml

O arquivo listview_states.xml monta o layout que é utilizado em cada linha do ListView, para posicionar as informações em um formato customizado.

<?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="5dp"
  android:background="#cccccc"
  >
  
  <ImageView
      android:id="@+id/imageState"
      android:layout_width="60dp"
      android:layout_height="42dp"
      android:background="#ffffff"
      android:scaleType="centerCrop"
  />
  
  <TextView
      android:id="@+id/textState"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
      android:layout_toRightOf="@+id/imageState"
      android:layout_alignTop="@+id/imageState"
      android:text="São Paulo - SP"
      android:textSize="20sp"    
      android:textColor="#333333"
      android:paddingLeft="5dp"  
  />
  
  <TextView
      android:id="@+id/textCapital"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_below="@+id/textState"
      android:layout_toRightOf="@+id/imageState"
      android:text="São Paulo"
      android:textColor="#333333"
      android:paddingLeft="5dp" 
  />
  
  <TextView
      android:id="@+id/textArea"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_below="@+id/textState"
      android:layout_alignParentRight="true"
      android:text="100000"
      android:textColor="#333333"
  />  
</RelativeLayout>

No método getView utilizando a classe LayoutInflater e a classe View podemos associar cada widget do layout à informação que deve ser exibida no ListView.

No exemplo deste post, quando um item do ListView recebe o foco a tradicional cor “Laranja” de fundo não aparece. Isto acontece porque o ListView possui uma cor de fundo opaca, neste caso “cinza”. Na verdade o foco “Laranja” é exibido, mas somente atrás do background do ListView.

Uma possível solução para exibir o fundo “Laranja” neste caso, é fazer uma pequena alteração da definição do layout da tela.

Primeiro precisamos criar um arquivo tipo StateList para definir as cores que serão apresentadas quando o item do ListView for pressionado, selecionado ou receber foco. Para isso foi criado o arquivo list_selector.xml, que deve ser colocado no diretório res/drawable

<?xml version="1.0" encoding="utf-8"?>
<selector
    xmlns:android="http://schemas.android.com/apk/res/android">
     <item
        android:state_pressed="true"
        android:drawable="@android:color/transparent" />
     <item
        android:state_selected="true"
        android:drawable="@android:color/transparent" />
     <item
        android:state_focused="true"
        android:drawable="@android:color/transparent" />
     <item
        android:drawable="@color/cinza" />
 </selector>

Os itens relativos aos estados pressionado, selecionado ou com focus devem ser configurados com o atributo android:drawable com cor transparente, somente o último item que representa o item sem mudança de estado deve ser configurado com a cor cinza. A cor cinza foi declarada no arquivo color.xml que deve ser colocado no diretório res/values.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="cinza">#cccccc</color>
</resources>

Para que o fundo “laranja” funcione altere no arquivo listview_states.xml o atributo android:drawable da tag RelativeLayout de android:background="#cccccc" para android:background="@drawable/list_selector".

Agora quando um item for clicado teremos a seguinte tela:

listviewfundolaranja

13 comments:

  1. Muito didático o exemplo, Rosana ! E pensar que procurei bastante em inglês para achar algo tão bem escrito em português ! Parabéns.

    ResponderExcluir
  2. muito bom o exemplo mesmo, só fiquei com uma dúvida, utilizei o exemplo acima para fazer uma lista parecida que eu precisava, sendo que minha lista não fica laranja quando clicamos no item, na verdade ela não fica de cor nenhuma, você pode me ajudar nessa informação?

    ResponderExcluir
  3. Fiz uma atualização do post apresentando uma possível solução para a exibição da cor laranja quando um item do ListView é clicado.

    ResponderExcluir
  4. Muito legal esse post.
    Você acha dá para usar como menu principal da aplicação neste estilo ao invés de botões ?

    ResponderExcluir
  5. Luis,
    Acho que pode ser utilizado sim, sem problemas, inclusive você pode trabalhar o layout das linhas do listview. Dê uma olhada no post http://romarconsultoria.blogspot.com/2011/08/customizando-cor-de-fundo-de-um-widget.html, onde descrevo uma forma de alterar a cor de fundo quando um item é clicado. Para o ListView a utilização é similar a dos botões demonstrado no post.

    ResponderExcluir
  6. Rosana,

    Faço coro junto com o Marcelo. Procurei na internet e até achei exemplos em inglês mas não achei nada tão bem escrito e ilustrado.

    Muito obrigado!

    ResponderExcluir
  7. Muito bom o artigo. Porem, ficou uma dúvida. Como posso saber qual elemento da lista foi clicado?

    ResponderExcluir
  8. Rjun,
    Para saber qual elemento da lista foi clicado, você deve chamar o método setOnItemClickListener do ListView e fazer o Override do método onItemClick, este método passa como parâmetros as informações de posição e do item clicado.

    Você pode ver um exemplo da implementação destes métodos no post http://romarconsultoria.blogspot.com/2011/08/utilizando-o-listview-para-selecionar.html na classe Main.

    ResponderExcluir
  9. Pergunta de iniciante: Onde o método getView é chamado?

    ResponderExcluir
  10. A classe StateAdapter fornece acesso aos dados e também é responsável por fazer uma View para cada item do dataset, por isso o método getView é chamado em todos os momentos que os dados do ListAdapter forem utilizados, por exemplo no método onCreate o método getView é chamado para cada item da lista de estados.

    ResponderExcluir
  11. Olá Rosana, parabenizo-lhe pelo Post.
    Tentei repetir este exemplo e na linha "View view = inflater.inflate(R.layout.listview_states, null);" da classe StateAdapater surge um erro, e as duas sugeridas para solucionar são: "Migrate Android Code e Rename.
    Lembra-se de algo parecido em alguns dos seus códigos?
    Atencipadamente agradeço pela atenção.
    Abraço.
    Ana Dolores

    ResponderExcluir
  12. Ana,
    Não vi este erro ainda. Você pode postar a mensagem de erro que você está recebendo?

    ResponderExcluir
  13. Rosane, primeiramente parabéns pelo post, me ajudou muito..

    Mas uma dúvida ainda está me dando trabalho.

    A parte de selecionar e ficar laranja simplesmente não deu certo.
    Está faltando algo?

    ResponderExcluir