Як відфільтрувати RecyclerView за допомогою SearchView


319

Я намагаюся реалізувати SearchViewз бібліотеки підтримки. Я хочу, щоб користувач використовував функцію SearchViewфільтрації Listфільмів у програмі RecyclerView.

Я дотримувався декількох підручників до цих пір, і я додав SearchViewїх ActionBar, але я не дуже впевнений, куди йти звідси. Я бачив кілька прикладів, але жоден з них не показує результатів, коли ви починаєте вводити текст.

Це моє 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);
    }
}

І це моє 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);
        }
    }
}

Відповіді:


913

Вступ

Оскільки з вашого питання не зовсім зрозуміло, з чим саме у вас виникають проблеми, я написав цей короткий посібник про те, як реалізувати цю функцію; якщо у вас все ще є запитання, не соромтеся задавати питання.

У мене є робочий приклад того, про що я говорю тут, у цьому сховищі GitHub .
Якщо ви хочете дізнатися більше про приклад проекту, відвідайте домашню сторінку проекту .

У будь-якому випадку результат повинен виглядати приблизно так:

демо-зображення

Якщо ви спершу хочете пограти з демонстраційним додатком, ви можете встановити його з Play Store:

Отримайте це в Google Play

Все одно давайте почнемо.


Налаштування SearchView

У папці res/menuстворіть новий файл під назвою main_menu.xml. У неї додати елемент і встановити actionViewClassв android.support.v7.widget.SearchView. Оскільки ви використовуєте бібліотеку підтримки, вам потрібно використовувати простір імен бібліотеки підтримки, щоб встановити actionViewClassатрибут. Ваш XML-файл повинен виглядати приблизно так:

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

У своєму Fragmentабо Activityвам доведеться надути це меню xml як звичайне, тоді ви можете шукати, MenuItemщо містить SearchViewі реалізувати те, OnQueryTextListenerщо ми збираємось використати для прослуховування змін у тексті, введеному в 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;
}

І тепер SearchViewготовий до використання. Ми будемо реалізовувати логіку фільтра згодом, onQueryTextChange()як тільки ми закінчимо реалізацію Adapter.


Налаштування Adapter

Перш за все це клас моделей, який я збираюся використовувати для цього прикладу:

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

Це просто ваша основна модель, яка відобразить текст у полі RecyclerView. Це макет, який я буду використовувати для відображення тексту:

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

Як ви бачите, я використовую прив'язку даних. Якщо ви ніколи не працювали з прив'язкою даних раніше, не перешкоджайте! Це дуже просто і потужно, проте я не можу пояснити, як це працює в рамках цієї відповіді.

Це ViewHolderдля 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);
    }
}

Знову нічого особливого. Він просто використовує прив'язку даних для прив'язки класу моделі до цього макета, як ми визначили в макеті xml вище.

Тепер ми можемо нарешті підійти до дійсно цікавої частини: Написання адаптера. Я збираюся пропустити основну реалізацію Adapterі замість цього зосереджуся на частинах, які мають значення для цієї відповіді.

Але спочатку є одне, про що ми повинні поговорити: SortedListКлас.


SortedList

Це SortedListабсолютно дивовижний інструмент, який є частиною RecyclerViewбібліотеки. Він піклується про сповіщення Adapterпро зміни в наборі даних і робить це дуже ефективним способом. Єдине, що вам потрібно зробити - це вказати порядок елементів. Вам потрібно зробити це, застосувавши compare()метод, який порівнює два елементи SortedListпросто так, як Comparator. Але замість сортування a Listвикористовується для сортування елементів у RecyclerView!

У SortedListвзаємодіє з Adapterчерез Callbackклас , який ви повинні реалізувати:

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

У способах у верхній частині зворотного виклику , як onMoved, onInsertedі т.д. Ви повинні викликати еквівалент повідомить вами спосіб Adapter. Три способи внизу compare, areContentsTheSameі areItemsTheSameви повинні реалізувати відповідно до того, який тип об’єктів ви хочете відображати та в якому порядку ці об'єкти повинні з’являтися на екрані.

