Como filtrar um RecyclerView com um SearchView

319

Estou tentando implementar o SearchViewda biblioteca de suporte. Eu quero que o usuário use o SearchViewfiltro de um Listfilme em um RecyclerView.

Eu segui alguns tutoriais até agora e os adicionei SearchViewao ActionBar, mas não tenho muita certeza de onde ir a partir daqui. Eu já vi alguns exemplos, mas nenhum deles mostra resultados quando você começa a digitar.

Este é o meu MainActivity:

public class MainActivity extends ActionBarActivity {

    RecyclerView mRecyclerView;
    RecyclerView.LayoutManager mLayoutManager;
    RecyclerView.Adapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_recycler_view);

        mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        mRecyclerView.setHasFixedSize(true);

        mLayoutManager = new LinearLayoutManager(this);
        mRecyclerView.setLayoutManager(mLayoutManager);

        mAdapter = new CardAdapter() {
            @Override
            public Filter getFilter() {
                return null;
            }
        };
        mRecyclerView.setAdapter(mAdapter);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
        SearchView searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
        searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }
}

E este é o meu Adapter:

public abstract class CardAdapter extends RecyclerView.Adapter<CardAdapter.ViewHolder> implements Filterable {

    List<Movie> mItems;

    public CardAdapter() {
        super();
        mItems = new ArrayList<Movie>();
        Movie movie = new Movie();
        movie.setName("Spiderman");
        movie.setRating("92");
        mItems.add(movie);

        movie = new Movie();
        movie.setName("Doom 3");
        movie.setRating("91");
        mItems.add(movie);

        movie = new Movie();
        movie.setName("Transformers");
        movie.setRating("88");
        mItems.add(movie);

        movie = new Movie();
        movie.setName("Transformers 2");
        movie.setRating("87");
        mItems.add(movie);

        movie = new Movie();
        movie.setName("Transformers 3");
        movie.setRating("86");
        mItems.add(movie);

        movie = new Movie();
        movie.setName("Noah");
        movie.setRating("86");
        mItems.add(movie);

        movie = new Movie();
        movie.setName("Ironman");
        movie.setRating("86");
        mItems.add(movie);

        movie = new Movie();
        movie.setName("Ironman 2");
        movie.setRating("86");
        mItems.add(movie);

        movie = new Movie();
        movie.setName("Ironman 3");
        movie.setRating("86");
        mItems.add(movie);
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
        View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.recycler_view_card_item, viewGroup, false);
        return new ViewHolder(v);
    }

    @Override
    public void onBindViewHolder(ViewHolder viewHolder, int i) {
        Movie movie = mItems.get(i);
        viewHolder.tvMovie.setText(movie.getName());
        viewHolder.tvMovieRating.setText(movie.getRating());
    }

    @Override
    public int getItemCount() {
        return mItems.size();
    }

    class ViewHolder extends RecyclerView.ViewHolder{

        public TextView tvMovie;
        public TextView tvMovieRating;

        public ViewHolder(View itemView) {
            super(itemView);
            tvMovie = (TextView)itemView.findViewById(R.id.movieName);
            tvMovieRating = (TextView)itemView.findViewById(R.id.movieRating);
        }
    }
}
Jacques Krause
fonte

Respostas:

913

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:

imagem demo

Se você deseja primeiro brincar com o aplicativo demo, pode instalá-lo na Play Store:

Obtê-lo no Google Play

De qualquer forma, vamos começar.


Configurando o SearchView

Na pasta, res/menucrie um novo arquivo chamado main_menu.xml. Nele, adicione um item e defina actionViewClasscomo 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 actionViewClassatributo. 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 Fragmentou Activityvocê precisa inflar esse menu em xml como de costume, pode procurar o MenuItemque contém o SearchViewe implementar o OnQueryTextListenerque 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 SearchViewestá 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 ViewHolderda ExampleModelclasse:

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 Adaptere, 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 SortedListclasse.


SortedList

O SortedListé uma ferramenta completamente incrível que faz parte da RecyclerViewbiblioteca. Ele cuida de notificar as Adapteralteraçõ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 SortedListmesma forma que a Comparator. Mas, em vez de classificar um List, é usado para classificar os itens no RecyclerView!

