Comment filtrer un RecyclerView avec un SearchView


319

J'essaye d'implémenter le SearchViewdepuis la bibliothèque de support. Je veux que l'utilisateur utilise le SearchViewpour filtrer un Listdes films dans unRecyclerView .

J’ai suivi quelques tutoriels jusqu’à présent et j’ai ajouté SearchViewleActionBar , mais je ne sais pas vraiment où aller à partir d'ici. J'ai vu quelques exemples, mais aucun d'eux ne donne de résultats lorsque vous commencez à taper.

C'est mon 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);
    }
}

Et voici mon 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);
        }
    }
}

Réponses:


913

introduction

Puisqu'il n'est pas vraiment clair de votre question avec quoi exactement vous rencontrez des problèmes, j'ai écrit cette procédure pas à pas sur la façon de mettre en œuvre cette fonctionnalité; si vous avez encore des questions, n'hésitez pas à demander.

J'ai un exemple de travail de tout ce dont je parle ici dans ce référentiel GitHub .
Si vous voulez en savoir plus sur l'exemple de projet, visitez la page d'accueil du projet .

Dans tous les cas, le résultat devrait ressembler à ceci:

image de démonstration

Si vous souhaitez d'abord jouer avec l'application de démonstration, vous pouvez l'installer à partir du Play Store:

Obtenez le sur Google Play

Quoi qu'il en soit, commençons.


Configuration du SearchView

Dans le dossier, res/menucréez un nouveau fichier appelé main_menu.xml. Dans ce document, ajoutez un élément et définissez le actionViewClasssur android.support.v7.widget.SearchView. Puisque vous utilisez la bibliothèque de support, vous devez utiliser l'espace de noms de la bibliothèque de support pour définir l' actionViewClassattribut. Votre fichier xml devrait ressembler à ceci:

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

Dans votre Fragmentou Activityvous devez gonfler ce menu xml comme d'habitude, alors vous pouvez rechercher celui MenuItemqui contient le SearchViewet implémenter le OnQueryTextListenerque nous allons utiliser pour écouter les modifications du texte entré dans le 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;
}

Et maintenant, le SearchViewest prêt à être utilisé. Nous implémenterons la logique de filtrage plus tard onQueryTextChange()une fois que nous aurons terminé d'implémenter le Adapter.


Configuration du Adapter

C'est avant tout la classe modèle que je vais utiliser pour cet exemple:

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

C'est juste votre modèle de base qui affichera un texte dans le RecyclerView . Voici la disposition que je vais utiliser pour afficher le texte:

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

Comme vous pouvez le voir, j'utilise la liaison de données. Si vous n'avez jamais travaillé avec la liaison de données auparavant, ne vous découragez pas! C'est très simple et puissant, mais je ne peux pas expliquer comment cela fonctionne dans le cadre de cette réponse.

C'est ViewHolderpour 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);
    }
}

Encore une fois rien de spécial. Il utilise simplement la liaison de données pour lier la classe de modèle à cette disposition comme nous l'avons défini dans le xml de disposition ci-dessus.

Maintenant, nous pouvons enfin arriver à la partie vraiment intéressante: écrire l'adaptateur. Je vais sauter la mise en œuvre de base de laAdapter et je vais plutôt me concentrer sur les parties qui sont pertinentes pour cette réponse.

Mais d'abord, nous devons parler d'une chose: la SortedListclasse.


SortedList

Le SortedListest un outil complètement incroyable qui fait partie de la RecyclerViewbibliothèque. Il prend soin de notifier les Adaptermodifications apportées à l'ensemble de données et le fait de manière très efficace. La seule chose qu'il vous oblige à faire est de spécifier un ordre des éléments. Vous devez le faire en implémentant une compare()méthode qui compare deux éléments dans le SortedListmême que a Comparator. Mais au lieu de trier un, Listil est utilisé pour trier les éléments dans le RecyclerView!

Le SortedListinteragit avec le Adaptervia une Callbackclasse que vous devez implémenter:

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

Dans les méthodes en haut de la fonction de rappel comme onMoved, onInserted, etc. , vous devez appeler l'équivalent notify méthode de votre Adapter. Les trois méthodes en bas compare, areContentsTheSameetareItemsTheSame vous devez implémenter en fonction du type d'objets que vous souhaitez afficher et dans quel ordre ces objets doivent apparaître à l'écran.

Passons en revue ces méthodes une par une:

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

C'est la compare()méthode dont j'ai parlé plus tôt. Dans cet exemple, je passe juste l'appel à un Comparatorqui compare les deux modèles. Si vous souhaitez que les éléments apparaissent par ordre alphabétique à l'écran. Ce comparateur pourrait ressembler à ceci:

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

