Come filtrare un RecyclerView con un SearchView


319

Sto cercando di implementare il SearchViewdalla libreria di supporto. Voglio che l'utente utilizzi SearchViewper filtrare un Listfilm in a RecyclerView.

Finora ho seguito alcuni tutorial e ho aggiunto SearchViewil ActionBar, ma non sono sicuro di dove andare da qui. Ho visto alcuni esempi, ma nessuno di loro mostra risultati quando inizi a digitare.

Questo è il mio 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 questo è il mio 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);
        }
    }
}

Risposte:


913

introduzione

Dal momento che non è davvero chiaro dalla tua domanda con cosa stai riscontrando esattamente problemi, ho scritto una breve guida su come implementare questa funzione; se hai ancora domande non esitare a chiedere.

Ho un esempio funzionante di tutto ciò di cui sto parlando qui in questo repository GitHub .
Se vuoi saperne di più sul progetto di esempio visita la pagina iniziale del progetto .

In ogni caso il risultato dovrebbe essere simile al seguente:

immagine dimostrativa

Se vuoi giocare per la prima volta con l'app demo, puoi installarla dal Play Store:

Scaricalo su Google Play

Comunque iniziamo.


Impostazione di SearchView

Nella cartella res/menucreare un nuovo file chiamato main_menu.xml. In esso aggiungere un elemento e impostare l' actionViewClassa android.support.v7.widget.SearchView. Poiché stai utilizzando la libreria di supporto, devi utilizzare lo spazio dei nomi della libreria di supporto per impostare l' actionViewClassattributo. Il tuo file xml dovrebbe assomigliare a questo:

<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>

Nel tuo Fragmento Activitydevi gonfiare questo menu xml come al solito, quindi puoi cercare quello MenuItemche contiene SearchViewe implementare quello OnQueryTextListenerche useremo per ascoltare le modifiche al testo inserito nel 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 ora SearchViewè pronto per essere utilizzato. In seguito implementeremo la logica del filtro onQueryTextChange()una volta terminata l'implementazione di Adapter.


Impostazione di Adapter

Innanzitutto questa è la classe di modello che userò per questo esempio:

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;
    }
}

È solo il tuo modello di base che visualizzerà un testo in RecyclerView. Questo è il layout che userò per visualizzare il testo:

<?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>

Come puoi vedere, utilizzo il Data Binding. Se non hai mai lavorato prima con l'associazione dei dati, non scoraggiarti! È molto semplice e potente, tuttavia non riesco a spiegare come funzioni nell'ambito di questa risposta.

Questo è ViewHolderper la 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);
    }
}

Ancora niente di speciale. Utilizza semplicemente l'associazione dei dati per associare la classe del modello a questo layout, come abbiamo definito nel layout xml sopra.

Ora possiamo finalmente arrivare alla parte davvero interessante: scrivere l'adattatore. Ho intenzione di saltare l'implementazione di base di Adaptere mi concentrerò invece sulle parti che sono rilevanti per questa risposta.

Ma prima c'è una cosa di cui dobbiamo parlare: la SortedListclasse.


SortedList

Il SortedListè uno strumento completamente stupefacente che fa parte della RecyclerViewbiblioteca. Si occupa di notificare le Adaptermodifiche al set di dati e lo fa in modo molto efficiente. L'unica cosa che devi fare è specificare un ordine degli elementi. È necessario farlo implementando un compare()metodo che confronta due elementi nel SortedListproprio come un Comparator. Ma invece di ordinare un Listviene usato per ordinare gli oggetti nel RecyclerView!

Le SortedListinteragisce con il Adaptertramite di una Callbackclasse che è necessario implementare:

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();
    }
}

Nei metodi nella parte superiore della richiamata come onMoved, onInsertedecc dovete chiamare l'equivalente metodo vostra notificare Adapter. I tre metodi in basso compare, areContentsTheSamee areItemsTheSamedevi implementare in base al tipo di oggetti che vuoi visualizzare e in quale ordine questi oggetti dovrebbero apparire sullo schermo.