O SortedListinterage com a classe Adapterthrough Callbackque 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, areContentsTheSamee areItemsTheSamevocê 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 Comparatorque 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 SortedListusa isso para determinar se um evento de mudança precisa ser chamado - em outras palavras, se o RecyclerViewcrossfade 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 ExampleModelclasse 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 SortedListusa esse método para verificar se dois itens se referem à mesma coisa. Em termos mais simples (sem explicar como SortedListfunciona), isso é usado para determinar se um objeto já está contido na Listanimaçã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.Callbackimplementado 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, SortedListvocê precisa passar a classe de seus modelos. O outro parâmetro é exatamente o SortedList.Callbackque definimos acima.

Agora, vamos ao que interessa: se implementarmos o Adaptercom 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 Comparatorusado para classificar o item é passado pelo construtor, para que possamos usar o mesmo, Adaptermesmo 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 Adapterquais 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 SortedListjá 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 Listdos modelos. Como o SortedListmé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 SortedListconjunto 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 SortedListe ele já estiver SortedListnele, ele não será adicionado novamente. Em vez disso, SortedListusa o areContentsTheSame()método para descobrir se o objeto foi alterado - e se ele possui o item no RecyclerViewserá atualizado.

De qualquer forma, o que eu normalmente prefiro é um método que me permite substituir todos os itens de RecyclerViewuma só vez. Remova tudo o que não está no Liste 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 Listao SortedListuso addAll()de adicionar todos os itens que já não estão no SortedListe - assim como eu descrevi acima - atualização todos os itens que já estão no SortedListmas foram alterados.

E com isso o Adapterestá 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 Listde todos os modelos possíveis. Para este exemplo, crio uma Listde ExampleModelinstâ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 Adaptere definimos como RecyclerView. Depois disso, criamos um Listdos modelos a partir dos nomes dos filmes na MOVIESmatriz. 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 Listde ExampleModels e a string de consulta. Chamamos então replaceAll()no Adaptere passar o filtrado Listretornado por filter(). Também precisamos chamar scrollToPosition(0)o RecyclerViewpara garantir que o usuário sempre possa ver todos os itens ao procurar algo. Caso contrário, ele RecyclerViewpoderá 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 Listque 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 Adapterbaseada em uma SortedListmuito 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 Adapterclasse base que já cuida de lidar com os SortedListmodelos, bem como vincula as ViewHolderinstâncias e fornece uma maneira conveniente de implementar uma Adapterbaseada em a SortedList. Para isso, temos que fazer duas coisas:

  • Precisamos criar uma ViewModelinterface que todas as classes de modelo precisam implementar
  • Precisamos criar uma ViewHoldersubclasse que defina um bind()método que Adapterpode ser usado para vincular modelos automaticamente.

Isso nos permite focar apenas no conteúdo que deve ser exibido no RecyclerViewapenas implementando os modelos e as ViewHolderimplementações correspondentes . Usando esta classe base, não precisamos nos preocupar com os detalhes intrincados do Adaptere 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 ViewHolderpara 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 ViewModelinterface:

    public class ExampleModel implements SortedListAdapter.ViewModel {
        ...
    }

Depois disso, basta atualizar o ExampleAdapterarquivo para estender SortedListAdaptere 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 SortedListAdapternão possui o mesmo add(), remove()ou os replaceAll()métodos que nosso original ExampleAdapterpossuía. Ele usa um Editorobjeto 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 Editorinstâ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 Editorobjeto:

mAdapter.edit()
        .replaceAll(mModels)
        .commit();

Se você esquecer de ligar commit(), nenhuma das suas alterações será aplicada!

Xaver Kapeller
fonte
4
@TiagoOliveira Bem, isso foi feito para funcionar imediatamente: D A ligação de dados é um obstáculo para as pessoas que não estão familiarizadas com ela, mas eu a incluí de qualquer maneira, porque é incrível e quero promovê-la. Por alguma razão, muitas pessoas não parecem saber sobre isso ...
Xaver Kapeller
78
Ainda não li toda a resposta, tive que fazer uma pausa na leitura em algum lugar pela metade para escrever esse comentário - esta é uma das melhores respostas que encontrei aqui no SO! Obrigado!
Daneejela 21/11/2016
16
Eu adoro como você é como: "Não está claro da sua pergunta o que você está tendo problemas com, então aqui está um exemplo de pleno direito Eu apenas fiz": D
Fred
7
+1 apenas para nos mostrar que a Vinculação de dados existe no Android! Nunca ouvi falar disso e parece que vou começar a usá-lo. Obrigado
Jorge Casariego 07/02
6
Essa solução é ridiculamente longa e, de maneira geral, exagerada. Vá para o segundo.
Enrico Casini
194

