segunda-feira, 15 de julho de 2013

ListView com seleção múltipla + ActionBar + ActionMode

Olá povo,

Depois de quase um mês sem postar, resolvi aproveitar o feriadão para colocar um post colaborativo e que me tirou um trauma. Senta que lá vem história...
Um dia desses, meu amigo André Melo (a.k.a. Deco Balaca) me perguntou como fazia uma ListView com seleção múltipla, e que os itens selecionados ficassem com o background diferente. Quatro anos mexendo com o danado do Android e me enrolei pra fazer isso.
A classe ListView tem uma propriedade android:listSelector que nos permite modificar o visual dos itens da lista de acordo com o seu estado (eu falei de selector nesse post aqui).
O problema é que apesar de haver a propriedade android:state_checked (usado também em componentes como Checkbox e ToggleButton), devemos usar a propriedade android:state_activated para indicar que um item da lista está checado (#tenso).
Muito bem! Tudo resolvido? Sim, se você não quiser dar suporte a versões anteriores à 3.0 (Honeycomb), caso contrário você terá que fazer as coisas manualmente.
Meu aluno do TECDAM, Carlos Eduardo Carneiro, me deu essa dica. Abaixo temos um adapter que verifica se a linha está checada, em caso positivo, ele muda o background da View da linha correspondente.
class MultiSelectAdapter extends ArrayAdapter<String>{

  public MultiSelectAdapter(
    Context context, 
    int textViewResourceId,
    List<String> objects) {

    super(context, textViewResourceId, objects);
  }

  @Override
  public View getView(int position, 
    View convertView, ViewGroup parent) {

    View v = super.getView(
      position, convertView, parent);

    ListView listView = (ListView) parent;
    int color = listView.isItemChecked(position) ? 
      Color.argb(0xFF, 0x31, 0xB6, 0xE7) : 
      Color.TRANSPARENT;

    v.setBackgroundColor(color);

    return v;
  }
}
Estou usando um ArrayAdapter simples, mas essa abordagem funcionaria para um adapter customizado também, só que você setaria o background do layout que você carregou...
Resolvido esse problema, resolvi aproveitar esse post para mostrar como utilizar um recurso muito utilizado em aplicações Android: o Action Mode. Essa é uma característica da ActionBar do Android que permite exibir opções de menu de acordo com a necessidade.
Se tornou praticamente padrão, excluir múltiplos itens de uma lista dando um clique longo em um dos itens e depois selecionar outros (podemos ver isso no Gmail, galeria de mídia, etc.). No nosso exemplo, vou mostrar como permitir excluir múltiplos itens de uma ListView. Mas você pode estar se perguntando: ActionBar não é só pro Android 3.0 ou superior? Dê uma olhada nesse link e conheça o Sherlock, pois vamos usá-lo aqui.
O exemplo consta apenas de uma Activity mostrada (uma parte) abaixo:
public class MainActivity 
  extends SherlockActivity 
  implements 
    OnItemClickListener, 
    OnItemLongClickListener, 
    ActionMode.Callback {

  private ListView listView;
  private List<String> nomes;
  private ActionMode actionMode;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setTheme(R.style.Theme_Sherlock_Light_DarkActionBar);

    nomes = new ArrayList<String>();
    nomes.add("nglauber");
    nomes.add("CESAR.edu");
    nomes.add("CESAR");
    nomes.add("Unibratec");

    listView = new ListView(this);
    listView.setOnItemClickListener(this);
    listView.setOnItemLongClickListener(this);
    listView.setAdapter(new MultiSelectAdapter(this,
      android.R.layout.simple_list_item_1, nomes));
      setContentView(listView);
  }

  @Override
  public void onItemClick(
    AdapterView<?> adapterView, View view,

    int position, long id) {
    if (actionMode == null) {
      // Faça algo ao clicar no item normalmente
    } else {
      int checkedCount =  
        atualizarItensMarcados(listView, position);

      if (checkedCount == 0) {
        actionMode.finish();
      }
    }
  }

  @Override
  public boolean onItemLongClick(
    AdapterView<?> adapterView, View view,
    int position, long id) {

    boolean consumed = (actionMode == null);

    if (consumed) {
      actionMode = startActionMode(this);
      listView.setChoiceMode(
        ListView.CHOICE_MODE_MULTIPLE);

      listView.setItemChecked(position, true);
      atualizarItensMarcados(listView, position);
    }
    return consumed;
  }
A nossa classe implementa três interfaces, onde as duas primeiras são bem conhecidas. A última é que vamos detalhar mais no próximo trecho de código. O atributo actionMode é que vai exibir a opção de excluir e mostrar quantos itens estão selecionados. Esse atributo é inicializado no método onItemLongClick através da chamada do método startActionMode. Nesse momento, habilitamos a seleção múltipla na ListView e já checamos o item da posição onde clicamos.
Sendo assim, no método onItemClick, nós verificamos se actionMode é igual a null, nesse caso, não estamos com o ActionMode ativo na ActionBar e devemos processar o clique normalmente. Caso contrário, atualizamos a lista com novo item que foi clicado. Mas se não houver mais itens selecionados, desativamos o ActionMode chamando o método finish.

Vamos agora ver o código da interface ActionMode.Callback e o método atualizarItensMarcados.
  @Override
  public boolean onCreateActionMode(
    ActionMode mode, Menu menu) {

    getSupportMenuInflater().inflate(
      R.menu.menu_delete_list, menu);

    return true;
  }

  @Override
  public boolean onPrepareActionMode(
    ActionMode mode, Menu menu) {

    return false;
  }

  @Override
  public boolean onActionItemClicked(
    ActionMode mode, MenuItem item) {

    if (item.getItemId() == R.id.action_delete) {
      SparseBooleanArray checked = 
        listView.getCheckedItemPositions();

      for (int i = checked.size()-1; i >= 0; i--) {
        if (checked.valueAt(i)) {
          nomes.remove(checked.keyAt(i));
        }
      }
      actionMode.finish();
      return true;
    }
    return false;
  }

  @Override
  public void onDestroyActionMode(ActionMode mode) {
    actionMode = null;
    listView.clearChoices();
    ((BaseAdapter) listView.getAdapter())
      .notifyDataSetChanged();

    listView.setChoiceMode(ListView.CHOICE_MODE_NONE);
  }

  private int atualizarItensMarcados(
    ListView l, int position) {

    SparseBooleanArray checked = 
      l.getCheckedItemPositions();

    l.setItemChecked(position, 
      l.isItemChecked(position));

    int checkedCount = 0;
    for (int i = 0; i < checked.size(); i++) {
      if (checked.valueAt(i)) {
        checkedCount++;
      }
    }

    actionMode.setTitle(
      checkedCount + " selecionados");

    return checkedCount;
  }
}
O método onCreateActionMode permite carregar um arquivo de menu para actionMode, note que ele retorna true para informar que o actionMode pode ser criado. Já o método onPrepareActionMode é usado para quando queremos atualizar o actionMode após sua criação, retornamos falso para indicar que ele não foi atualizado. Quando uma opção de menu do ActionMode é selecionada, o método onActionItemClicked é chamado. Nesse método, checamos se a opção selecionada foi a de excluir, em caso positivo, obtemos a lista das posições da lista que estão marcadas. Removemos essas posições da lista e depois finalizamos o ActionMode.
Ao chamar o método finish do ActionMode, o método onDestroyActionMode é chamado. Nesse momento, setamos o atributo actionMode para null, desmarcamos os itens (caso haja algum), chamamos o notifyDatasetChanged para que a ListView seja redesenhada, e por fim, voltamos o tipo de seleção da lista como nenhum.
O método atualizarItensMarcados apenas retorna a quantidade de itens que estão checados e seta o título da actionMode.

