terça-feira, 8 de novembro de 2011

AutoCompleteTextView personalizado

Olá povo,

Estou com um monte de posts legais pra publicar, mas infelizmente (pra variar) estou sem tempo. São coisas que precisei fazer nos projetos que trabalhei, e que gosto de compartilhar com quem lê o blog. Além do mais, me serve como fonte de consulta para uma uso posterior. Afinal de contas, é pra isso que serve o blog (pelo menos pra mim).

Hoje vou mostrar como criar uma consulta personalizada para o componente AutoCompleteTextView. Esse componente é utilizado quando você tem uma lista de valores, e ao invés de selecioná-lo em uma lista, você digita parte do texto e os resultados vão sendo exibidos em uma lista (estilo drop-down) abaixo do componente. Este componente já conta com um mecanismo de busca padrão, entretanto a necessidade de personalizá-lo surgiu quando precisei realizar a busca por nomes de cidades. O cliente solicitou que a acentuação fosse ignorada, ou seja, quando eu digitasse "sao" era para aparecer "São Paulo" nos resultados.

Vamos à implementação! Primeiro vou mostrar o método que substitui os caracteres acentuados pelos mesmos não acentuados.

public static String[] REPLACES = 
{ "a", "e", "i", "o", "u", "c" };

public static Pattern[] PATTERNS = null;

public static void compilePatterns() {
PATTERNS = new Pattern[REPLACES.length];
PATTERNS[0] = Pattern.compile(
"[âãáàä]", Pattern.CASE_INSENSITIVE);
PATTERNS[1] = Pattern.compile(
"[éèêë]", Pattern.CASE_INSENSITIVE);
PATTERNS[2] = Pattern.compile(
"[íìîï]", Pattern.CASE_INSENSITIVE);
PATTERNS[3] = Pattern.compile(
"[óòôõö]", Pattern.CASE_INSENSITIVE);
PATTERNS[4] = Pattern.compile(
"[úùûü]", Pattern.CASE_INSENSITIVE);
PATTERNS[5] = Pattern.compile(
"[ç]", Pattern.CASE_INSENSITIVE);
}

public static String removeAcentos(String text) {
if (PATTERNS == null) {
compilePatterns();
}

String result = text;
for (int i = 0; i < PATTERNS.length; i++) {
Matcher matcher = PATTERNS[i].matcher(result);
result = matcher.replaceAll(REPLACES[i]);
}
return result.toUpperCase();
}

O código acima, utiliza as classes Pattern e Matcher para checar a presença de caracteres acentuados e substitui-los pelos caracteres correspondentes sem acentuação. Observe que só são sendo tratadas as vogais e o cedilha, para textos em outros idiomas você deve fazer os ajustes necessários (como o 'ñ' do espanhol).

Para que um AutoCompleteTextView faça a busca, é necessário definir um Adapter com os dados que ele irá filtrar para exibir. Para fazer nossa busca personalizada (ignorando acentuação) devemos criar nosso Adapter e sobrescrever o método getFilter(). Esse método deve retornar um objeto da classe android.widget.Filter que representa o resultado da busca.

public class MeuAutoCompleteAdapter
extends ArrayAdapter<String>
implements Filterable {

private List<String> listaCompleta;
private List<String> resultados;
private Filter meuFiltro;

public MeuAutoCompleteAdapter(
Context ctx, int layout,
List<String> textos) {

super(ctx, layout, textos);
this.listaCompleta = textos;
this.resultados = listaCompleta;
this.meuFiltro = new MeuFiltro();
}

@Override
public int getCount() {
return resultados.size();
}

@Override
public String getItem(int position) {
if (resultados != null
&& resultados.size() > 0
&& position < resultados.size()){
return resultados.get(position);
} else {
return null;
}
}

@Override
public Filter getFilter() {
return meuFiltro;
}

private class MeuFiltro extends Filter {
@Override
protected FilterResults performFiltering(
CharSequence constraint) {

FilterResults filterResults =
new FilterResults();

ArrayList<String> temp =
new ArrayList<String>();

if (constraint != null) {
String term = removeAcentos(
constraint.toString().trim().toLowerCase());

String placeStr;
for (String p : listaCompleta) {
placeStr = removeAcentos(p.toLowerCase());

if ( placeStr.indexOf(term) > -1){
temp.add(p);
}
}
}
filterResults.values = temp;
filterResults.count = temp.size();
return filterResults;
}

@SuppressWarnings("unchecked")
@Override
protected void publishResults(
CharSequence contraint,
FilterResults filterResults) {

resultados = (ArrayList<String>)
filterResults.values;

notifyDataSetChanged();
}
}
}

A classe acima tem duas listas de strings: a original e a que serve como resultado da busca. Note que os métodos getCount e getItem trabalha em cima da lista dos resultados. O outro atributo da classe é da classe MeuFiltro, e esse atributo é retornado no método getFilter.
A classe interna MeuFiltro faz a nossa busca personalizada. O filtro é realizado no método performFiltering, que retorna um objeto da classe android.widget.Filter.FilterResults que contém o resultado da busca e o total de registros encontrados. Esse resultado é passado para o método publishResults que seta o resultado da busca no atributo resultados, e logo após atualiza o adapter através do método notifyDatasetChanged.

