Comment filtrer une vue RecyclerView avec une vue de recherche

j'essaie de mettre en œuvre le SearchView à partir de la bibliothèque de soutien. Je veux que l'utilisateur soit d'utiliser le "151920920 de" filtrer List de films dans un RecyclerView .

j'ai suivi quelques tutoriels jusqu'à présent et j'ai ajouté le SearchView à la ActionBar , mais je ne suis pas vraiment sûr où aller à partir d'ici. J'ai vu quelques exemples, mais aucun d'entre eux montrent des résultats que 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);
        }
    }
}
251
demandé sur Xaver Kapeller 2015-05-22 16:39:43

7 réponses

Introduction

Puisqu'il n'est pas vraiment clair de la forme de votre question avec ce que exactement vous avez des problèmes, j'ai écrit cette revue rapide sur la façon de mettre en œuvre cette fonctionnalité, si vous avez encore des questions n'hésitez pas à poser.

j'ai un exemple concret de tout ce dont je parle ici dans ce dépôt GitHub .

Si vous voulez savoir pour en savoir plus sur le projet exemple, visitez le page d'accueil du projet .

dans tous les cas le résultat devrait ressembler à quelque chose comme ceci:

demo image

si vous voulez d'abord jouer avec l'application de démonstration, vous pouvez l'installer à partir de la boutique de jeu:

Get it on Google Play

commençons.


mise en place du SearchView

dans le dossier res/menu créez un nouveau fichier appelé main_menu.xml . Dans ce ajouter un article et mettre le actionViewClass à android.support.v7.widget.SearchView . Puisque vous utilisez la bibliothèque de soutien, vous devez utiliser l'espace de noms de la bibliothèque de soutien pour définir l'attribut actionViewClass . Votre fichier xml devrait ressembler à quelque chose comme 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, alors vous pouvez chercher le MenuItem qui contient le SearchView et mettre en œuvre le OnQueryTextListener que nous allons utiliser pour écouter les changements au 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) MenuItemCompat.getActionView(searchItem);
    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 mettrons en œuvre la logique du filtre plus tard dans onQueryTextChange() une fois que nous aurons terminé mise en œuvre du Adapter .


mise en place du Adapter

D'abord et avant tout c'est la classe de 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 data binding avant ne soyez pas découragé! Il est très simple et puissant, mais je ne peux pas expliquer comment il fonctionne dans la portée 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 model à cette mise en page comme nous l'avons défini dans le xml de mise en page ci-dessus.

maintenant nous pouvons enfin venir à la partie vraiment intéressante: écrire L'Adaptateur. Je vais passer outre la mise en œuvre de base du Adapter et je vais plutôt me concentrer sur les parties qui sont pertinentes pour cette réponse.

mais il y a d'abord une chose dont nous devons parler: SortedList classe.


SortedList

le SortedList est un outil complètement étonnant qui fait partie de la bibliothèque RecyclerView . Il prend soin d'informer le Adapter des modifications apportées à l'ensemble de données et il le fait d'une 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 mettant en œuvre une méthode compare() qui compare deux éléments dans le SortedList tout comme un Comparator . Mais au lieu de trier un List il est utilisé pour trier les articles dans le RecyclerView !

le SortedList interagit avec le Adapter à travers 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 callback comme 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 vous devez mettre en œuvre selon quel type d'objets vous voulez afficher et dans quel ordre ces objets doit apparaître sur l'écran.

voyons 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 ne fais que passer l'appel à un Comparator qui compare les deux modèles. Si vous voulez que les éléments apparaissent dans l'ordre alphabétique sur 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());
    }
};

maintenant regardons 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é. Le SortedList utilise ceci pour déterminer si un événement de changement doit être invoqué - en d'autres termes si le RecyclerView doit croiser l'ancienne et la nouvelle version. Si les classes de modèles ont une implémentation correcte equals() et hashCode() , vous pouvez généralement l'implémenter comme ci-dessus. Si on ajoute un equals() et hashCode() mise en œuvre de la classe ExampleModel il devrait ressembler à quelque chose comme 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 la fonctionnalité de générer des implémentations equals() et hashCode() pour vous en appuyant sur un bouton! Si vous n'avez pas à les mettre en œuvre vous-même. Regardez sur internet Comment ça marche dans votre IDE!

voyons maintenant la dernière méthode:

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