Esaminiamo questi metodi uno per uno:

@Override
public int compare(ExampleModel a, ExampleModel b) {
    return mComparator.compare(a, b);
}

Questo è il compare()metodo di cui ho parlato prima. In questo esempio sto solo passando la chiamata a una Comparatorche confronta i due modelli. Se si desidera che gli elementi vengano visualizzati in ordine alfabetico sullo schermo. Questo comparatore potrebbe apparire così:

private static final Comparator<ExampleModel> ALPHABETICAL_COMPARATOR = new Comparator<ExampleModel>() {
    @Override
    public int compare(ExampleModel a, ExampleModel b) {
        return a.getText().compareTo(b.getText());
    }
};

Ora diamo un'occhiata al prossimo metodo:

@Override
public boolean areContentsTheSame(ExampleModel oldItem, ExampleModel newItem) {
    return oldItem.equals(newItem);
}

Lo scopo di questo metodo è determinare se il contenuto di un modello è cambiato. Lo SortedListutilizza per determinare se è necessario invocare un evento di modifica, in altre parole se si RecyclerViewdeve dissolvere la versione precedente e nuova. Se le tue classi di modelli hanno una corretta equals()e hashCode()implementazione, di solito puoi semplicemente implementarla come sopra. Se aggiungiamo un equals()e hashCode()implementazione per la ExampleModelclasse che dovrebbe essere simile a questa:

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 a margine: la maggior parte degli IDE come Android Studio, IntelliJ ed Eclipse hanno funzionalità per generare equals()e hashCode()implementazioni per te con la semplice pressione di un pulsante! Quindi non è necessario implementarli da soli. Cerca su Internet come funziona nel tuo IDE!

Ora diamo un'occhiata all'ultimo metodo:

@Override
public boolean areItemsTheSame(ExampleModel item1, ExampleModel item2) {
    return item1.getId() == item2.getId();
}

Il SortedListutilizza questo metodo per controllare se due voci si riferiscono alla stessa cosa. In termini più semplici (senza spiegare come SortedListfunziona) questo viene usato per determinare se un oggetto è già contenuto Listnell'animazione e se è necessario riprodurre un'animazione di aggiunta, spostamento o modifica. Se i tuoi modelli hanno un ID, di solito confronteresti solo l'id in questo metodo. In caso contrario, devi trovare un altro modo per verificarlo, ma comunque finisci per implementarlo dipende dalla tua app specifica. Di solito è l'opzione più semplice per fornire un ID a tutti i modelli, che potrebbe ad esempio essere il campo chiave principale se si esegue una query sui dati da un database.

Con l' SortedList.Callbackimplementazione corretta possiamo creare un'istanza di SortedList:

final SortedList<ExampleModel> list = new SortedList<>(ExampleModel.class, mCallback);

Come primo parametro nel costruttore del SortedListè necessario passare la classe dei tuoi modelli. L'altro parametro è proprio quello SortedList.Callbacksopra definito.

Ora mettiamoci al lavoro: se implementiamo il Adaptercon un SortedList, dovrebbe assomigliare a questo:

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();
    }
}

L' Comparatorusato per ordinare l'oggetto viene passato attraverso il costruttore in modo che possiamo usare lo stesso Adapteranche se si suppone che gli articoli vengano visualizzati in un ordine diverso.

Ora abbiamo quasi finito! Ma prima abbiamo bisogno di un modo per aggiungere o rimuovere elementi su Adapter. A tale scopo possiamo aggiungere metodi ai Adapterquali ci consentono di aggiungere e rimuovere elementi a 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();
}

Non abbiamo bisogno di chiamare alcun metodo di notifica qui perché lo SortedListfa già per tramite SortedList.Callback! A parte ciò, l'implementazione di questi metodi è piuttosto semplice con un'eccezione: il metodo remove che rimuove un Listmodello. Dal momento che SortedListha un solo metodo di rimozione che può rimuovere un singolo oggetto, dobbiamo scorrere l'elenco e rimuovere i modelli uno per uno. Chiamando beginBatchedUpdates()all'inizio vengono SortedListraggruppati tutti i cambiamenti che andremo a fare insieme e migliorano le prestazioni. Quando chiamiamo endBatchedUpdates()il RecyclerViewviene notificato tutte le modifiche in una volta.