Tudo que você precisa fazer é adicionar o filtermétodo em RecyclerView.Adapter:

public void filter(String text) {
    items.clear();
    if(text.isEmpty()){
        items.addAll(itemsCopy);
    } else{
        text = text.toLowerCase();
        for(PhoneBookItem item: itemsCopy){
            if(item.name.toLowerCase().contains(text) || item.phone.toLowerCase().contains(text)){
                items.add(item);
            }
        }
    }
    notifyDataSetChanged();
}

itemsCopy é inicializado no construtor do adaptador como itemsCopy.addAll(items) .

Se você fizer isso, basta ligar filterpara OnQueryTextListener:

searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
    @Override
    public boolean onQueryTextSubmit(String query) {
        adapter.filter(query);
        return true;
    }

    @Override
    public boolean onQueryTextChange(String newText) {
        adapter.filter(newText);
        return true;
    }
});

É um exemplo de filtragem da minha agenda telefônica por nome e número de telefone.

klimat
fonte
11
Eu acho que essa deve ser a resposta aceita. É mais simples e ele simplesmente funciona
Jose_GD
6
Simples e eficiente!
AlxDroidDev 18/02
11
Observe que você perde a animação se seguir essa abordagem em vez da resposta do @Xaver Kapeller.
humazed
23
Não tentei a resposta aceita porque é muito longa. Esta resposta funciona e fácil de implementar. Não se esqueça de adicionar "app: actionViewClass =" android.support.v7.widget.SearchView "no item de menu XML.
SajithK 13/03/17
3
O que é exatamente itens e itensCopiar aqui?
Lucky_girl
82

Seguindo @Shruthi Kamoji de uma maneira mais limpa, podemos usar um filtro filtrável, para isso:

public abstract class GenericRecycleAdapter<E> extends RecyclerView.Adapter implements Filterable
{
    protected List<E> list;
    protected List<E> originalList;
    protected Context context;

    public GenericRecycleAdapter(Context context,
    List<E> list)
    {
        this.originalList = list;
        this.list = list;
        this.context = context;
    }

    ...

    @Override
    public Filter getFilter() {
        return new Filter() {
            @SuppressWarnings("unchecked")
            @Override
            protected void publishResults(CharSequence constraint, FilterResults results) {
                list = (List<E>) results.values;
                notifyDataSetChanged();
            }

            @Override
            protected FilterResults performFiltering(CharSequence constraint) {
                List<E> filteredResults = null;
                if (constraint.length() == 0) {
                    filteredResults = originalList;
                } else {
                    filteredResults = getFilteredResults(constraint.toString().toLowerCase());
                }

                FilterResults results = new FilterResults();
                results.values = filteredResults;

                return results;
            }
        };
    }

    protected List<E> getFilteredResults(String constraint) {
        List<E> results = new ArrayList<>();

        for (E item : originalList) {
            if (item.getName().toLowerCase().contains(constraint)) {
                results.add(item);
            }
        }
        return results;
    }
} 

OE aqui é um tipo genérico, você pode estendê-lo usando sua classe:

public class customerAdapter extends GenericRecycleAdapter<CustomerModel>

Ou apenas altere o E para o tipo desejado (<CustomerModel> por exemplo)

Em seguida, no searchView (o widget que você pode colocar no menu.xml):

searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
    @Override
    public boolean onQueryTextSubmit(String text) {
        return false;
    }

    @Override
    public boolean onQueryTextChange(String text) {
        yourAdapter.getFilter().filter(text);
        return true;
    }
});
sagits
fonte
Eu uso algo assim! Funciona bem e amostra genérica!
Mateus
Olá, quem pode me ajudar passo-y-passo com esta: stackoverflow.com/questions/40754174/...
Thorvald Olavsen
A resposta mais limpa!
Adalpari
4
Isso é muito melhor do que a resposta votada porque a operação é feita em um thread de trabalho no método performFiltering.
Hmmm
1
Mas você atribui uma referência à mesma lista para diferentes variáveis. Por exemplo, this.originalList = list; Você deve usar addAll vez ou passar a lista no construtor ArrayList
Florian Walther
5