Le SortedList utilise cette méthode pour vérifier si deux éléments font référence à la même chose. En termes simples (sans expliquer comment fonctionne le SortedList ), ceci est utilisé pour déterminer si un objet est déjà contenu dans le List et si une animation add, move ou change doit être jouée. Si vos modèles ont un id, vous comparez habituellement juste l'id dans cette méthode. Si elles ne vous besoin de trouver un autre moyen de vérifier cela, mais cependant, vous finissez par mettre en œuvre cela dépend de votre application spécifique. Habituellement, c'est l'option la plus simple pour donner à tous les Modèles un id - qui pourrait par exemple être le champ clé primaire si vous interrogez les données d'une base de données.

avec le SortedList.Callback correctement mis en œuvre, nous pouvons créer une instance du SortedList :

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

comme premier paramètre dans le constructeur du SortedList vous devez passer la classe de votre modèle. L'autre paramètre est juste le SortedList.Callback nous avons défini ci-dessus.

maintenant passons aux affaires: si nous mettons en œuvre le Adapter avec un SortedList il devrait ressembler à quelque chose comme 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'article est passé par le constructeur de sorte que nous pouvons utiliser le même Adapter même si les articles sont censés être affichés dans un ordre différent.

maintenant nous sont presque fait! Mais nous avons d'abord besoin d'un moyen d'ajouter ou de supprimer des éléments de la Adapter . À cette fin, nous pouvons ajouter des méthodes au Adapter qui nous permettent d'ajouter et de supprimer des articles au 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 des méthodes de notification ici parce que le SortedList le fait déjà à travers le SortedList.Callback ! En dehors de cela, la mise en œuvre de ces méthodes est assez simple avec une exception: la méthode supprimer qui supprime un List de modèles. Comme le SortedList n'a qu'une seule méthode de suppression qui peut supprimer un seul objet, nous devons boucler la liste et supprimer les Modèles un par un. Le fait d'appeler beginBatchedUpdates() au début permet d'effectuer tous les changements que nous allons apporter au SortedList ensemble et d'améliorer les performances. Lorsque nous appelons endBatchedUpdates() le RecyclerView est informé de tous les changements à la fois.

en outre, ce que vous devez comprendre est que si vous ajoutez un objet au SortedList et qu'il est déjà dans le SortedList , il ne sera plus ajouté. Au lieu de cela, le SortedList utilise la méthode areContentsTheSame() pour déterminer si l'objet a changé - et s'il a l'article dans le RecyclerView sera mis à jour.

quoi qu'il en soit, ce que je préfère habituellement est une méthode qui me permet de remplacer tous les articles dans le RecyclerView à la fois. Supprimer tout ce qui n'est pas dans le List et d'ajouter tous les éléments qui sont manquant 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 de nouveau toutes les mises à jour pour augmenter la performance. La première boucle est inversée car supprimer un élément au début de la boucle perturberait les index de tous les éléments qui surgissent après lui et cela peut conduire dans certains cas à des problèmes comme des incohérences de données. Après cela, nous ajoutons simplement le List au SortedList en utilisant addAll() pour ajouter tous les articles qui ne sont pas déjà dans le SortedList et - comme je l'ai décrit ci-dessus-mettre à jour tous les éléments qui sont déjà dans le SortedList mais ont changé.

et que le Adapter est complet. Le tout devrait ressembler à quelque chose comme 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!


mise en œuvre de la logique du filtre

pour mettre en œuvre la logique de filtre nous tout d'abord définir un List de tous les modèles possibles. Pour cet exemple je crée un List de ExampleModel instances à 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);
}

il ne se passe rien de spécial ici, nous instancions juste le Adapter et le plaçons sur le RecyclerView . Après cela, nous créons un List de modèles à partir des noms de films dans le tableau MOVIES . Ensuite, nous ajoutons tous les modèles au SortedList .

maintenant nous peut revenir à onQueryTextChange() que nous avons défini plus tôt et commencer à mettre en œuvre la logique de filtre:

@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 passer dans le List de ExampleModel s ainsi que la chaîne de requête. Nous appelons ensuite replaceAll() sur le Adapter et passer dans le filtré List retourné par filter() . Nous avons aussi appeler scrollToPosition(0) sur le RecyclerView pour s'assurer que l'utilisateur peut toujours voir tous les éléments lors de la recherche de quelque chose. Dans le cas contraire, le RecyclerView pourrait rester dans une position défilante tout en filtrant et ensuite cacher quelques articles. Défilement vers le haut assure une meilleure expérience utilisateur lors de la recherche.

la seule chose à faire maintenant est de mettre en œuvre 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 de notre fonction de recherche pour être sensible à la casse et en appelant toLowerCase() sur toutes les chaînes que nous comparons, nous pouvons nous assurer que nous retournons les mêmes résultats quel que soit le cas. Puis parcourt tous les modèles de la List nous avons passé en elle 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 sera exécuté au niveau 7 de L'API et au-dessus et à partir du niveau 11 de L'API vous obtiendrez des animations gratuites!

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 réellement, mais il y a un moyen de généraliser tout ce problème et de rendre la mise en œuvre d'un Adapter basé sur un SortedList beaucoup plus simple.