Inoltre, ciò che devi capire è che se aggiungi un oggetto a SortedListed è già SortedListpresente non verrà aggiunto di nuovo. Invece SortedListusa il areContentsTheSame()metodo per capire se l'oggetto è cambiato - e se ha l'elemento in RecyclerViewverrà aggiornato.

Comunque, quello che di solito preferisco è un metodo che mi consente di sostituire tutti gli elementi in RecyclerViewuna volta. Rimuovi tutto ciò che non è presente Liste aggiungi tutti gli elementi mancanti da 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();
}

Questo metodo raggruppa nuovamente tutti gli aggiornamenti per aumentare le prestazioni. Il primo ciclo è al contrario poiché la rimozione di un elemento all'inizio comprometterebbe gli indici di tutti gli elementi che ne derivano e ciò può portare in alcuni casi a problemi come incoerenze dei dati. Dopodiché aggiungiamo semplicemente Listl' SortedListuso addAll()per aggiungere tutti gli elementi che non sono già presenti in SortedListe - proprio come ho descritto sopra - aggiorniamo tutti gli elementi che sono già presenti in SortedListma che sono stati modificati.

E con ciò il Adapterè completo. L'intero aspetto dovrebbe assomigliare a questo:

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();
    }
}

L'unica cosa che manca ora è implementare il filtro!


Implementazione della logica del filtro

Per implementare la logica del filtro dobbiamo prima definire uno Listdi tutti i possibili modelli. Per questo esempio creo una Listdelle ExampleModelistanze da una serie di film:

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);
}

Non sta succedendo nulla di speciale qui, abbiamo solo un'istanza di Adaptere impostato su RecyclerView. Successivamente, creiamo una serie Listdi modelli dai nomi dei film nella MOVIESmatrice. Quindi aggiungiamo tutti i modelli a SortedList.

Ora possiamo tornare a ciò onQueryTextChange()che abbiamo definito in precedenza e iniziare a implementare la logica del filtro:

@Override
public boolean onQueryTextChange(String query) {
    final List<ExampleModel> filteredModelList = filter(mModels, query);
    mAdapter.replaceAll(filteredModelList);
    mBinding.recyclerView.scrollToPosition(0);
    return true;
}

Questo è di nuovo piuttosto semplice. Chiamiamo il metodo filter()e passiamo in Listof ExampleModels così come nella stringa di query. Chiamiamo quindi replaceAll()il Adaptere passiamo al filtro Listrestituito da filter(). Dobbiamo anche chiamare scrollToPosition(0)il RecyclerViewper garantire che l'utente possa sempre vedere tutti gli elementi durante la ricerca di qualcosa. In caso contrario, RecyclerViewpotrebbero rimanere in una posizione di scorrimento verso il basso durante il filtraggio e successivamente nascondere alcuni elementi. Lo scorrimento verso l'alto assicura un'esperienza utente migliore durante la ricerca.

L'unica cosa che resta da fare ora è implementare filter()se stesso:

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;
}

La prima cosa che facciamo qui è chiamare toLowerCase()la stringa di query. Non vogliamo che la nostra funzione di ricerca sia sensibile al maiuscolo / minuscolo e chiamando toLowerCase()tutte le stringhe che confrontiamo possiamo assicurarci di restituire gli stessi risultati indipendentemente dal caso. Quindi scorre solo attraverso tutti i modelli in cui Listsiamo passati e verifica se la stringa di query è contenuta nel testo del modello. In tal caso, il modello viene aggiunto al filtro List.

E questo è tutto! Il codice sopra verrà eseguito a livello di API 7 e superiore e a partire dal livello API 11 otterrai animazioni di oggetti gratis!