basta criar duas listas no adaptador uma original e uma temporária e implementa o Filterable .

    @Override
    public Filter getFilter() {
        return new Filter() {
            @Override
            protected FilterResults performFiltering(CharSequence constraint) {
                final FilterResults oReturn = new FilterResults();
                final ArrayList<T> results = new ArrayList<>();
                if (origList == null)
                    origList = new ArrayList<>(itemList);
                if (constraint != null && constraint.length() > 0) {
                    if (origList != null && origList.size() > 0) {
                        for (final T cd : origList) {
                            if (cd.getAttributeToSearch().toLowerCase()
                                    .contains(constraint.toString().toLowerCase()))
                                results.add(cd);
                        }
                    }
                    oReturn.values = results;
                    oReturn.count = results.size();//newly Aded by ZA
                } else {
                    oReturn.values = origList;
                    oReturn.count = origList.size();//newly added by ZA
                }
                return oReturn;
            }

            @SuppressWarnings("unchecked")
            @Override
            protected void publishResults(final CharSequence constraint,
                                          FilterResults results) {
                itemList = new ArrayList<>((ArrayList<T>) results.values);
                // FIXME: 8/16/2017 implement Comparable with sort below
                ///Collections.sort(itemList);
                notifyDataSetChanged();
            }
        };
    }

Onde

public GenericBaseAdapter(Context mContext, List<T> itemList) {
        this.mContext = mContext;
        this.itemList = itemList;
        this.origList = itemList;
    }
Zar E Ahmer
fonte
Ótima solução. Criei duas listas e usei um método de filtro simples. Não consigo passar a posição correta do adaptador para um item para a próxima Atividade. Gostaria de receber quaisquer pensamentos ou idéias que você poderia sugerir para este: stackoverflow.com/questions/46027110/…
AJW
4

No adaptador:

public void setFilter(List<Channel> newList){
        mChannels = new ArrayList<>();
        mChannels.addAll(newList);
        notifyDataSetChanged();
    }

Em Atividade:

searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
            @Override
            public boolean onQueryTextSubmit(String query) {
                return false;
            }

            @Override
            public boolean onQueryTextChange(String newText) {
                newText = newText.toLowerCase();
                ArrayList<Channel> newList = new ArrayList<>();
                for (Channel channel: channels){
                    String channelName = channel.getmChannelName().toLowerCase();
                    if (channelName.contains(newText)){
                        newList.add(channel);
                    }
                }
                mAdapter.setFilter(newList);
                return true;
            }
        });
Firoz Ahmed
fonte
3

Com os componentes da arquitetura Android por meio do uso do LiveData, isso pode ser facilmente implementado com qualquer tipo de adaptador . Você simplesmente precisa executar as seguintes etapas:

1. Configure seus dados para retornar do banco de dados da sala como LiveData, como no exemplo abaixo:

@Dao
public interface CustomDAO{

@Query("SELECT * FROM words_table WHERE column LIKE :searchquery")
    public LiveData<List<Word>> searchFor(String searchquery);
}

2. Crie um objeto ViewModel para atualizar seus dados ao vivo através de um método que conectará seu DAO e sua interface do usuário

public class CustomViewModel extends AndroidViewModel {

    private final AppDatabase mAppDatabase;

    public WordListViewModel(@NonNull Application application) {
        super(application);
        this.mAppDatabase = AppDatabase.getInstance(application.getApplicationContext());
    }

    public LiveData<List<Word>> searchQuery(String query) {
        return mAppDatabase.mWordDAO().searchFor(query);
    }

}

3. Ligue para os dados do ViewModel em tempo real, passando a consulta pelo onQueryTextListener, conforme abaixo:

Dentro, onCreateOptionsMenudefina seu ouvinte da seguinte maneira

searchView.setOnQueryTextListener(onQueryTextListener);

Configure seu ouvinte de consulta em algum lugar da sua classe SearchActivity da seguinte maneira

