web-dev-qa-db-fra.com

Comment filtrer un RecyclerView avec un SearchView

J'essaie d'implémenter le SearchView de la bibliothèque de support. Je veux que l'utilisateur puisse utiliser le SearchView pour filtrer un List de films dans un RecyclerView.

J'ai suivi quelques tutoriels jusqu'à présent et j'ai ajouté la SearchView à la ActionBar, mais je ne sais pas trop où aller à partir de maintenant. J'ai vu quelques exemples, mais aucun ne montre les résultats obtenus lorsque vous commencez à taper.

Ceci 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);
        }
    }
}
292
Jacques Krause

Introduction

Comme votre question ne dit pas vraiment en quoi vous avez des problèmes, j’ai écrit cette procédure pas à pas qui explique comment implémenter 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 GitHub Repository .
Si vous souhaitez en savoir plus sur le projet exemple, visitez la page d'accueil du projet .

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

demo image

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

Get it on Google Play

Quoi qu'il en soit permet de commencer.


Configurer le SearchView

Dans le dossier res/menu créez un nouveau fichier nommé main_menu.xml. Dans ce dernier, ajoutez un élément et définissez le paramètre actionViewClass sur 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'attribut actionViewClass. 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 Fragment ou Activity, vous devez gonfler ce menu xml comme d'habitude. Vous pouvez alors rechercher le MenuItem qui contient le SearchView et implémenter le OnQueryTextListener que nous allons utiliser pour écouter les modifications apportées au texte saisi 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 SearchView est prêt à être utilisé. Nous allons implémenter la logique de filtrage plus tard dans onQueryTextChange() une fois que nous aurons fini d'implémenter Adapter.


Configurer le Adapter

Tout d’abord, c’est 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 RecyclerView. Voici la mise en page 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 Data Binding. 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 le ViewHolder pour la classe ExampleModel:

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 rien de spécial. Il utilise simplement la liaison de données pour lier la classe de modèle à cette présentation, comme nous l'avons défini dans la présentation XML ci-dessus.

Nous pouvons maintenant arriver à la partie vraiment intéressante: Écrire l'adaptateur. Je vais sauter l’implémentation de base de Adapter et je vais plutôt me concentrer sur les parties pertinentes pour cette réponse.

Mais d’abord, il ya une chose dont nous devons parler: La SortedList classe.


SortedList

SortedList est un outil complètement incroyable qui fait partie de la bibliothèque RecyclerView. Il se charge d'informer le Adapter des modifications apportées à l'ensemble de données et le fait de manière très efficace. La seule chose à faire est de spécifier un ordre des éléments. Pour ce faire, vous devez implémenter une méthode compare() qui compare deux éléments de SortedList exactement comme un Comparator. Mais au lieu de trier un List, il est utilisé pour trier les éléments dans le RecyclerView!

SortedList interagit avec Adapter par le biais d'une classe Callback 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 du rappel telles que onMoved, onInserted, etc., vous devez appeler la méthode de notification équivalente de votre Adapter. Les trois méthodes en bas compare, areContentsTheSame et areItemsTheSame que vous devez implémenter en fonction du type d’objets que vous souhaitez afficher et de l’ordre dans lequel 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 méthode compare() dont j'ai parlé plus tôt. Dans cet exemple, je passe simplement l'appel à un Comparator qui compare les deux modèles. Si vous souhaitez que les éléments apparaissent dans l’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());
    }
};

Regardons 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é. SortedList l'utilise pour déterminer si un événement de changement doit être appelé - autrement dit, si RecyclerView doit effectuer un crossfade entre l'ancienne et la nouvelle version. Si vos classes de modèle ont une implémentation correcte de equals() et hashCode(), vous pouvez généralement l'implémenter comme ci-dessus. Si nous ajoutons une implémentation equals() et hashCode() à la classe ExampleModel, 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;
    }
}

Petite remarque: la plupart des IDE tels que Android Studio, IntelliJ et Eclipse ont des fonctionnalités permettant de générer les implémentations equals() et hashCode() à la simple pression d'un bouton! Donc, vous n'avez pas à les mettre en œuvre vous-même. Recherchez sur Internet comment cela fonctionne dans votre IDE!

Regardons maintenant la dernière méthode:

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

