Introdução
Como não está realmente claro na sua pergunta com o que exatamente você está tendo problemas, escrevi esta rápida explicação sobre como implementar esse recurso; Se você ainda tiver dúvidas, não hesite em perguntar.
Eu tenho um exemplo prático de tudo o que estou falando aqui neste Repositório do GitHub .
Se você quiser saber mais sobre o projeto de exemplo, visite a página inicial do projeto .
De qualquer forma, o resultado deve ser algo como isto:
Se você deseja primeiro brincar com o aplicativo demo, pode instalá-lo na Play Store:
De qualquer forma, vamos começar.
Configurando o SearchView
Na pasta, res/menu
crie um novo arquivo chamado main_menu.xml
. Nele, adicione um item e defina actionViewClass
como android.support.v7.widget.SearchView
. Como você está usando a biblioteca de suporte, é necessário usar o espaço para nome da biblioteca de suporte para definir o actionViewClass
atributo. Seu arquivo xml deve ser algo como isto:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_search"
android:title="@string/action_search"
app:actionViewClass="android.support.v7.widget.SearchView"
app:showAsAction="always"/>
</menu>
No seu Fragment
ou Activity
você precisa inflar esse menu em xml como de costume, pode procurar o MenuItem
que contém o SearchView
e implementar o OnQueryTextListener
que vamos usar para ouvir as alterações no texto digitado no SearchView
:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
final MenuItem searchItem = menu.findItem(R.id.action_search);
final SearchView searchView = (SearchView) searchItem.getActionView();
searchView.setOnQueryTextListener(this);
return true;
}
@Override
public boolean onQueryTextChange(String query) {
// Here is where we are going to implement the filter logic
return false;
}
@Override
public boolean onQueryTextSubmit(String query) {
return false;
}
E agora o SearchView
está pronto para ser usado. Implementaremos a lógica do filtro mais tarde, onQueryTextChange()
assim que terminarmos de implementar o Adapter
.
Configurando o Adapter
Em primeiro lugar, esta é a classe de modelo que vou usar neste exemplo:
public class ExampleModel {
private final long mId;
private final String mText;
public ExampleModel(long id, String text) {
mId = id;
mText = text;
}
public long getId() {
return mId;
}
public String getText() {
return mText;
}
}
É apenas o seu modelo básico que exibirá um texto no RecyclerView
. Este é o layout que vou usar para exibir o texto:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="model"
type="com.github.wrdlbrnft.searchablerecyclerviewdemo.ui.models.ExampleModel"/>
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="@{model.text}"/>
</FrameLayout>
</layout>
Como você pode ver, eu uso Ligação de Dados. Se você nunca trabalhou com ligação de dados antes, não desanime! É muito simples e poderoso, no entanto, não posso explicar como isso funciona no escopo desta resposta.
Este é o ViewHolder
da ExampleModel
classe:
public class ExampleViewHolder extends RecyclerView.ViewHolder {
private final ItemExampleBinding mBinding;
public ExampleViewHolder(ItemExampleBinding binding) {
super(binding.getRoot());
mBinding = binding;
}
public void bind(ExampleModel item) {
mBinding.setModel(item);
}
}
Mais uma vez nada de especial. Ele apenas usa ligação de dados para vincular a classe de modelo a esse layout, conforme definimos no xml de layout acima.
Agora podemos finalmente chegar à parte realmente interessante: escrever o adaptador. Vou pular a implementação básica do Adapter
e, em vez disso, vou me concentrar nas partes que são relevantes para esta resposta.
Mas primeiro há uma coisa sobre a qual devemos falar: a SortedList
classe.
SortedList
O SortedList
é uma ferramenta completamente incrível que faz parte da RecyclerView
biblioteca. Ele cuida de notificar as Adapter
alterações sobre o conjunto de dados e faz isso de uma maneira muito eficiente. A única coisa que você precisa fazer é especificar uma ordem dos elementos. Você precisa fazer isso implementando um compare()
método que compara dois elementos da SortedList
mesma forma que a Comparator
. Mas, em vez de classificar um List
, é usado para classificar os itens no RecyclerView
!
O SortedList
interage com a classe Adapter
through Callback
que você precisa implementar:
private final SortedList.Callback<ExampleModel> mCallback = new SortedList.Callback<ExampleModel>() {
@Override
public void onInserted(int position, int count) {
mAdapter.notifyItemRangeInserted(position, count);
}
@Override
public void onRemoved(int position, int count) {
mAdapter.notifyItemRangeRemoved(position, count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
mAdapter.notifyItemMoved(fromPosition, toPosition);
}
@Override
public void onChanged(int position, int count) {
mAdapter.notifyItemRangeChanged(position, count);
}
@Override
public int compare(ExampleModel a, ExampleModel b) {
return mComparator.compare(a, b);
}
@Override
public boolean areContentsTheSame(ExampleModel oldItem, ExampleModel newItem) {
return oldItem.equals(newItem);
}
@Override
public boolean areItemsTheSame(ExampleModel item1, ExampleModel item2) {
return item1.getId() == item2.getId();
}
}
Nos métodos na parte superior do retorno de chamada onMoved
, como onInserted
, etc., você deve chamar o método de notificação equivalente ao seu Adapter
. Os três métodos na parte inferior compare
, areContentsTheSame
e areItemsTheSame
você tem que implementar de acordo com o tipo de objetos que você deseja exibir e em que ordem esses objetos devem aparecer na tela.
Vamos passar por esses métodos, um por um:
@Override
public int compare(ExampleModel a, ExampleModel b) {
return mComparator.compare(a, b);
}
Este é o compare()
método sobre o qual falei anteriormente. Neste exemplo, estou apenas passando a chamada para a Comparator
que compara os dois modelos. Se você deseja que os itens apareçam em ordem alfabética na tela. Esse comparador pode ficar assim:
private static final Comparator<ExampleModel> ALPHABETICAL_COMPARATOR = new Comparator<ExampleModel>() {
@Override
public int compare(ExampleModel a, ExampleModel b) {
return a.getText().compareTo(b.getText());
}
};
Agora vamos dar uma olhada no próximo método:
@Override
public boolean areContentsTheSame(ExampleModel oldItem, ExampleModel newItem) {
return oldItem.equals(newItem);
}
O objetivo deste método é determinar se o conteúdo de um modelo foi alterado. Ele SortedList
usa isso para determinar se um evento de mudança precisa ser chamado - em outras palavras, se o RecyclerView
crossfade deve ser feito entre a versão antiga e a nova. Se você modelar classes com uma correta equals()
e hashCode()
implementação, geralmente poderá implementá-la como acima. Se somarmos um equals()
e hashCode()
implementação da ExampleModel
classe deve ser algo como isto:
public class ExampleModel implements SortedListAdapter.ViewModel {
private final long mId;
private final String mText;
public ExampleModel(long id, String text) {
mId = id;
mText = text;
}
public long getId() {
return mId;
}
public String getText() {
return mText;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ExampleModel model = (ExampleModel) o;
if (mId != model.mId) return false;
return mText != null ? mText.equals(model.mText) : model.mText == null;
}
@Override
public int hashCode() {
int result = (int) (mId ^ (mId >>> 32));
result = 31 * result + (mText != null ? mText.hashCode() : 0);
return result;
}
}
Nota lateral rápida: a maioria dos IDE, como Android Studio, IntelliJ e Eclipse, tem funcionalidade para gerar equals()
e hashCode()
implementações para você com o pressionar de um botão! Então você não precisa implementá-los você mesmo. Procure na internet como funciona no seu IDE!
Agora vamos dar uma olhada no último método:
@Override
public boolean areItemsTheSame(ExampleModel item1, ExampleModel item2) {
return item1.getId() == item2.getId();
}
O SortedList
usa esse método para verificar se dois itens se referem à mesma coisa. Em termos mais simples (sem explicar como SortedList
funciona), isso é usado para determinar se um objeto já está contido na List
animação e se é necessário reproduzir uma animação de adicionar, mover ou alterar. Se seus modelos tiverem um ID, você normalmente compararia apenas o ID neste método. Caso contrário, você precisa descobrir outra maneira de verificar isso, mas, no entanto, você acaba implementando isso depende do seu aplicativo específico. Geralmente, é a opção mais simples de fornecer um ID a todos os modelos - que poderia, por exemplo, ser o campo da chave primária se você estiver consultando os dados de um banco de dados.
Com o SortedList.Callback
implementado corretamente, podemos criar uma instância do SortedList
:
final SortedList<ExampleModel> list = new SortedList<>(ExampleModel.class, mCallback);
Como o primeiro parâmetro no construtor do, SortedList
você precisa passar a classe de seus modelos. O outro parâmetro é exatamente o SortedList.Callback
que definimos acima.
Agora, vamos ao que interessa: se implementarmos o Adapter
com um SortedList
, deve ser algo como isto:
public class ExampleAdapter extends RecyclerView.Adapter<ExampleViewHolder> {
private final SortedList<ExampleModel> mSortedList = new SortedList<>(ExampleModel.class, new SortedList.Callback<ExampleModel>() {
@Override
public int compare(ExampleModel a, ExampleModel b) {
return mComparator.compare(a, b);
}
@Override
public void onInserted(int position, int count) {
notifyItemRangeInserted(position, count);
}
@Override
public void onRemoved(int position, int count) {
notifyItemRangeRemoved(position, count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
notifyItemMoved(fromPosition, toPosition);
}
@Override
public void onChanged(int position, int count) {
notifyItemRangeChanged(position, count);
}
@Override
public boolean areContentsTheSame(ExampleModel oldItem, ExampleModel newItem) {
return oldItem.equals(newItem);
}
@Override
public boolean areItemsTheSame(ExampleModel item1, ExampleModel item2) {
return item1.getId() == item2.getId();
}
});
private final LayoutInflater mInflater;
private final Comparator<ExampleModel> mComparator;
public ExampleAdapter(Context context, Comparator<ExampleModel> comparator) {
mInflater = LayoutInflater.from(context);
mComparator = comparator;
}
@Override
public ExampleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final ItemExampleBinding binding = ItemExampleBinding.inflate(inflater, parent, false);
return new ExampleViewHolder(binding);
}
@Override
public void onBindViewHolder(ExampleViewHolder holder, int position) {
final ExampleModel model = mSortedList.get(position);
holder.bind(model);
}
@Override
public int getItemCount() {
return mSortedList.size();
}
}
O Comparator
usado para classificar o item é passado pelo construtor, para que possamos usar o mesmo, Adapter
mesmo que os itens devam ser exibidos em uma ordem diferente.
Agora estamos quase terminando! Mas primeiro precisamos de uma maneira de adicionar ou remover itens ao arquivo Adapter
. Para esse fim, podemos adicionar métodos aos Adapter
quais nos permitem adicionar e remover itens ao SortedList
:
public void add(ExampleModel model) {
mSortedList.add(model);
}
public void remove(ExampleModel model) {
mSortedList.remove(model);
}
public void add(List<ExampleModel> models) {
mSortedList.addAll(models);
}
public void remove(List<ExampleModel> models) {
mSortedList.beginBatchedUpdates();
for (ExampleModel model : models) {
mSortedList.remove(model);
}
mSortedList.endBatchedUpdates();
}
Não precisamos chamar nenhum método de notificação aqui, porque o SortedList
já faz isso por meio do SortedList.Callback
! Além disso, a implementação desses métodos é bastante direta, com uma exceção: o método remove, que remove um List
dos modelos. Como o SortedList
método remove apenas um que pode remover um único objeto, precisamos percorrer a lista e remover os modelos um a um. Chamar beginBatchedUpdates()
no início lotes de todas as alterações que faremos no SortedList
conjunto e melhorar o desempenho. Quando chamamos endBatchedUpdates()
o RecyclerView
é notificado sobre todas as alterações de uma só vez.
Além disso, o que você precisa entender é que se você adicionar um objeto ao SortedList
e ele já estiver SortedList
nele, ele não será adicionado novamente. Em vez disso, SortedList
usa o areContentsTheSame()
método para descobrir se o objeto foi alterado - e se ele possui o item no RecyclerView
será atualizado.
De qualquer forma, o que eu normalmente prefiro é um método que me permite substituir todos os itens de RecyclerView
uma só vez. Remova tudo o que não está no List
e adicione todos os itens que estão faltando no SortedList
:
public void replaceAll(List<ExampleModel> models) {
mSortedList.beginBatchedUpdates();
for (int i = mSortedList.size() - 1; i >= 0; i--) {
final ExampleModel model = mSortedList.get(i);
if (!models.contains(model)) {
mSortedList.remove(model);
}
}
mSortedList.addAll(models);
mSortedList.endBatchedUpdates();
}
Esse método novamente agrupa todas as atualizações para aumentar o desempenho. O primeiro loop é inverso, pois a remoção de um item no início atrapalhava os índices de todos os itens que surgiram depois e isso pode levar, em alguns casos, a problemas como inconsistências de dados. Depois disso, basta adicionar o List
ao SortedList
uso addAll()
de adicionar todos os itens que já não estão no SortedList
e - assim como eu descrevi acima - atualização todos os itens que já estão no SortedList
mas foram alterados.
E com isso o Adapter
está completo. A coisa toda deve se parecer com isso:
public class ExampleAdapter extends RecyclerView.Adapter<ExampleViewHolder> {
private final SortedList<ExampleModel> mSortedList = new SortedList<>(ExampleModel.class, new SortedList.Callback<ExampleModel>() {
@Override
public int compare(ExampleModel a, ExampleModel b) {
return mComparator.compare(a, b);
}
@Override
public void onInserted(int position, int count) {
notifyItemRangeInserted(position, count);
}
@Override
public void onRemoved(int position, int count) {
notifyItemRangeRemoved(position, count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
notifyItemMoved(fromPosition, toPosition);
}
@Override
public void onChanged(int position, int count) {
notifyItemRangeChanged(position, count);
}
@Override
public boolean areContentsTheSame(ExampleModel oldItem, ExampleModel newItem) {
return oldItem.equals(newItem);
}
@Override
public boolean areItemsTheSame(ExampleModel item1, ExampleModel item2) {
return item1 == item2;
}
});
private final Comparator<ExampleModel> mComparator;
private final LayoutInflater mInflater;
public ExampleAdapter(Context context, Comparator<ExampleModel> comparator) {
mInflater = LayoutInflater.from(context);
mComparator = comparator;
}
@Override
public ExampleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final ItemExampleBinding binding = ItemExampleBinding.inflate(mInflater, parent, false);
return new ExampleViewHolder(binding);
}
@Override
public void onBindViewHolder(ExampleViewHolder holder, int position) {
final ExampleModel model = mSortedList.get(position);
holder.bind(model);
}
public void add(ExampleModel model) {
mSortedList.add(model);
}
public void remove(ExampleModel model) {
mSortedList.remove(model);
}
public void add(List<ExampleModel> models) {
mSortedList.addAll(models);
}
public void remove(List<ExampleModel> models) {
mSortedList.beginBatchedUpdates();
for (ExampleModel model : models) {
mSortedList.remove(model);
}
mSortedList.endBatchedUpdates();
}
public void replaceAll(List<ExampleModel> models) {
mSortedList.beginBatchedUpdates();
for (int i = mSortedList.size() - 1; i >= 0; i--) {
final ExampleModel model = mSortedList.get(i);
if (!models.contains(model)) {
mSortedList.remove(model);
}
}
mSortedList.addAll(models);
mSortedList.endBatchedUpdates();
}
@Override
public int getItemCount() {
return mSortedList.size();
}
}
A única coisa que falta agora é implementar a filtragem!
Implementando a lógica do filtro
Para implementar a lógica do filtro, primeiro precisamos definir um List
de todos os modelos possíveis. Para este exemplo, crio uma List
de ExampleModel
instâncias a partir de uma matriz de filmes:
private static final String[] MOVIES = new String[]{
...
};
private static final Comparator<ExampleModel> ALPHABETICAL_COMPARATOR = new Comparator<ExampleModel>() {
@Override
public int compare(ExampleModel a, ExampleModel b) {
return a.getText().compareTo(b.getText());
}
};
private ExampleAdapter mAdapter;
private List<ExampleModel> mModels;
private RecyclerView mRecyclerView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
mAdapter = new ExampleAdapter(this, ALPHABETICAL_COMPARATOR);
mBinding.recyclerView.setLayoutManager(new LinearLayoutManager(this));
mBinding.recyclerView.setAdapter(mAdapter);
mModels = new ArrayList<>();
for (String movie : MOVIES) {
mModels.add(new ExampleModel(movie));
}
mAdapter.add(mModels);
}
Nada de especial está acontecendo aqui, apenas instanciamos Adapter
e definimos como RecyclerView
. Depois disso, criamos um List
dos modelos a partir dos nomes dos filmes na MOVIES
matriz. Em seguida, adicionamos todos os modelos ao arquivo SortedList
.
Agora podemos voltar ao onQueryTextChange()
que definimos anteriormente e começar a implementar a lógica do filtro:
@Override
public boolean onQueryTextChange(String query) {
final List<ExampleModel> filteredModelList = filter(mModels, query);
mAdapter.replaceAll(filteredModelList);
mBinding.recyclerView.scrollToPosition(0);
return true;
}
Isso é novamente bastante direto. Chamamos o método filter()
e passamos o List
de ExampleModel
s e a string de consulta. Chamamos então replaceAll()
no Adapter
e passar o filtrado List
retornado por filter()
. Também precisamos chamar scrollToPosition(0)
o RecyclerView
para garantir que o usuário sempre possa ver todos os itens ao procurar algo. Caso contrário, ele RecyclerView
poderá permanecer em uma posição rolada para baixo durante a filtragem e subsequentemente ocultar alguns itens. Rolar para o topo garante uma melhor experiência do usuário durante a pesquisa.
A única coisa que resta a fazer agora é se implementar filter()
:
private static List<ExampleModel> filter(List<ExampleModel> models, String query) {
final String lowerCaseQuery = query.toLowerCase();
final List<ExampleModel> filteredModelList = new ArrayList<>();
for (ExampleModel model : models) {
final String text = model.getText().toLowerCase();
if (text.contains(lowerCaseQuery)) {
filteredModelList.add(model);
}
}
return filteredModelList;
}
A primeira coisa que fazemos aqui é chamar toLowerCase()
a string de consulta. Não queremos que nossa função de toLowerCase()
pesquisa faça distinção entre maiúsculas e minúsculas e, chamando todas as sequências comparadas, podemos garantir que retornemos os mesmos resultados, independentemente do caso. Ele então itera através de todos os modelos no List
que passamos para ele e verifica se a string de consulta está contida no texto do modelo. Se for, o modelo é adicionado ao filtrado List
.
E é isso! O código acima será executado no nível 7 da API e acima. A partir do nível 11 da API, você obtém animações de itens de graça!
Percebo que esta é uma descrição muito detalhada que provavelmente faz com que tudo pareça mais complicado do que realmente é, mas existe uma maneira de generalizar todo esse problema e tornar a implementação Adapter
baseada em uma SortedList
muito mais simples.
Generalizando o Problema e Simplificando o Adaptador
Nesta seção, não entrarei em muitos detalhes - em parte porque estou correndo contra o limite de caracteres para obter respostas no Stack Overflow, mas também porque a maioria já foi explicada acima -, mas para resumir as mudanças: Podemos implementar uma Adapter
classe base que já cuida de lidar com os SortedList
modelos, bem como vincula as ViewHolder
instâncias e fornece uma maneira conveniente de implementar uma Adapter
baseada em a SortedList
. Para isso, temos que fazer duas coisas:
- Precisamos criar uma
ViewModel
interface que todas as classes de modelo precisam implementar
- Precisamos criar uma
ViewHolder
subclasse que defina um bind()
método que Adapter
pode ser usado para vincular modelos automaticamente.
Isso nos permite focar apenas no conteúdo que deve ser exibido no RecyclerView
apenas implementando os modelos e as ViewHolder
implementações correspondentes . Usando esta classe base, não precisamos nos preocupar com os detalhes intrincados do Adapter
e dele SortedList
.
SortedListAdapter
Devido ao limite de caracteres para respostas no StackOverflow, não posso executar cada etapa da implementação dessa classe base ou até mesmo adicionar o código fonte completo aqui, mas você pode encontrar o código fonte completo dessa classe base - como eu chamei SortedListAdapter
- neste GitHub Gist .
Para simplificar sua vida, publiquei uma biblioteca no jCenter que contém o SortedListAdapter
! Se você deseja usá-lo, basta adicionar essa dependência ao arquivo build.gradle do seu aplicativo:
compile 'com.github.wrdlbrnft:sorted-list-adapter:0.2.0.1'
Você pode encontrar mais informações sobre esta biblioteca na página inicial da biblioteca .
Usando o SortedListAdapter
Para usar o SortedListAdapter
, temos que fazer duas alterações:
Altere o ViewHolder
para que ele se estenda SortedListAdapter.ViewHolder
. O parâmetro type deve ser o modelo que deve ser associado a isso ViewHolder
- neste caso ExampleModel
. Você precisa vincular dados aos seus modelos em performBind()
vez de bind()
.
public class ExampleViewHolder extends SortedListAdapter.ViewHolder<ExampleModel> {
private final ItemExampleBinding mBinding;
public ExampleViewHolder(ItemExampleBinding binding) {
super(binding.getRoot());
mBinding = binding;
}
@Override
protected void performBind(ExampleModel item) {
mBinding.setModel(item);
}
}
Verifique se todos os seus modelos implementam a ViewModel
interface:
public class ExampleModel implements SortedListAdapter.ViewModel {
...
}
Depois disso, basta atualizar o ExampleAdapter
arquivo para estender SortedListAdapter
e remover tudo o que não precisamos mais. O parâmetro type deve ser o tipo de modelo com o qual você está trabalhando - neste caso ExampleModel
. Mas se você estiver trabalhando com diferentes tipos de modelos, em seguida, definir o tipo de parâmetro para ViewModel
.
public class ExampleAdapter extends SortedListAdapter<ExampleModel> {
public ExampleAdapter(Context context, Comparator<ExampleModel> comparator) {
super(context, ExampleModel.class, comparator);
}
@Override
protected ViewHolder<? extends ExampleModel> onCreateViewHolder(LayoutInflater inflater, ViewGroup parent, int viewType) {
final ItemExampleBinding binding = ItemExampleBinding.inflate(inflater, parent, false);
return new ExampleViewHolder(binding);
}
@Override
protected boolean areItemsTheSame(ExampleModel item1, ExampleModel item2) {
return item1.getId() == item2.getId();
}
@Override
protected boolean areItemContentsTheSame(ExampleModel oldItem, ExampleModel newItem) {
return oldItem.equals(newItem);
}
}
Depois que terminamos! No entanto, uma última coisa a mencionar: o SortedListAdapter
não possui o mesmo add()
, remove()
ou os replaceAll()
métodos que nosso original ExampleAdapter
possuía. Ele usa um Editor
objeto separado para modificar os itens da lista que podem ser acessados através do edit()
método Portanto, se você deseja remover ou adicionar itens aos quais você precisa ligar edit()
, adicione e remova os itens nessa Editor
instância e, quando terminar, chame commit()
-o para aplicar as alterações no SortedList
:
mAdapter.edit()
.remove(modelToRemove)
.add(listOfModelsToAdd)
.commit();
Todas as alterações feitas dessa maneira são agrupadas em lote para aumentar o desempenho. O replaceAll()
método que implementamos nos capítulos acima também está presente neste Editor
objeto:
mAdapter.edit()
.replaceAll(mModels)
.commit();
Se você esquecer de ligar commit()
, nenhuma das suas alterações será aplicada!