private android.support.v7.widget.SearchView.OnQueryTextListener onQueryTextListener =
            new android.support.v7.widget.SearchView.OnQueryTextListener() {
                @Override
                public boolean onQueryTextSubmit(String query) {
                    getResults(query);
                    return true;
                }

                @Override
                public boolean onQueryTextChange(String newText) {
                    getResults(newText);
                    return true;
                }

                private void getResults(String newText) {
                    String queryText = "%" + newText + "%";
                    mCustomViewModel.searchQuery(queryText).observe(
                            SearchResultsActivity.this, new Observer<List<Word>>() {
                                @Override
                                public void onChanged(@Nullable List<Word> words) {
                                    if (words == null) return;
                                    searchAdapter.submitList(words);
                                }
                            });
                }
            };

Nota : As etapas (1.) e (2.) são a implementação padrão do AAC ViewModel e DAO , a única "mágica" real acontecendo aqui é no OnQueryTextListener, que atualizará os resultados da sua lista dinamicamente à medida que o texto da consulta for alterado.

Se você precisar de mais esclarecimentos sobre o assunto, não hesite em perguntar. Espero que isso tenha ajudado :).

Panos Gr
fonte
1

Esta é minha opinião sobre a expansão da resposta do @klimat para não perder a animação de filtragem.

public void filter(String query){
    int completeListIndex = 0;
    int filteredListIndex = 0;
    while (completeListIndex < completeList.size()){
        Movie item = completeList.get(completeListIndex);
        if(item.getName().toLowerCase().contains(query)){
            if(filteredListIndex < filteredList.size()) {
                Movie filter = filteredList.get(filteredListIndex);
                if (!item.getName().equals(filter.getName())) {
                    filteredList.add(filteredListIndex, item);
                    notifyItemInserted(filteredListIndex);
                }
            }else{
                filteredList.add(filteredListIndex, item);
                notifyItemInserted(filteredListIndex);
            }
            filteredListIndex++;
        }
        else if(filteredListIndex < filteredList.size()){
            Movie filter = filteredList.get(filteredListIndex);
            if (item.getName().equals(filter.getName())) {
                filteredList.remove(filteredListIndex);
                notifyItemRemoved(filteredListIndex);
            }
        }
        completeListIndex++;
    }
}

Basicamente, o que faz é procurar uma lista completa e adicionar / remover itens a uma lista filtrada, um por um.

AhmadF
fonte
0

Eu recomendo modificar a solução do @Xaver Kapeller com as duas coisas abaixo para evitar um problema depois que você limpou o texto pesquisado (o filtro não funcionava mais) porque a lista no verso do adaptador tem tamanho menor que a lista de filtros e ocorreu a IndexOutOfBoundsException. Portanto, o código precisa modificar como abaixo

public void addItem(int position, ExampleModel model) {
    if(position >= mModel.size()) {
        mModel.add(model);
        notifyItemInserted(mModel.size()-1);
    } else {
        mModels.add(position, model);
        notifyItemInserted(position);
    }
}

E modifique também na funcionalidade moveItem

public void moveItem(int fromPosition, int toPosition) {
    final ExampleModel model = mModels.remove(fromPosition);
    if(toPosition >= mModels.size()) {
        mModels.add(model);
        notifyItemMoved(fromPosition, mModels.size()-1);
    } else {
        mModels.add(toPosition, model);
        notifyItemMoved(fromPosition, toPosition); 
    }
}

Espero que possa ajudá-lo!

toidv
fonte
Isso não é de todo necessário.
Xaver Kapeller
Para obter uma resposta original, se você não fizer isso, a IndexOutOfBoundsException acontecerá; então, por que não é necessário ???? Você quer um log? @XaverKapeller
toidv
Não, a exceção só acontecerá se você estiver implementando da Adaptermaneira errada. Sem ver seu código, acho que o problema mais provável é que você não está passando uma cópia da lista com todos os itens para o Adapter.
Xaver Kapeller
O log de erros: W / System.err: java.lang.IndexOutOfBoundsException: índice 36 inválido, o tamanho é 35 W / System.err: em java.util.ArrayList.throwIndexOutOfBoundsException (ArrayList.java:255) W / System.err: Em java.util.ArrayList.add (ArrayList.java:147) W / System.err: em com.quomodo.inploi.ui.adapter.MultipleSelectFilterAdapter.addItem (MultipleSelectFilterAdapter.java:125) W / System.err: at com .quomodo.inploi.ui.adapter.MultipleSelectFilterAdapter.applyAndAnimateAdditions (MultipleSelectFilterAdapter.java:78)
toidv
Por favor, ajude a verificar o código-fonte abaixo @XaverKapeller gist.github.com/toidv/fe71dc45169e4138271b52fdb29420c5
toidv
0