Mi rendo conto che questa è una descrizione molto dettagliata che probabilmente rende l'intera faccenda più complicata di quanto non sia in realtà, ma c'è un modo per generalizzare l'intero problema e rendere l'implementazione Adapterbasata su una SortedListmolto più semplice.


Generalizzare il problema e semplificare l'adattatore

In questa sezione non ho intenzione di entrare nei dettagli, in parte perché sto correndo contro il limite di caratteri per le risposte su Stack Overflow, ma anche perché la maggior parte è già spiegata sopra, ma per riassumere le modifiche: possiamo implementare una Adapterclasse base che si occupa già di gestire i SortedListmodelli oltre che vincolanti per le ViewHolderistanze e fornisce un modo conveniente per implementare un Adapterbasato su a SortedList. Per questo dobbiamo fare due cose:

  • Dobbiamo creare ViewModelun'interfaccia che tutte le classi di modelli devono implementare
  • Dobbiamo creare una ViewHoldersottoclasse che definisce un bind()metodo che Adapterpuò essere utilizzato per associare automaticamente i modelli.

Questo ci consente di concentrarci solo sul contenuto che dovrebbe essere visualizzato nel RecyclerViewsemplicemente implementando i modelli e le relative ViewHolderimplementazioni. Usando questa classe base non dobbiamo preoccuparci degli intricati dettagli di Adaptere suoi SortedList.

SortedListAdapter

A causa del limite di caratteri per le risposte su StackOverflow non posso passare attraverso ogni fase dell'implementazione di questa classe di base o persino aggiungere qui il codice sorgente completo, ma puoi trovare il codice sorgente completo di questa classe base - l'ho chiamato SortedListAdapter- in questo GitHub Gist .

Per semplificarti la vita, ho pubblicato una libreria su jCenter che contiene il SortedListAdapter! Se vuoi usarlo, tutto ciò che devi fare è aggiungere questa dipendenza al file build.gradle della tua app:

compile 'com.github.wrdlbrnft:sorted-list-adapter:0.2.0.1'

Puoi trovare maggiori informazioni su questa biblioteca nella homepage della biblioteca .

Utilizzo di SortedListAdapter

Per usare il SortedListAdapterdobbiamo fare due cambiamenti:

  • Cambia in ViewHoldermodo che si estenda SortedListAdapter.ViewHolder. Il parametro type dovrebbe essere il modello che dovrebbe essere associato a questo ViewHolder- in questo caso ExampleModel. Devi associare i dati ai tuoi modelli performBind()anziché a 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);
        }
    }
    
  • Assicurati che tutti i tuoi modelli implementino l' ViewModelinterfaccia:

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

Dopodiché non ci resta che aggiornare il ExampleAdapterper estendere SortedListAdaptere rimuovere tutto ciò di cui non abbiamo più bisogno. Il parametro type dovrebbe essere il tipo di modello con cui stai lavorando, in questo caso ExampleModel. Ma se stai lavorando con diversi tipi di modelli, imposta il parametro type su 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);
    }
}

Dopo di ciò abbiamo finito! Comunque un'ultima cosa da menzionare: The SortedListAdapternon ha lo stesso add(), remove()o replaceAll()metodi che il nostro originale ExampleAdapteraveva. Utilizza un Editoroggetto separato per modificare gli elementi nell'elenco a cui è possibile accedere tramite il edit()metodo. Quindi, se si desidera rimuovere o aggiungere elementi che è necessario chiamare, edit()aggiungere e rimuovere gli elementi in questa Editoristanza e, una volta terminato, chiamare commit()per applicare le modifiche a SortedList:

mAdapter.edit()
        .remove(modelToRemove)
        .add(listOfModelsToAdd)
        .commit();

Tutte le modifiche apportate in questo modo vengono raggruppate per aumentare le prestazioni. Il replaceAll()metodo che abbiamo implementato nei capitoli sopra è presente anche su questo Editoroggetto:

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