Voyons maintenant la méthode suivante:

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

Le but de cette méthode est de déterminer si le contenu d'un modèle a changé. L' SortedListutilise pour déterminer si un événement de modification doit être appelé - en d'autres termes, si le RecyclerViewfondu enchaîné avec l'ancienne et la nouvelle version. Si vous modélisez les classes avec une implémentation correcte equals(), hashCode()vous pouvez généralement l'implémenter comme ci-dessus. Si nous ajoutons une implémentation equals()et hashCode()à la ExampleModelclasse, cela devrait ressembler à ceci:

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

Note rapide: la plupart des IDE comme Android Studio, IntelliJ et Eclipse ont des fonctionnalités pour générer equals()et hashCode()implémenter pour vous en appuyant sur un bouton! Vous n'avez donc pas à les mettre en œuvre vous-même. Regardez sur Internet comment cela fonctionne dans votre IDE!

Voyons maintenant la dernière méthode:

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

Le SortedListutilise cette méthode pour vérifier si deux éléments font référence à la même chose. En termes plus simples (sans expliquer comment cela SortedListfonctionne), ceci est utilisé pour déterminer si un objet est déjà contenu dans leList et si une animation d'ajout, de déplacement ou de changement doit être lue. Si vos modèles ont un identifiant, vous ne comparez généralement que l'identifiant dans cette méthode. S'ils ne le font pas, vous devez trouver un autre moyen de vérifier cela, mais vous finirez par l'implémenter, cela dépend de votre application spécifique. Habituellement, c'est l'option la plus simple de donner à tous les modèles un identifiant - qui pourrait par exemple être le champ de clé primaire si vous interrogez les données d'une base de données.

Avec l' SortedList.Callbackimplémentation correcte, nous pouvons créer une instance de laSortedList :

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

En tant que premier paramètre du constructeur du SortedList vous devez passer la classe de vos modèles. L'autre paramètre est juste celui que SortedList.Callbacknous avons défini ci-dessus.

Passons maintenant aux choses sérieuses: si nous implémentons le Adapteravec un, SortedListil devrait ressembler à ceci:

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

le Comparator outil utilisé pour trier l'élément est transmis via le constructeur afin que nous puissions utiliser le même, Adaptermême si les éléments sont censés être affichés dans un ordre différent.

Maintenant, nous avons presque terminé! Mais nous avons d'abord besoin d'un moyen d'ajouter ou de supprimer des éléments dans le Adapter. À cette fin, nous pouvons ajouter des méthodes à la Adapterqui nous permettent d'ajouter et de supprimer des éléments à la 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();
}

Nous n'avons pas besoin d'appeler de méthodes de notification ici, car le fait SortedListdéjà cela via le SortedList.Callback! En dehors de cela, l'implémentation de ces méthodes est assez simple à une exception près: la méthode remove qui supprime un Listdes modèles. Étant donné SortedListque la méthode a une seule suppression qui peut supprimer un seul objet, nous devons parcourir la liste et supprimer les modèles un par un. L'appel beginBatchedUpdates()au début regroupe tous les changements que nous allons apporter à l' SortedListensemble et améliore les performances. Quand on appelleendBatchedUpdates() le RecyclerViewest informé de tous les changements à la fois.

De plus, ce que vous devez comprendre, c'est que si vous ajoutez un objet au SortedListet qu'il est déjà dans le, SortedListil ne sera pas ajouté à nouveau. Au lieu de cela, la méthode SortedListutilise areContentsTheSame()pour déterminer si l'objet a changé - et s'il contient l'élément dans le RecyclerViewsera mis à jour.

Quoi qu'il en soit, ce que je préfère habituellement, c'est une méthode qui me permet de remplacer tous les articles en RecyclerViewune seule fois. Supprimez tout ce qui n'est pas dans le Listet ajoutez tous les éléments manquants dans le 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();
}

Cette méthode regroupe à nouveau toutes les mises à jour pour augmenter les performances. La première boucle est inversée car la suppression d'un élément au début gâcherait les index de tous les éléments qui le suivent et cela peut entraîner dans certains cas des problèmes tels que des incohérences de données. Après cela, nous ajoutons simplement le Listà l' SortedListaide de addAll()pour ajouter tous les éléments qui ne sont pas déjà dans leSortedList et - comme je l'ai décrit ci-dessus - mettez à jour tous les éléments qui sont déjà dans le SortedListmais qui ont changé.

Et avec cela le Adapterest complet. Le tout devrait ressembler à ceci:

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