Reciclar visualização com searchview e clicklistener

Adicione uma interface ao seu adaptador.

public interface SelectedUser{

    void selectedUser(UserModel userModel);

}

implemente a interface em sua atividade principal e substitua o método @Override public void selectedUser (UserModel userModel) {

    startActivity(new Intent(MainActivity.this, SelectedUserActivity.class).putExtra("data",userModel));



}

Tutorial completo e código fonte: Recyclerview com searchview e onclicklistener

Richard Kamere
fonte
-1

Eu resolvi o mesmo problema usando o link com algumas modificações. Filtro de pesquisa no RecyclerView with Cards. Isso é possível?(espero que isto ajude).

Aqui está minha classe de adaptadores

public class ContactListRecyclerAdapter extends RecyclerView.Adapter<ContactListRecyclerAdapter.ContactViewHolder> implements Filterable {

Context mContext;
ArrayList<Contact> customerList;
ArrayList<Contact> parentCustomerList;


public ContactListRecyclerAdapter(Context context,ArrayList<Contact> customerList)
{
    this.mContext=context;
    this.customerList=customerList;
    if(customerList!=null)
    parentCustomerList=new ArrayList<>(customerList);
}

   // other overrided methods

@Override
public Filter getFilter() {
    return new FilterCustomerSearch(this,parentCustomerList);
}
}

// Classe de filtro

import android.widget.Filter;
import java.util.ArrayList;


public class FilterCustomerSearch extends Filter
{
private final ContactListRecyclerAdapter mAdapter;
ArrayList<Contact> contactList;
ArrayList<Contact> filteredList;

public FilterCustomerSearch(ContactListRecyclerAdapter mAdapter,ArrayList<Contact> contactList) {
    this.mAdapter = mAdapter;
    this.contactList=contactList;
    filteredList=new ArrayList<>();
}

@Override
protected FilterResults performFiltering(CharSequence constraint) {
    filteredList.clear();
    final FilterResults results = new FilterResults();

    if (constraint.length() == 0) {
        filteredList.addAll(contactList);
    } else {
        final String filterPattern = constraint.toString().toLowerCase().trim();

        for (final Contact contact : contactList) {
            if (contact.customerName.contains(constraint)) {
                filteredList.add(contact);
            }
            else if (contact.emailId.contains(constraint))
            {
                filteredList.add(contact);

            }
            else if(contact.phoneNumber.contains(constraint))
                filteredList.add(contact);
        }
    }
    results.values = filteredList;
    results.count = filteredList.size();
    return results;
}

@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
    mAdapter.customerList.clear();
    mAdapter.customerList.addAll((ArrayList<Contact>) results.values);
    mAdapter.notifyDataSetChanged();
}

}

// Classe de atividade

public class HomeCrossFadeActivity extends AppCompatActivity implements View.OnClickListener,OnFragmentInteractionListener,OnTaskCompletedListner
{
Fragment fragment;
 protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_homecrossfadeslidingpane2);CardView mCard;
   setContentView(R.layout.your_main_xml);}
   //other overrided methods
  @Override
   public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.

    MenuInflater inflater = getMenuInflater();
    // Inflate menu to add items to action bar if it is present.
    inflater.inflate(R.menu.menu_customer_view_and_search, menu);
    // Associate searchable configuration with the SearchView
    SearchManager searchManager =
            (SearchManager) getSystemService(Context.SEARCH_SERVICE);
    SearchView searchView =
            (SearchView) menu.findItem(R.id.menu_search).getActionView();
    searchView.setQueryHint("Search Customer");
    searchView.setSearchableInfo(
            searchManager.getSearchableInfo(getComponentName()));

    searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
        @Override
        public boolean onQueryTextSubmit(String query) {
            return false;
        }

        @Override
        public boolean onQueryTextChange(String newText) {
            if(fragment instanceof CustomerDetailsViewWithModifyAndSearch)
                ((CustomerDetailsViewWithModifyAndSearch)fragment).adapter.getFilter().filter(newText);
            return false;
        }
    });



    return true;
}
}

No método OnQueryTextChangeListener (), use seu adaptador. Eu o projetei para fragmentar como meu adpter está em fragmento. Você pode usar o adaptador diretamente se estiver em sua classe de atividade.

Shruthi Kamoji
fonte