Se si dimentica di chiamare, commit()nessuna delle modifiche verrà applicata!


4
@TiagoOliveira Beh, è ​​fatto per funzionare fuori dagli schemi: D L'associazione dei dati è un ostacolo per le persone che non hanno familiarità con esso, ma l'ho incluso comunque perché è fantastico e voglio promuoverlo. Per qualche ragione, non molte persone sembrano saperlo ...
Xaver Kapeller,

78
Non ho ancora letto l'intera risposta, ho dovuto mettere in pausa la mia lettura da qualche parte a metà per scrivere questo commento - questa è una delle migliori risposte che ho trovato qui sul SO! Grazie!
daneejela,

16
Adoro come sei: "Dalla tua domanda non è chiaro con cosa stai avendo problemi, quindi ecco un esempio completo che ho appena fatto": D
Fred

7
+1 solo per mostrarci che esiste il Data Binding in Android! Non ne ho mai sentito parlare e sembra che inizierò ad usarlo. Grazie
Jorge Casariego,

6
Questa soluzione è ridicolmente lunga e in generale troppo ingegnerizzata. Scegli il secondo.
Enrico Casini,

194

Tutto quello che devi fare è aggiungere il filtermetodo in 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è inizializzato nel costruttore dell'adattatore come itemsCopy.addAll(items).

Se lo fai, chiama filterda 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;
    }
});

È un esempio del filtraggio della mia rubrica per nome e numero di telefono.


11
Penso che questa dovrebbe essere la risposta accettata. È più semplice e funziona
Jose_GD

6
Semplice ed efficiente!
AlxDroidDev,

11
Nota che perdi l'animazione se segui questo approccio invece della risposta di @Xaver Kapeller.
impazzito il

23
Non ho provato la risposta accettata perché è troppo lunga. Questa risposta funziona e facile da implementare. Non dimenticare di aggiungere "app: actionViewClass =" android.support.v7.widget.SearchView "sulla voce di menu XML.
SajithK

3
Che cosa sono esattamente gli articoli e gli articoli Copia qui?
Lucky_girl,

82

Seguendo @Shruthi Kamoji in modo più pulito, possiamo semplicemente usare un filtro, che è pensato per questo:

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;
    }
} 

La E qui è di tipo generico, puoi estenderla usando la tua classe:

public class customerAdapter extends GenericRecycleAdapter<CustomerModel>

O semplicemente cambia la E nel tipo che desideri ( <CustomerModel>ad esempio)

Quindi da searchView (il widget che puoi mettere su 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;
    }
});

Uso qualcosa del genere! Funziona bene e campione generico!
Mateus,

Ciao, chi mi può aiutare passo-y-passo con questo: stackoverflow.com/questions/40754174/...
Thorvald Olavsen

La risposta più pulita!
adalpari,

4
Questo è molto meglio della risposta votata perché l'operazione viene eseguita su un thread di lavoro nel metodo performFiltering.
Hmmm,

1
Ma si assegna un riferimento allo stesso Elenco a variabili diverse. Ad esempio this.originalList = list; Dovresti usare addAll invece o passare l'elenco nel costruttore ArrayList
Florian Walther,

5

basta creare due elenchi nell'adattatore uno originale e uno temporaneo e implementare 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();
            }
        };
    }

dove

public GenericBaseAdapter(Context mContext, List<T> itemList) {
        this.mContext = mContext;
        this.itemList = itemList;
        this.origList = itemList;
    }

Bella soluzione. Ho creato due elenchi e utilizzato un metodo di filtro semplice. Non riesco a passare la posizione corretta dell'adattatore per un articolo all'attività successiva. Gradirei qualsiasi pensiero o idea che potresti suggerire per questo: stackoverflow.com/questions/46027110/…
AJW,

4

Nell'adattatore:

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

In attività:

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;
            }
        });

3

Con Android Architecture Components tramite LiveData questo può essere facilmente implementato con qualsiasi tipo di adattatore . Devi semplicemente fare i seguenti passi:

1. Configura i tuoi dati per restituirli dal database di Room come LiveData come nell'esempio seguente:

@Dao
public interface CustomDAO{

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

2. Creare un oggetto ViewModel per aggiornare i dati in tempo reale attraverso un metodo che collegherà il DAO e l' interfaccia utente

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. Chiama i tuoi dati da ViewModel al volo passando la query tramite onQueryTextListener come di seguito:

All'interno onCreateOptionsMenuimposta il tuo ascoltatore come segue

searchView.setOnQueryTextListener(onQueryTextListener);

Imposta il tuo listener di query da qualche parte nella classe SearchActivity come segue

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 : i passaggi (1.) e (2.) sono l' implementazione standard di AAC ViewModel e DAO , l'unica vera "magia" in corso qui è in OnQueryTextListener che aggiornerà i risultati dell'elenco in modo dinamico man mano che il testo della query cambia.

Se hai bisogno di ulteriori chiarimenti in merito, non esitare a chiedere. Spero che questo abbia aiutato :).


1

Questa è la mia opinione sull'espansione della risposta di @klimat per non perdere l'animazione del filtro.

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++;
    }
}

Fondamentalmente quello che fa è guardare attraverso un elenco completo e aggiungere / rimuovere elementi a un elenco filtrato uno per uno.


0

Raccomando di modificare la soluzione di @Xaver Kapeller con 2 cose di seguito per evitare un problema dopo aver cancellato il testo cercato (il filtro non funzionava più) a causa dell'elenco di adattatori con dimensioni inferiori rispetto all'elenco di filtri e IndexOutOfBoundsException si è verificato. Quindi il codice deve essere modificato come di seguito

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 modifica anche nella funzionalità 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); 
    }
}

Spero che possa aiutarti!


Questo non è affatto necessario.
Xaver Kapeller,

Per una risposta originale, se non lo fai, accadrà IndexOutOfBoundsException, quindi perché non è necessario ???? Vuoi un registro? @XaverKapeller
toidv

Nessuna eccezione si verificherà solo se si sta implementando nel Adaptermodo sbagliato. Senza vedere il tuo codice immagino che il problema più probabile sia che non stai passando una copia dell'elenco con tutti gli elementi al Adapter.
Xaver Kapeller,

Il registro errori: W / System.err: java.lang.IndexOutOfBoundsException: indice 36 non valido, la dimensione è 35 W / System.err: at java.util.ArrayList.throwIndexOutOfBoundsException (ArrayList.java:255) W / System.err: at java.util.ArrayList.add (ArrayList.java:147) W / System.err: at com.quomodo.inploi.ui.adapter.MultipleSelectFilterAdapter.addItem (MultipleSelectFilterAdapter.java:125) W / System.err: com .quomodo.inploi.ui.adapter.MultipleSelectFilterAdapter.applyAndAnimateAdditions (MultipleSelectFilterAdapter.java:78)
toidv

Aiuta a controllare il codice sorgente di seguito @XaverKapeller gist.github.com/toidv/fe71dc45169e4138271b52fdb29420c5
toidv

0

Recyclerview con searchview e clicklistener

Aggiungi un'interfaccia nel tuo adattatore.

public interface SelectedUser{

    void selectedUser(UserModel userModel);

}

implementare l'interfaccia nella tua attività principale e sovrascrivere il metodo. @Override void pubblico selezionatoUser (UserModel userModel) {

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



}

Tutorial completo e codice sorgente: Recyclerview con searchview e onclicklistener


-1

Ho risolto lo stesso problema usando il link con alcune modifiche. Filtro di ricerca su RecyclerView with Cards. È anche possibile? (spero che questo ti aiuti).

Ecco la mia classe di adattatore

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 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 di attività

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;
}
}

Nel metodo OnQueryTextChangeListener () utilizzare l'adattatore. L'ho lanciato in frammento poiché il mio adptto è in frammento. È possibile utilizzare l'adattatore direttamente se è nella propria classe di attività.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.