La seule chose qui manque maintenant est d'implémenter le filtrage!


Implémentation de la logique de filtrage

Pour implémenter la logique de filtrage, nous devons d'abord définir un Listde tous les modèles possibles. Pour cet exemple , je crée une Listdes ExampleModelinstances d'un tableau de films:

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

Rien de spécial ne se passe ici, nous instancions juste le Adapteret le mettons au RecyclerView. Après cela, nous créons un Listdes modèles à partir des noms de films dans le MOVIEStableau. Ensuite, nous ajoutons tous les modèles au SortedList.

Maintenant, nous pouvons revenir à onQueryTextChange()ce que nous avons défini précédemment et commencer à implémenter la logique de filtrage:

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

C'est encore une fois assez simple. Nous appelons la méthode filter()et passons le Listde ExampleModels ainsi que la chaîne de requête. Nous appelons ensuite replaceAll()le Adapteret passons le filtré Listrenvoyé par filter(). Nous devons également faire appel scrollToPosition(0)à RecyclerViewpour garantir que l'utilisateur puisse toujours voir tous les éléments lorsqu'il recherche quelque chose. Sinon, le RecyclerViewpeut rester dans une position déroulante pendant le filtrage et masquer ensuite quelques éléments. Faire défiler vers le haut garantit une meilleure expérience utilisateur lors de la recherche.

Il ne reste plus qu'à s'implémenter 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;
}

La première chose que nous faisons ici est d'appeler toLowerCase()la chaîne de requête. Nous ne voulons pas que notre fonction de recherche soit sensible à la casse et en appelant toLowerCase()toutes les chaînes que nous comparons, nous pouvons nous assurer que nous retournons les mêmes résultats indépendamment de la casse. Il réitère ensuite tous les modèles du fichier que Listnous lui avons transmis et vérifie si la chaîne de requête est contenue dans le texte du modèle. Si c'est le cas, le modèle est ajouté au filtre List.

Et c'est tout! Le code ci-dessus fonctionnera au niveau 7 et supérieur de l'API et à partir du niveau 11 de l'API, vous obtiendrez des animations d'objets gratuitement!

Je me rends compte que c'est une description très détaillée qui rend probablement tout cela plus compliqué qu'il ne l'est vraiment, mais il y a un moyen de généraliser tout ce problème et de rendre l'implémentation d'un Adapterbasé sur SortedListbeaucoup plus simple.


Généraliser le problème et simplifier l'adaptateur

Dans cette section, je ne vais pas entrer dans les détails - en partie parce que je me heurte à la limite de caractères pour les réponses sur Stack Overflow mais aussi parce que la plupart d'entre eux ont déjà été expliqués ci-dessus - mais pour résumer les changements: nous pouvons implémenter une Adapterclasse de base qui prend déjà en charge le traitement SortedListdes modèles ainsi que la liaison aux ViewHolderinstances et fournit un moyen pratique d'implémenter un Adapterbasé sur un SortedList. Pour cela, nous devons faire deux choses:

  • Nous devons créer un ViewModel interface que toutes les classes de modèles doivent implémenter
  • Nous devons créer une ViewHoldersous - classe qui définit une bind()méthode Adapterpouvant être utilisée pour lier automatiquement des modèles.

Cela nous permet de nous concentrer uniquement sur le contenu qui est censé être affiché dans le RecyclerViewen implémentant simplement les modèles et les ViewHolderimplémentations correspondantes . En utilisant cette classe de base, nous n'avons pas à nous soucier des détails complexes du Adapteret de son SortedList.

SortedListAdapter

En raison de la limite de caractères pour les réponses sur StackOverflow, je ne peux pas passer par chaque étape de l'implémentation de cette classe de base ou même ajouter le code source complet ici, mais vous pouvez trouver le code source complet de cette classe de base - je l'ai appelé SortedListAdapter- dans ce GitHub Gist .

Pour vous simplifier la vie, j'ai publié une bibliothèque sur jCenter qui contient le SortedListAdapter! Si vous souhaitez l'utiliser, il vous suffit d'ajouter cette dépendance au fichier build.gradle de votre application:

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

Vous pouvez trouver plus d'informations sur cette bibliothèque sur la page d'accueil de la bibliothèque .

Utilisation de SortedListAdapter

Pour utiliser le, SortedListAdapternous devons apporter deux modifications:

  • Modifiez le ViewHolderpour qu'il s'étende SortedListAdapter.ViewHolder. Le paramètre type doit être le modèle qui doit y être lié ViewHolder- dans ce cas ExampleModel. Vous devez lier des données à vos modèles au performBind()lieu 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);
        }
    }
  • Assurez-vous que tous vos modèles implémentent l' ViewModelinterface:

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