Давайте переглянемо ці методи по черзі:

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

Це compare()метод, про який я говорив раніше. У цьому прикладі я просто передаю дзвінок на те, Comparatorщо порівнює дві моделі. Якщо ви хочете, щоб елементи відображалися в алфавітному порядку на екрані. Цей порівняльник може виглядати приблизно так:

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

Тепер розглянемо наступний метод:

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

Мета цього методу - визначити, чи змінився вміст моделі. В SortedListвикористовує це , щоб визначити , чи потребує викликатися подія зміни - іншими словами , якщо RecyclerViewнеобхідно кроссфейд стару і нову версію. Якщо у модельних класах є правильна equals()та hashCode()реалізація, зазвичай ви можете просто реалізувати її, як вище. Якщо ми додамо equals()та додаток hashCode()до ExampleModelкласу, він повинен виглядати приблизно так:

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

Швидка зауваження: у більшості IDE, таких як Android Studio, IntelliJ та Eclipse, є функціонал для створення equals()та hashCode()реалізації для вас одним натисненням кнопки! Тож вам не доведеться їх реалізовувати самостійно. Подивіться в Інтернеті, як це працює у вашому IDE!

Тепер давайте розглянемо останній метод:

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

Цей SortedListметод використовує для перевірки, чи два елементи відносяться до однієї речі. Найпростіше кажучи (не пояснюючи, як це SortedListпрацює), це використовується для визначення того, чи об’єкт уже міститься в програмі, Listі чи потрібно відтворювати анімацію додавання, переміщення або зміни. Якщо у ваших моделей є ідентифікатор, ви зазвичай порівнюєте лише ідентифікатор у цьому методі. Якщо вони не потребують іншого способу, щоб перевірити це, але ви все-таки реалізуєте це, залежить від конкретного додатка. Зазвичай це найпростіший варіант, щоб надати всім моделям ідентифікатор - який, наприклад, може бути полем первинного ключа, якщо ви запитуєте дані з бази даних.

При SortedList.Callbackправильному виконанні ми можемо створити екземпляр SortedList:

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

В якості першого параметра в конструкторі SortedListвам потрібно передати клас ваших моделей. Інший параметр - це лише SortedList.Callbackвизначений нами вище.

Тепер приступимо до справи: Якщо ми реалізуємо Adapterз a, SortedListце має виглядати приблизно так:

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

ComparatorВикористовується для сортування пункт передається через конструктор , тому ми можемо використовувати один і той же , Adapterнавіть якщо елементи повинні відображатися в іншому порядку.

Зараз ми майже готові! Але для початку нам потрібен спосіб додавання або видалення елементів до Adapter. З цією метою ми можемо додати методи, до Adapterяких можна додавати та видаляти елементи до 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();
}

Нам тут не потрібно викликати будь-які способи сповіщення, оскільки це SortedListвже робиться через SortedList.Callback! Крім цього, реалізація цих методів є досить прямою вперед за одним винятком: метод видалення, який видаляє Listмодель. Оскільки SortedListіснує лише один метод видалення, який може видалити один об'єкт, нам потрібно перевести список і видалити моделі по черзі. Виклик beginBatchedUpdates()на початку містить усі зміни, які ми збираємось внести SortedListразом, і покращує продуктивність. Коли ми називаємо повідомляється про всі зміни відразу.endBatchedUpdates()RecyclerView

Крім того, ви повинні розуміти, що якщо ви додасте об'єкт до SortedListі він вже є, SortedListвін більше не буде доданий. Натомість SortedListвикористовує areContentsTheSame()метод, щоб з'ясувати, чи змінився об'єкт - і чи є у нього елемент у RecyclerViewзаповіті буде оновлено.

У будь-якому випадку, як правило, я вважаю за краще один метод, який дозволяє мені замінити всі елементи в RecyclerViewодразу. Видаліть усе, чого немає, Listі додайте всі елементи, які відсутні у 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();
}