O arquivo de menu utilizado no método onCreateActionMode é mostrado abaixo:
<menu 
  xmlns:android="http://schemas.android.com/apk/res/android">
  <item
    android:id="@+id/action_delete"
    android:icon="@android:drawable/ic_menu_delete"
    android:orderInCategory="100"
    android:showAsAction="ifRoom"/>
</menu>

E o resultado, podemos ver na figura a seguir:

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

11 comentários:

me disse...

Opa, Glauber,

Nesse exemplo, vc não está se importando em manter a seleção enquanto girar o aparelho, não é verdade?

No caso, eu estou usando um SherlockListFragment. Devo resselecionar todo mundo no onActivityCreated?

[]'s

Nelson Glauber de Vasconcelos Leal disse...

Olá me,

Isso mesmo. Você teria que usar uma dessas abordagens:
http://nglauber.blogspot.com.br/2011/10/android-tratando-mudanca-de-orientacao.html

4br4ç05,
nglauber

Anônimo disse...

Glauber, legal seu tutorial. Como que faria se clicasse em algum item da lista abrir novos menus na ActionBar.

Nelson Glauber de Vasconcelos Leal disse...

Oi Anônimo,

Normal... Pois cada Fragment/Activity carrega suas próprias opções de Menu.

4br4ç05,
nglauber

Anônimo disse...

Olá Glauber, tudo bem. Como faço para implementar isto no exercício 10 do seu livro? Já tentei e não consegui.

rubson lima disse...

ola Glauber como faço pra mudar a cor da barra?

Nelson Glauber disse...

Oi Rubson,

Que barra? A de título? Quando está normal ou em modo de seleção?

4br4ç05,
nglauber

Webert Ribeiro disse...

Olá professor,

Gostaria de saber como faço para implantar uma seleção múltipla com o BaseAdapter?

Seria como somar o exercício 10 - Adapter e o exercício 16/19 - SQLITE.

Parabéns pelo livro, estou gostando muito de estudar e praticar.

Abraço, Webert Ribeiro

Nelson Glauber disse...

Oi Webert,

Eu implementei essa funcionalidade no livro no capítulo 7...
https://goo.gl/1YVQF9

4br4ç05,
nglauber

Webert Ribeiro disse...

Entendi,

A funcionalidade para seleção múltipla que me refiro seria para um adaptador customizado.

No livro é dito que o Base Adapter é a superclasse de todos os adaptadores do Android, então para que a seleção múltipla funcione num adaptador customizado, basta incorporar a classe que o senhor criou no capitulo 7 (MultiSelectAdapter) na classe do meu adaptador customizado, como por exemplo, aquele que foi criado no capítulo 4 (CarrosAdapter) ?

Obrigado pela atenção.
Webert

Nelson Glauber disse...

Oi Webert. Isso mesmo ;)