SortedList utilise cette méthode pour vérifier si deux éléments font référence à la même chose. En termes plus simples (sans expliquer le fonctionnement de SortedList), ceci est utilisé pour déterminer si un objet est déjà contenu dans List et si une animation doit être ajoutée, déplacée ou modifiée. Si vos modèles ont un identifiant, vous ne comparerez généralement que l'identifiant de cette méthode. Si ce n'est pas le cas, vous devez trouver un autre moyen de vérifier cela, mais la mise en œuvre dépend toutefois de votre application. En règle générale, l'option la plus simple consiste à attribuer un identifiant à tous les modèles. Il peut s'agir par exemple du champ de clé primaire si vous interrogez les données d'une base de données.

Avec le SortedList.Callback correctement implémenté, nous pouvons créer une instance de SortedList:

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

En tant que premier paramètre du constructeur de SortedList, vous devez transmettre la classe de vos modèles. L'autre paramètre est simplement le SortedList.Callback que nous avons défini ci-dessus.

Passons maintenant aux choses sérieuses: si nous implémentons Adapter avec un SortedList, il 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 utilisé pour trier l'élément est passé par le constructeur afin que nous puissions utiliser le même Adapter même si les éléments sont supposés être affichés dans un ordre différent.

Maintenant nous avons presque fini! Mais nous avons d’abord besoin d’un moyen d’ajouter ou de supprimer des éléments à la variable Adapter. Pour cela, nous pouvons ajouter des méthodes à Adapter permettant d'ajouter et de supprimer des éléments à 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 besoin d’appeler aucune méthode de notification ici car le SortedList le fait déjà pour via le SortedList.Callback! Mis à part cela, la mise en œuvre de ces méthodes est assez simple, à une exception près: la méthode remove qui supprime un modèle List. Étant donné que SortedList ne dispose que d’une méthode de suppression permettant de supprimer un seul objet, nous devons parcourir la liste en boucle et supprimer les modèles un par un. L'appel de beginBatchedUpdates() au début regroupe tous les changements que nous allons apporter à SortedList et améliore les performances. Lorsque nous appelons endBatchedUpdates(), RecyclerView est informé de tous les changements en même temps.

De plus, vous devez comprendre que si vous ajoutez un objet à SortedList et qu’il figure déjà dans SortedList, il ne sera pas ajouté à nouveau. Au lieu de cela, SortedList utilise la méthode areContentsTheSame() pour déterminer si l'objet a changé - et s'il contient l'élément dans RecyclerView sera mis à jour.

Quoi qu’il en soit, ce que je préfère en général est une méthode qui me permet de remplacer tous les éléments de la variable RecyclerView à la fois. Supprimez tout ce qui ne figure pas dans List et ajoutez tous les éléments manquants dans 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 améliorer les performances. La première boucle est inversée, car supprimer un élément au début gâcherait l'index de tous les éléments qui suivraient, ce qui pourrait parfois entraîner des problèmes tels que des incohérences dans les données. Après cela, nous ajoutons simplement List à SortedList en utilisant addAll() pour ajouter tous les éléments qui ne figurent pas déjà dans SortedList et - comme je l’ai décrit ci-dessus - met à jour tous les éléments qui sont déjà dans SortedList mais qui ont changé.

Et avec cela, le Adapter est terminé. 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 de mettre en œuvre le filtrage!


Implémentation de la logique de filtrage

Pour implémenter la logique de filtrage, nous devons d’abord définir un List parmi tous les modèles possibles. Pour cet exemple, je crée une instance List sur ExampleModel à partir 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 simplement Adapter et le configurons sur RecyclerView. Après cela, nous créons un List de modèles à partir des noms de films du tableau MOVIES. Ensuite, nous ajoutons tous les modèles à la variable SortedList.

Nous pouvons maintenant revenir à onQueryTextChange() que nous avons défini précédemment et commencer à mettre en œuvre 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 assez simple. Nous appelons la méthode filter() et passons le List de ExampleModels ainsi que la chaîne de requête. Nous appelons ensuite replaceAll() sur Adapter et transmettons le List filtré renvoyé par filter(). Nous devons également appeler scrollToPosition(0) sur RecyclerView pour nous assurer que l’utilisateur peut toujours voir tous les éléments lors de la recherche de quelque chose. Sinon, RecyclerView risque de rester en position de défilement pendant le filtrage et de masquer ensuite quelques éléments. Faire défiler vers le haut assure une meilleure expérience utilisateur lors de la recherche.

Il ne reste plus maintenant qu'à implémenter filter() lui-même:

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() sur la chaîne de requête. Nous ne voulons pas que notre fonction de recherche soit sensible à la casse et en appelant toLowerCase() sur toutes les chaînes comparées, nous pouvons nous assurer de renvoyer les mêmes résultats quelle que soit la casse. Il parcourt ensuite tous les modèles de la variable List dans laquelle nous avons passé 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é à la List filtrée.

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

