Ответ 1
Вступление
Поскольку из вашего вопроса не совсем ясно, с чем именно у вас возникают проблемы, я написал краткое руководство о том, как реализовать эту функцию; если у вас все еще есть вопросы, не стесняйтесь спрашивать.
У меня есть рабочий пример всего, о чем я говорю здесь, в этом репозитории 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
точно так же, как 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
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()
в начале 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()
регистр, и, вызывая 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
. Но если вы работаете с моделями разных типов, установите для параметра 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);
}
}
После этого мы сделали! Однако следует упомянуть еще одну вещь: SortedListAdapter
не имеет тех же методов add()
, remove()
или replaceAll()
которые были в нашем оригинальном ExampleAdapter
. Он использует отдельный объект Editor
для изменения элементов в списке, доступ к которым можно получить с помощью метода edit()
. Поэтому, если вы хотите удалить или добавить элементы, вам нужно вызвать edit()
затем добавить и удалить элементы в этом экземпляре Editor
и как только вы закончите, вызовите commit()
для него, чтобы применить изменения к SortedList
:
mAdapter.edit()
.remove(modelToRemove)
.add(listOfModelsToAdd)
.commit();
Все изменения, которые вы делаете таким образом, объединяются для повышения производительности. Метод replaceAll()
который мы реализовали в replaceAll()
главах, также присутствует в этом объекте Editor
:
mAdapter.edit()
.replaceAll(mModels)
.commit();
Если вы забудете вызвать commit()
то ни одно из ваших изменений не будет применено!