Как фильтровать RecyclerView с помощью SearchView

Я пытаюсь реализовать 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);
        }
    }
}

Ответ 1

Вступление

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

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

В любом случае результат должен выглядеть примерно так:

demo image

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

Get it on 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. Но вместо сортировки 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() то ни одно из ваших изменений не будет применено!

Ответ 2

Все, что вам нужно сделать, это добавить метод 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;
    }
});

Это пример фильтрации моей телефонной книги по имени и номеру телефона.

Ответ 3

Следуя @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;
    }
});

Ответ 4

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

Ответ 5

Благодаря компонентам Android Architecture благодаря использованию 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, который будет динамически обновлять результаты вашего списка по мере изменения текста запроса.

Если вам нужно больше разъяснений по этому вопросу, пожалуйста, не стесняйтесь спрашивать. Надеюсь, это помогло :).

Ответ 6

Я рекомендую изменить решение @Xaver Kapeller на 2 вещи ниже, чтобы избежать проблемы после того, как вы очистили найденный текст (фильтр больше не работал) из-за того, что список адаптеров имеет меньший размер, чем список фильтров, и Событие 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); 
    }
}

Надеюсь, что это может вам помочь!

Ответ 7

Это мой взгляд на расширение ответа @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++;
    }
}

По сути, он просматривает полный список и добавляет/удаляет элементы в отфильтрованном списке по одному.

Ответ 8

Я решил ту же проблему, используя ссылку с некоторыми изменениями в ней. Фильтр поиска на 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() используйте ваш адаптер. Я наложил его на фрагмент, так как мой аддтер во фрагменте. Вы можете использовать адаптер напрямую, если он находится в вашем классе активности.

Ответ 9

В адаптере:

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