Введение
Поскольку из вашего вопроса не совсем понятно, с чем именно у вас возникают проблемы, я написал краткое руководство о том, как реализовать эту функцию; если у вас все еще есть вопросы, не стесняйтесь спрашивать.
У меня есть рабочий пример всего, о чем я говорю здесь, в этом репозитории 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()
то ни одно из ваших изменений не будет применено!