généraliser le problème et simplifier L'Adaptateur

dans cette section Je ne suis pas je vais entrer dans beaucoup de détails - en partie parce que je suis en train de courir contre la limite de caractères pour les réponses sur le débordement de la pile mais aussi parce que la plupart de cela a déjà expliqué ci - dessus-mais pour résumer les changements: nous pouvons implémenter une classe de base Adapter qui s'occupe déjà du traitement des SortedList ainsi que des modèles de liaison aux instances ViewHolder et fournit un moyen pratique pour 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èles doivent implémenter
  • nous devons créer une sous-classe ViewHolder qui définit une méthode bind() que le 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 le RecyclerView en implémentant simplement les modèles et là mise en œuvre ViewHolder correspondante. En utilisant cette classe de base, nous n'avons pas à nous soucier des détails complexes du Adapter et 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 la mise en œuvre 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 - en ce Github Gist .

pour vous simplifier la vie, j'ai publié une bibliothèque sur jCenter qui contient le SortedListAdapter ! Si vous voulez l'utiliser, alors tout ce que vous devez faire est d'ajouter cette dépendance à votre application construire.gradle fichier:

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

vous pouvez trouver plus d'information sur cette bibliothèque sur la page d'accueil de la bibliothèque .

en utilisant le désembueur

pour utiliser le SortedListAdapter nous devons faire deux changements:

  • changer le ViewHolder de sorte qu'il s'étend SortedListAdapter.ViewHolder . Le paramètre type doit être le modèle qui doit ê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 avons juste à mettre à jour le ExampleAdapter pour étendre SortedListAdapter et enlever 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. à 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 sommes finis! Toutefois, une dernière chose à mentionner: le SortedListAdapter n'a pas les mêmes add() , remove() ou replaceAll() méthodes notre ExampleAdapter original avait. Il utilise un objet séparé Editor pour modifier les éléments de la liste auxquels on peut accéder par la méthode edit() . Donc, si vous voulez supprimer ou ajouter des éléments vous devez appeler edit() puis Ajouter et supprimer les éléments sur cette instance Editor et une fois que vous avez terminé, appelez commit() sur elle pour appliquer les changements au SortedList :

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

tous les changements que vous faites de cette façon sont assemblés pour augmenter la performance. La méthode replaceAll() que nous avons mise en œuvre dans les chapitres ci-dessus est également présente sur cet Editor objet:

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

si vous oubliez d'appeler commit() alors aucun de vos changements ne sera appliqué!

794
répondu Xaver Kapeller 2016-09-01 19:48:13

Tout ce que vous devez faire est d'ajouter filter méthode 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 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.

143
répondu klimat 2016-09-15 22:17:49

suivant @Shruthi Kamoji d'une manière plus propre, nous pouvons juste utiliser un filtrable, c'est fait 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 A ici est un Type générique, vous pouvez l'étendre en utilisant votre classe:

public class customerAdapter extends GenericRecycleAdapter<CustomerModel>

ou tout simplement changer le E au type que vous voulez ( <CustomerModel> par exemple)

puis à partir de searchView (le widget que vous pouvez mettre dans le 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;
    }
});
50
répondu sagits 2018-06-21 11:25:05

il suffit de créer deux listes dans l'adaptateur un origal et un temp et met en œuvre filtrable .

    @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;
    }
4
répondu Xar E Ahmer 2017-08-18 11:10:42

je recommande de modifier la solution de @Xaver Kapeller avec 2 choses ci-dessous pour éviter un problème après que vous avez effacé le texte recherché (le filtre ne fonctionnait plus) en raison de la liste arrière de l'adaptateur a plus petite taille que la liste de filtre et L'IndexOutOfBoundsException est arrivé. 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 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); 
    }
}

espérons qu'il pourrait vous aider!

0
répondu toidv 2016-08-10 21:42:08

j'ai résolu le même problème en utilisant le lien avec quelques modifications. filtre de recherche sur RecyclerView avec cartes. Est-il même possible? (espérons que cela aide).

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 Je l'ai moulé pour qu'il se fragmente car mon adpter est en fragment. Vous pouvez utiliser l'adaptateur directement si son dans votre classe d'activité.

-1
répondu Shruthi Kamoji 2015-07-09 12:41:51

Dans 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;
            }
        });
-1
répondu Firoz Ahmed 2017-08-30 15:06:13