Введение
Поскольку из вашего вопроса не совсем понятно, с чем именно у вас возникают проблемы, я написал краткое руководство о том, как реализовать эту функцию; если у вас все еще есть вопросы, не стесняйтесь спрашивать.
У меня есть рабочий пример всего, о чем я говорю здесь, в этом репозитории GitHub .
Если вы хотите узнать больше о примере проекта, посетите домашнюю страницу проекта .
В любом случае результат должен выглядеть примерно так:

Если вы сначала хотите поиграть с демо-приложением, вы можете установить его из Play Store:

В любом случае, давайте начнем.
Настройка 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как a Comparator. Но вместо сортировки 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с, 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! Кроме того, реализация этих методов довольно проста, за одним исключением: метод remove, который удаляет 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();
}
}
Единственное, чего не хватает сейчас - это реализовать фильтрацию!
Реализация логики фильтра
Чтобы реализовать логику фильтра, мы должны сначала определить 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.
И это все! Приведенный выше код будет работать на уровне API 7 и выше, а начиная с уровня 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и удалить все , что нам не нужно больше. Параметр type должен соответствовать типу модели, с которой вы работаете - в данном случае 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()то ни одно из ваших изменений не будет применено!