Après cela, il nous suffit de mettre à jour le ExampleAdapterpour étendre SortedListAdapteret supprimer tout ce dont nous n'avons plus besoin. Le paramètre type doit être le type de modèle avec lequel vous travaillez - dans ce cas ExampleModel. Mais si vous travaillez avec différents types de modèles, définissez le paramètre type sur 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);
    }
}

Après cela, nous avons terminé! Cependant une dernière chose à mentionner: le SortedListAdaptern'a pas le même add(), remove()ni les replaceAll()méthodes que notre original ExampleAdapteravait. Il utilise un Editorobjet séparé pour modifier les éléments de la liste auxquels il est possible d'accéder via la edit()méthode. Donc, si vous souhaitez supprimer ou ajouter des éléments que vous devez appeler, edit()ajoutez et supprimez les éléments sur cette Editorinstance et une fois que vous avez terminé, appelez- commit()le pour appliquer les modifications à SortedList:

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

Toutes les modifications que vous apportez de cette façon sont regroupées pour augmenter les performances. La replaceAll()méthode que nous avons implémentée dans les chapitres ci-dessus est également présente sur cet Editorobjet:

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

Si vous oubliez d'appeler, commit()aucune de vos modifications ne sera appliquée!


4
@TiagoOliveira Eh bien, il est fait pour fonctionner tout seul: D La liaison de données est un obstacle pour les personnes qui ne la connaissent pas, mais je l'ai quand même incluse car elle est incroyable et je veux la promouvoir. Pour une raison quelconque, peu de gens semblent le savoir ...
Xaver Kapeller

78
Je n'ai pas encore lu toute la réponse, j'ai dû mettre ma lecture en pause quelque part à moitié pour écrire ce commentaire - c'est l'une des meilleures réponses que j'ai trouvées ici sur le SO! Merci!
daneejela

16
J'adore la façon dont vous êtes: "Il ne ressort pas clairement de votre question avec quoi vous avez des problèmes, alors voici un exemple complet que je viens de faire": D
Fred

7
+1 juste pour nous montrer que la liaison de données existe dans Android! Je n'en ai jamais entendu parler et semble que je vais commencer à l'utiliser. Merci
Jorge Casariego

6
Cette solution est ridiculement longue et généralement sur-conçue. Optez pour le deuxième.
Enrico Casini

194

Tout ce que vous devez faire est d'ajouter une filterméthode dans 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 est initialisé dans le constructeur de l'adaptateur comme itemsCopy.addAll(items) .

Si vous le faites, appelez simplement filterdeOnQueryTextListener :

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

C'est un exemple de filtrage de mon répertoire par nom et numéro de téléphone.


11
Je pense que cela devrait être la réponse acceptée. C'est plus simple et ça fonctionne
Jose_GD

6
Simple et efficace!
AlxDroidDev

11
Notez que vous perdez l'animation si vous suivez cette approche au lieu de la réponse @Xaver Kapeller.
humazed le

23
N'a pas essayé la réponse acceptée car elle est beaucoup trop longue. Cette réponse fonctionne et est facile à mettre en œuvre. N'oubliez pas d'ajouter "app: actionViewClass =" android.support.v7.widget.SearchView "sur votre élément de menu XML.
SajithK

3
Quels sont exactement les articles et les articlesCopiez ici?
Lucky_girl

82

En suivant @Shruthi Kamoji d'une manière plus propre, nous pouvons simplement utiliser un filtrable, son destiné à cela:

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

Le E ici est un type générique, vous pouvez l'étendre en utilisant votre classe:

public class customerAdapter extends GenericRecycleAdapter<CustomerModel>

Ou changez simplement l'E pour le type que vous voulez ( <CustomerModel>par exemple)

Puis à partir de searchView (le widget que vous pouvez mettre sur 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;
    }
});

J'utilise quelque chose comme ça! Fonctionne bien et échantillon générique!
Mateus

Bonjour, qui peut m'aider étape par étape avec celui-ci: stackoverflow.com/questions/40754174/…
Thorvald Olavsen

La réponse la plus propre!
adalpari

4
C'est beaucoup mieux que la réponse votée car l'opération est effectuée sur un thread de travail dans la méthode performFiltering.
Hmmm

1
Mais vous affectez une référence à la même liste à différentes variables. Par exemple, this.originalList = list; Vous devez utiliser addAll à la place ou passer la liste dans le constructeur ArrayList
Florian Walther

5

créez simplement deux listes dans l'adaptateur un original et un temp et implémente 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();
            }
        };
    }

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