Цей метод знову збирає всі оновлення разом для підвищення продуктивності. Перший цикл знаходиться в зворотному порядку, оскільки вилучення елемента на початку може зіпсувати індекси всіх елементів, які з’являються після нього, і це може призвести в деяких випадках до проблем, таких як невідповідності даних. Після цього ми просто додаємо Listдо SortedListвикористовуваного, addAll()щоб додати всі елементи, які вже не перебувають у - SortedListі, як я описав вище, - оновити всі елементи, які вже є, SortedListале змінилися.

І цим Adapterє повне. Вся справа повинна виглядати приблизно так:

public class ExampleAdapter extends RecyclerView.Adapter<ExampleViewHolder> {

    private final SortedList<ExampleModel> mSortedList = new SortedList<>(ExampleModel.class, new SortedList.Callback<ExampleModel>() {
        @Override
        public int compare(ExampleModel a, ExampleModel b) {
            return mComparator.compare(a, b);
        }

        @Override
        public void onInserted(int position, int count) {
            notifyItemRangeInserted(position, count);
        }

        @Override
        public void onRemoved(int position, int count) {
            notifyItemRangeRemoved(position, count);
        }

        @Override
        public void onMoved(int fromPosition, int toPosition) {
            notifyItemMoved(fromPosition, toPosition);
        }

        @Override
        public void onChanged(int position, int count) {
            notifyItemRangeChanged(position, count);
        }

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

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

    private final Comparator<ExampleModel> mComparator;
    private final LayoutInflater mInflater;

    public ExampleAdapter(Context context, Comparator<ExampleModel> comparator) {
        mInflater = LayoutInflater.from(context);
        mComparator = comparator;
    }

    @Override
    public ExampleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        final ItemExampleBinding binding = ItemExampleBinding.inflate(mInflater, parent, false);
        return new ExampleViewHolder(binding);
    }

    @Override
    public void onBindViewHolder(ExampleViewHolder holder, int position) {
        final ExampleModel model = mSortedList.get(position);
        holder.bind(model);
    }

    public void add(ExampleModel model) {
        mSortedList.add(model);
    }

    public void remove(ExampleModel model) {
        mSortedList.remove(model);
    }

    public void add(List<ExampleModel> models) {
        mSortedList.addAll(models);
    }

    public void remove(List<ExampleModel> models) {
        mSortedList.beginBatchedUpdates();
        for (ExampleModel model : models) {
            mSortedList.remove(model);
        }
        mSortedList.endBatchedUpdates();
    }

    public void replaceAll(List<ExampleModel> models) {
        mSortedList.beginBatchedUpdates();
        for (int i = mSortedList.size() - 1; i >= 0; i--) {
            final ExampleModel model = mSortedList.get(i);
            if (!models.contains(model)) {
                mSortedList.remove(model);
            }
        }
        mSortedList.addAll(models);
        mSortedList.endBatchedUpdates();
    }

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

Єдине, чого зараз бракує, це здійснити фільтрацію!


Реалізація логіки фільтра

Для реалізації логіки фільтру спершу треба визначити a Listз усіх можливих моделей. Для цього прикладу я створюю Listз ExampleModelпримірників з масиву фільмів:

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

Нічого особливого не відбувається тут, ми просто створимо його Adapterі встановимо його RecyclerView. Після цього ми створюємо Listмоделі з імен фільмів у MOVIESмасиві. Потім ми додаємо всі моделі до SortedList.

Тепер ми можемо повернутися до того, onQueryTextChange()що ми визначили раніше, і розпочати реалізацію логіки фільтра:

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

Це знову досить прямо вперед. Ми називаємо метод filter()і передати в Listз ExampleModelх, а також в рядку запиту. Потім ми викликаємо replaceAll()на Adapterі передати в відфільтрованої Listповертається filter(). Ми також повинні викликати scrollToPosition(0)на RecyclerViewдля того, щоб користувач завжди може побачити всі деталі при пошуку чогось - то. Інакше RecyclerViewпід час фільтрування ви можете залишитися в положенні прокрутки вниз і згодом приховати кілька елементів. Прокручування до верху забезпечує кращу роботу користувачів під час пошуку.

Єдине, що залишилося зараз зробити - це реалізувати 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;
}

Перше, що ми робимо тут - це дзвінок toLowerCase()у рядку запиту. Ми не хочемо, щоб наша пошукова функція була чутливою до регістру, і, зателефонувавши toLowerCase()до всіх порівняних рядків, ми можемо забезпечити повернення однакових результатів незалежно від випадку. Потім він просто повторює всі моделі в Listми передали в нього і перевіряє, чи рядок запиту міститься в тексті моделі. Якщо це так, модель додається до відфільтрованого List.

І це все! Наведений вище код працюватиме на рівні 7 і вище API, і, починаючи з рівня 11 API, ви отримуєте анімацію предмета безкоштовно!

Я розумію, що це дуже детальний опис, який, ймовірно, робить всю цю справу більш складною, ніж є насправді, але є спосіб, як ми можемо узагальнити всю цю проблему та зробити реалізацію на Adapterоснові SortedListнабагато простішою.


Узагальнення проблеми та спрощення адаптера

У цьому розділі я не збираюся детально описуватись - частково тому, що я переживаю обмеження символів для відповідей на переповнення стека, а також тому, що більшість із них уже пояснено вище - але підсумовую зміни: Ми можемо реалізувати базовий Adapterклас яка вже піклується про поводження з SortedListмоделями, а також прив'язування до ViewHolderпримірників і забезпечує зручний спосіб реалізації Adapterоснови на основі SortedList. Для цього ми повинні зробити дві речі:

  • Нам потрібно створити ViewModelінтерфейс, який мають реалізувати всі класи моделей
  • Нам потрібно створити ViewHolderпідклас, який визначає bind()метод, який Adapterможна використовувати для автоматичного прив’язування моделей.

Це дозволяє нам просто зосередитись на вмісті, який повинен бути відображений у RecyclerViewпростому впровадженні моделей, а також відповідних ViewHolderреалізаціях. Використовуючи цей базовий клас, нам не потрібно турбуватися про складні деталі Adapterта його SortedList.

SortedListAdapter

Через обмеження символів для відповідей на StackOverflow я не можу пройти кожен етап реалізації цього базового класу або навіть додати тут повний вихідний код, але ви можете знайти повний вихідний код цього базового класу - я його назвав SortedListAdapter- у цьому GitHub Gist .

Щоб зробити ваше життя простим, я опублікував бібліотеку на jCenter, яка містить SortedListAdapter! Якщо ви хочете використовувати його, то все, що вам потрібно зробити, це додати цю залежність до файлу build.gradle програми:

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

Ви можете знайти більше інформації про цю бібліотеку на домашній сторінці бібліотеки .

Використання SortedListAdapter

Для використання SortedListAdapterми повинні внести дві зміни:

  • Змініть ViewHolderтак, щоб він розширився SortedListAdapter.ViewHolder. Параметр типу повинен бути моделлю, яка повинна бути пов'язана з цим ViewHolder- в цьому випадку ExampleModel. Ви повинні прив’язувати дані до своїх моделей performBind()замість 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);
        }
    }
  • Переконайтесь, що всі ваші моделі реалізують ViewModelінтерфейс:

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

Після цього нам просто потрібно оновити ExampleAdapterрозширення SortedListAdapterта видалити все, що більше не потрібно. Параметр типу повинен бути типом моделі, з якою ви працюєте - в цьому випадку ExampleModel. Але якщо ви працюєте з моделями різних типів, тоді встановіть параметр типу 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);
    }
}

Після цього ми робимо! Однак одне останнє, що слід зазначити: У них SortedListAdapterнемає тих же методів add(), remove()чи у replaceAll()наших оригіналів ExampleAdapter. Він використовує окремий Editorоб'єкт для зміни елементів у списку, до яких можна отримати доступ edit()методом. Отже, якщо ви хочете видалити або додати елементи, вам потрібно зателефонувати, edit()потім додайте та видаліть елементи цього Editorпримірника, і як тільки ви це зробите, зателефонуйте commit()йому, щоб застосувати зміни до SortedList:

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