Je me rends compte qu’il s’agit d’une description très détaillée qui rend probablement tout cela plus complexe qu’elle ne l’est réellement, mais il existe un moyen de généraliser tout ce problème et de simplifier considérablement la mise en œuvre de Adapter à partir de SortedList.


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 au débordement de pile, mais aussi parce que la plupart d’entre elles ont déjà été expliquées ci-dessus - mais pour résumer les changements: Nous pouvons implémenter une base Adapter classe qui s'occupe déjà de traiter les SortedList ainsi que les modèles de liaison aux instances ViewHolder et fournit un moyen pratique d'implémenter un Adapter basé sur un SortedList. Pour cela, nous devons faire deux choses:

  • Nous devons créer une interface ViewModel que toutes les classes de modèle doivent implémenter.
  • Nous devons créer une sous-classe ViewHolder qui définit une méthode bind() que Adapter peut utiliser pour lier des modèles automatiquement.

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

SortedListAdapter

En raison de la limite de caractères pour les réponses sur StackOverflow, je ne peux pas suivre chaque étape de l'implémentation de cette classe de base ni 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ée SortedListAdapter - dans ceci GitHub Gist .

Pour vous simplifier la vie, j'ai publié une bibliothèque sur jCenter qui contient le fichier SortedListAdapter! Si vous voulez 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 paramètre SortedListAdapter, nous devons apporter deux modifications:

  • Modifiez le paramètre ViewHolder afin qu’il soit étendu SortedListAdapter.ViewHolder. Le paramètre type devrait être le modèle qui devrait être lié à ce ViewHolder - dans ce cas, ExampleModel. Vous devez lier les données à vos modèles dans performBind() au 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'interface ViewModel:

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

Après cela, nous devons simplement mettre à jour ExampleAdapter pour étendre SortedListAdapter et supprimer tout ce dont nous n’avons plus besoin. Le paramètre type doit correspondre au type de modèle avec lequel vous travaillez - dans ce cas, ExampleModel. Toutefois, 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 fini! Cependant, une dernière chose à mentionner: le SortedListAdapter n’a pas les mêmes méthodes add(), remove() ou replaceAll() que notre ExampleAdapter avait déjà. Il utilise un objet Editor séparé pour modifier les éléments de la liste auxquels on peut accéder via la méthode edit(). Donc, si vous souhaitez supprimer ou ajouter des éléments, vous devez appeler edit(), puis ajouter et supprimer les éléments de cette instance Editor. Une fois que vous avez terminé, appelez commit() 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 méthode replaceAll() que nous avons implémentée dans les chapitres ci-dessus est également présente sur cet objet Editor:

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

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

863
Xaver Kapeller

Tout ce que vous avez à faire est d'ajouter la méthode filter 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 filter à partir de 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;
    }
});

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

170
klimat

Après @Shruthi Kamoji d’une manière plus propre, nous pouvons simplement utiliser un filtrable, c’est pour ça:

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 est un type générique, vous pouvez l’étendre en utilisant votre classe:

public class customerAdapter extends GenericRecycleAdapter<CustomerModel>

Ou changez simplement le E en le type souhaité (<CustomerModel> par exemple)

Puis depuis 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;
    }
});
71
sagits

créez simplement deux listes dans l'adaptateur, un orignal 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;
    }
5
Xar E Ahmer

Avec Composants d'architecture Android grâce à l'utilisation de LiveData cela peut être facilement mis en œuvre avec tout type de Adaptateur. Vous devez simplement suivre les étapes suivantes:

1. Configurez vos données pour qu'elles reviennent de RoomBase de données comme 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 UI

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

}

. Appelez vos données à partir du ViewModel à la volée en transmettant la requête via onQueryTextListener comme ci-dessous:

Inside onCreateOptionsMenu configurez 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);
                                }
                            });
                }
            };

Note: Les étapes (1.) et (2.) sont standard AAC ViewModel et DAO implémentation, la seule véritable "magie" allant On se trouve ici dans OnQueryTextListener, qui mettra à jour les résultats de votre liste de manière dynamique à 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é :).

2
Panos Gr

Je recommande de modifier la solution de @Xaver Kapeller avec les 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 inférieure à celle de la liste de filtrage et que l'exception IndexOutOfBoundsException s'est produite. Donc, le code doit ê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 modifier aussi 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!

0
toidv

C’est mon point de vue sur l’expansion de la réponse @klimat afin de 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 parcourt une liste complète et ajoute/supprime un à un des éléments d'une liste filtrée.

0
AhmadF

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;
            }
        });
0
Firoz Ahmed