Belle solution. J'ai créé deux listes et utilisé une méthode de filtrage simple. Je n'arrive pas à passer la position d'adaptateur correcte pour un élément à la prochaine activité. J'apprécierais toutes les pensées ou idées que vous pourriez suggérer pour celui-ci: stackoverflow.com/questions/46027110/…
AJW

4

Dans l'adaptateur:

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

En activité:

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

Avec les composants d'architecture Android grâce à l'utilisation de LiveData, cela peut être facilement implémenté avec tout type d' adaptateur . Vous devez simplement effectuer les étapes suivantes:

1. Configurez vos données pour revenir de la base de données de salle en tant que LiveData comme dans l'exemple ci-dessous:

@Dao
public interface CustomDAO{

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

2. Créez un objet ViewModel pour mettre à jour vos données en direct grâce à une méthode qui connectera votre DAO et votre interface utilisateur

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. Appelez vos données à partir du ViewModel à la volée en passant la requête via onQueryTextListener comme ci-dessous:

À l'intérieur, onCreateOptionsMenudéfinissez votre auditeur comme suit

searchView.setOnQueryTextListener(onQueryTextListener);

Configurez votre écouteur de requête quelque part dans votre classe SearchActivity comme suit

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

Remarque : les étapes (1.) et (2.) sont une implémentation AAC ViewModel et DAO standard , la seule véritable "magie" en cours ici est dans OnQueryTextListener qui mettra à jour les résultats de votre liste dynamiquement à mesure que le texte de la requête change.

Si vous avez besoin de plus de précisions à ce sujet, n'hésitez pas à demander. J'espère que cela a aidé :).


1

C'est mon point de vue sur l'expansion de la réponse @klimat pour ne pas perdre l'animation de filtrage.

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

Fondamentalement, il recherche une liste complète et ajoute / supprime des éléments à une liste filtrée un par un.


0

Je recommande de modifier la solution de @Xaver Kapeller avec 2 choses ci-dessous pour éviter un problème après avoir effacé le texte recherché (le filtre ne fonctionnait plus) car la liste de l'adaptateur a une taille plus petite que la liste des filtres et l'exception IndexOutOfBoundsException s'est produite. Le code doit donc être modifié comme ci-dessous

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

Et modifiez également dans la fonctionnalité 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); 
    }
}

J'espère que cela pourrait vous aider!


Ce n'est pas du tout nécessaire.
Xaver Kapeller

Pour une réponse originale si vous ne faites pas cela, l'IndexOutOfBoundsException se produira, alors pourquoi n'est-ce pas nécessaire ???? Voulez-vous un journal? @XaverKapeller
toidv

Non, l'exception ne se produira que si vous implémentez Adapterla mauvaise façon. Sans voir votre code, je suppose que le problème le plus probable est que vous ne passez pas une copie de la liste avec tous les éléments au Adapter.
Xaver Kapeller

Le journal des erreurs: W / System.err: java.lang.IndexOutOfBoundsException: index 36 non valide, la taille est de 35 W / System.err: à java.util.ArrayList.throwIndexOutOfBoundsException (ArrayList.java:255) W / System.err: sur java.util.ArrayList.add (ArrayList.java:147) W / System.err: sur com.quomodo.inploi.ui.adapter.MultipleSelectFilterAdapter.addItem (MultipleSelectFilterAdapter.java:125) W / System.err: sur com .quomodo.inploi.ui.adapter.MultipleSelectFilterAdapter.applyAndAnimateAdditions (MultipleSelectFilterAdapter.java:78)
toidv

Veuillez aider à vérifier le code source ci-dessous @XaverKapeller gist.github.com/toidv/fe71dc45169e4138271b52fdb29420c5
toidv

0

Vue d'ensemble du recyclage avec recherche et écoute des clics

Ajoutez une interface dans votre adaptateur.

public interface SelectedUser{

    void selectedUser(UserModel userModel);

}

implémentez l'interface dans votre activité principale et remplacez la méthode. @Override public void selectedUser (UserModel userModel) {

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



}

Tutoriel complet et code source: Recyclerview avec searchview et onclicklistener


-1

J'ai résolu le même problème en utilisant le lien avec quelques modifications. Filtre de recherche sur RecyclerView avec cartes. Est-ce même possible? (J'espère que cela t'aides).

Voici ma classe d'adaptateur

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 filtre

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 d'activité

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

Dans la méthode OnQueryTextChangeListener (), utilisez votre adaptateur. Je l'ai moulé en fragments car mon adpter est en fragments. Vous pouvez utiliser l'adaptateur directement s'il est dans votre classe d'activité.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.