Усі внесені вами зміни збираються разом для підвищення продуктивності. replaceAll()Метод ми реалізували в попередніх розділах також присутній на цьому Editorоб'єкті:

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

Якщо ви забудете зателефонувати, commit()то жодна з ваших змін не буде застосована!


4
@TiagoOliveira Добре, що це просто працювати з поля: D Прив'язка даних є перешкодою для незнайомих людей, але я все одно включив їх, тому що це дивовижно, і я хочу просувати його. Чомусь не так багато людей, здається, знають про це ...
Xaver Kapeller

78
Я ще не прочитав усієї відповіді, мені довелося призупинити читання десь на половині, щоб написати цей коментар - це одна з найкращих відповідей, яку я маю знайти тут, на SO! Дякую!
daneejela

16
Мені просто подобається, як ти схожий: "З вашого запитання не зрозуміло, з чим у вас виникають проблеми, тож ось повноцінний приклад, який я щойно зробив": D
Фред

7
+1, щоб показати нам, що прив'язка даних існує в Android! Я ніколи про це не чув і, здається, я почну його використовувати. Спасибі
Хорхе Касаріего

6
Це рішення смішно довге і взагалі кажучи надмірно розроблене. Перейдіть на другий.
Енріко Казіні

194

Все, що вам потрібно зробити - це додати filterметод у 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ініціалізується у конструкторі адаптера, як itemsCopy.addAll(items).

Якщо ви це зробите, просто зателефонуйте filterз 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;
    }
});

Це приклад фільтрації моєї телефонної книги за іменем та номером телефону.


11
Я думаю, це має бути прийнятою відповіддю. Це простіше, і це просто працює
Jose_GD

6
Просто та ефективно!
AlxDroidDev

11
Зауважте, що ви втрачаєте анімацію, якщо дотримуєтесь цього підходу замість @Xaver Kapeller.
гумав

23
Не спробував прийняти відповідь, оскільки це занадто тривалий час. Ця відповідь працює і просто реалізується. Не забудьте додати "додаток: actionViewClass =" android.support.v7.widget.SearchView "у свій пункт XML у меню.
SajithK

3
Які саме предмети та предметикопіюйте тут?
Lucky_girl

82

Слідуючи @Shruthi Kamoji більш чітким способом, ми можемо просто використовувати фільтруючий матеріал, призначений для цього:

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

E тут - це загальний тип, ви можете поширити його за допомогою свого класу:

public class customerAdapter extends GenericRecycleAdapter<CustomerModel>

Або просто змініть E на потрібний тип ( <CustomerModel>наприклад)

Потім з searchView (віджет, який ви можете помістити в 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;
    }
});

Я використовую щось подібне! Діє чудовий та загальний зразок!
Матеус

Привіт, хто може допомогти мені крок-у-крок з цієї: stackoverflow.com/questions/40754174 / ...
Торвальд Olavsen

Найчистіша відповідь!
adalpari

4
Це набагато краще, ніж відповідь, що проголосується, оскільки операція виконується на робочій нитці методом performFiltering.
Хммм,

1
Але ви призначаєте посилання на той самий Список різним змінним. Наприклад this.originalList = список; Ви повинні використовувати замість addAll або передати список у конструкторі ArrayList
Флоріан Уолтер

5

просто створити два списку в адаптері один оригінальний і один темп та реалізує 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;
    }

Приємне рішення. Я створив два списки і застосував простий метод фільтра. Я, здається, не можу передати правильне положення адаптера для елемента до наступної діяльності. Буду вдячний за будь-які думки чи ідеї, які ви можете запропонувати для цього: stackoverflow.com/questions/46027110/…
AJW

4

В адаптері:

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

У діяльності:

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

З компонентами архітектури Android за допомогою LiveData це можна легко реалізувати з будь-яким типом адаптера . Вам просто потрібно виконати наступні дії:

1. Налаштуйте ваші дані для повернення з бази даних кімнати як LiveData, як у наведеному нижче прикладі:

@Dao
public interface CustomDAO{

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

2. Створіть об’єкт ViewModel, щоб оновити дані в прямому ефірі за допомогою методу, який з'єднає ваш DAO і ваш інтерфейс користувача

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. Викликайте свої дані з ViewModel на льоту, передавши запит через onQueryTextListener, як показано нижче:

Всередині onCreateOptionsMenuвстановіть свого слухача таким чином

searchView.setOnQueryTextListener(onQueryTextListener);

Налаштуйте слухача запитів десь у вашому класі SearchActivity наступним чином

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

Примітка : кроки (1.) та (2.) - це стандартне реалізація AAC ViewModel та DAO , єдине справжнє "магія", що відбувається тут, - це OnQueryTextListener, який буде динамічно оновлювати результати списку у міру зміни тексту запиту.

Якщо вам потрібні додаткові роз'яснення з цього питання, будь ласка, не соромтеся запитати. Сподіваюся, це допомогло :).


1

Це мій погляд на розширення відповіді @klimat, щоб не втратити анімаційну фільтрацію.

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

В основному те, що він робить, - перегляд повного списку та додавання / видалення елементів до відфільтрованого списку по одному.


0

Я рекомендую змінити рішення @Xaver Kapeller з двома пунктами нижче, щоб уникнути проблеми після того, як ви очистили шуканий текст (фільтр більше не працював) через те, що спинка адаптера має менший розмір, ніж список фільтрів, і відбулося IndexOutOfBoundsException. Тому код потрібно змінити, як показано нижче

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

І змінити також функцію 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); 
    }
}

Сподіваємось, що це могло б вам допомогти!


Це зовсім не потрібно.
Xaver Kapeller

Для оригінальної відповіді, якщо ви цього не зробите, то IndexOutOfBoundsException станеться, то чому б це не потрібно ???? Хочете журнал? @XaverKapeller
toidv

Не виняток станеться лише в тому випадку, якщо ви реалізуєте Adapterнеправильний шлях. Не бачачи свого коду, я думаю, найімовірнішою проблемою є те, що ви не передаєте копію списку зі всіма елементами до Adapter.
Xaver Kapeller

Журнал помилок: W / System.err: java.lang.IndexOutOfBoundsException: Недійсний індекс 36, розмір 35 Вт / System.err: на java.util.ArrayList.throwIndexOutOfBoundsException (ArrayList.java:255) W / System.err: на java.util.ArrayList.add (ArrayList.java:147) W / System.err: на com.quomodo.inploi.ui.adapter.MultipleSelectFilterAdapter.addItem (MultipleSelectFilterAdapter.java:125) W / System.err: at com .quomodo.inploi.ui.adapter.MultipleSelectFilterAdapter.applyAndAnimateAdditions (MultipleSelectFilterAdapter.java:78)
toidv

Будь ласка, допоможіть перевірити вихідний код нижче @XaverKapeller gist.github.com/toidv/fe71dc45169e4138271b52fdb29420c5
toidv

0

Перегляд рециркуляції з переглядом пошуку та користувачем кліків

Додайте інтерфейс у свій адаптер.

public interface SelectedUser{

    void selectedUser(UserModel userModel);

}

реалізуйте інтерфейс у вашій маніактивності та замініть метод. @Override public void selectedUser (UserModel userModel) {

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



}

Повний підручник та вихідний код: Recyclerview з searchview та onclicklistener


-1

Я вирішив ту саму проблему, використовуючи посилання з деякими модифікаціями в ньому. Фільтр пошуку на RecyclerView за допомогою карт. Чи можливо це навіть? (сподіваюся, що це допомагає).

Ось мій клас адаптера

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

// Клас фільтра

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

}

// Клас діяльності

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

У методі OnQueryTextChangeListener () використовуйте адаптер. Я закинув його на фрагмент, оскільки мій адптер знаходиться в фрагменті. Ви можете використовувати адаптер безпосередньо, якщо він у вашому класі активності.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.