Agora vamos ver como usar esse nosso adapter.


List<String> cidade = new ArrayList<String>();
cidade.add("Recife");
cidade.add("São Paulo");
cidade.add("Santos");
cidade.add("Santa Cruz");

MeuAutoCompleteAdapter adapter =
new MeuAutoCompleteAdapter(
contexto,
android.R.layout.simple_dropdown_item_1line,
cidade);

AutoCompleteTextView actv = (AutoCompleteTextView)
findViewById(R.id.autoCompleteTextView1);
actv.setAdapter(adapter);


O resultado é exibido na figura abaixo:


Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

14 comentários:

Felipe Teste disse...

Muito bom a dica... será bem útil, assim como muitos outros posts do blog...

Unknown disse...

Muito bom! Parabéns

@tacrocha disse...

Valeu, Glauber! Seguindo sua dica, fiz uma lista com autocomplete personaliada que apresenta os amigos do facebook com nome e foto. \o/

Unknown disse...

Excelente!
Se eu setar o meu AutoComplete com valores vindos do banco SQLite, por exemplo nome da pessoa, como que eu pegaria o id da pessoa selecionada no AutoComplete? Já faz alguns dias que to me debatendo com isto

Abraços.

Nelson Glauber disse...

Oi Tiago,

Só é implementar o método getItemId no seu adapter personalizado. A partir do AutoCompleteTextView, você pega a instância do Adapter e chama o método.

4br4ç05,
nglauber

Unknown disse...

Olá Glauber,

Fiz o seu exemplo e funcionou belezinha... Parabéns.

Só ficou uma dúvida que pelo que já vi nos fóruns muita gente também tem essa mesma dúvida;

Seguinte: Quando eu digito algum texto no Edit, ele apresenta as sugestões que contenham aquela expressão. Eu precisava que fosse apresentado as sugestões que comecem com a expressão digitada pelo usuário. Sabe se tem como fazer isso?

Forte Abraço.

Nelson Glauber disse...

Olá Unknown,

O método performFiltering da classe MeuFiltro é que faz a filtragem dos dados. Nele você pode fazer a consulta do jeito que você quiser.

[]´s
Glauber

Unknown disse...

Parabéns pelo tutorial, Glauber!

Tenho uma dúvida: isso só serve para um adaptador ArrayAdapter? O meu adaptador é um BaseAdapter de uma classe minha... e gostaria que o acento fosse ignorado, como no seu caso.

Eu não entendi o que eu tenho que fazer nesse momento, para comparar a string com a minha classe:

String placeStr;
for (String p : lista_ingredientes) {
placeStr = removeAcentos(p.toLowerCase());

if ( placeStr.indexOf(term) > -1){
temp.add(p);
}
}

Poderia me ajudar? Obrigada desde já! Abraços

Márcio disse...

Muito obrigado cara! me ajudou muito!

Só tenho uma dúvida.

Como que faço para chamar um método passando por parâmetro o nome da cidade escolhida?

Por exemplo:
Se a pessoa escolheu Recife eu mandar fazer um select no banco onde cidade = "Recife".

Só preciso saber em qual parte que eu chamo o Método
PegarDados(Cidade)
Já tenho ele pronto aqui

private void PegarDados(cidade){
Select *from clientes where cidade = cidade
}
onde Cidade é o nome da cidade em que o cara escolheu..

Anônimo disse...

Grande Tutorial!!

Se eu quizer adicionar também as Siglas das Cidades, tem como?
O Cara pesquisar pela Sigla ou pela descrição da cidade?

Nelson Glauber disse...

Oi Anônimo,

A lógica da busca é feita no método performFiltering, então lá você pode personalizar a forma que os dados são buscados.

4br4ç05,
nglauber

Nelson Glauber disse...

Oi Marcio,

Desculpa a demora em responder, mas basicamente é usar o OnItemClickListener.

seuAutoComplete.setOnItemClickListener(new OnItemClickListener() {

@Override
public void onItemClick(AdapterView parent, View arg1, int pos, long id) {
// Faça a busca
}
});

4br4ç05,
nglauber

Unknown disse...

Olá Glauber,

Parabéns pelo grande tutorial, me ajudou bastante, mas surgiu uma dúvida enquanto estava aplicando e queria pode fazer outro tipo de filtro com que no meu AutoComplete aparecesse somente palavras que possuam os caracteres digitados na frente e não em todo o corpo da palavra, por exemplo:

Quando eu digito as letras "ma", no AutoComplete apareceria "aMAzonas".

Se puder me dar uma força eu agradeço.

Abraços.

Nelson Glauber disse...

Oi André,

Você pode personalizar isso da maneira que desejar. É só implementar sua lógica no performFiltering().

4br4ç05,
nglauber