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

319

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

До сих пор я следовал нескольким учебным пособиям и добавил SearchViewк ним ActionBar, но я не совсем уверен, куда идти дальше. Я видел несколько примеров, но ни один из них не показывает результаты, когда вы начинаете печатать.

Это мое MainActivity:

public class MainActivity extends ActionBarActivity {

    RecyclerView mRecyclerView;
    RecyclerView.LayoutManager mLayoutManager;
    RecyclerView.Adapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_recycler_view);

        mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        mRecyclerView.setHasFixedSize(true);

        mLayoutManager = new LinearLayoutManager(this);
        mRecyclerView.setLayoutManager(mLayoutManager);

        mAdapter = new CardAdapter() {
            @Override
            public Filter getFilter() {
                return null;
            }
        };
        mRecyclerView.setAdapter(mAdapter);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
        SearchView searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
        searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }
}

И это мое Adapter:

public abstract class CardAdapter extends RecyclerView.Adapter<CardAdapter.ViewHolder> implements Filterable {

    List<Movie> mItems;

    public CardAdapter() {
        super();
        mItems = new ArrayList<Movie>();
        Movie movie = new Movie();
        movie.setName("Spiderman");
        movie.setRating("92");
        mItems.add(movie);

        movie = new Movie();
        movie.setName("Doom 3");
        movie.setRating("91");
        mItems.add(movie);

        movie = new Movie();
        movie.setName("Transformers");
        movie.setRating("88");
        mItems.add(movie);

        movie = new Movie();
        movie.setName("Transformers 2");
        movie.setRating("87");
        mItems.add(movie);

        movie = new Movie();
        movie.setName("Transformers 3");
        movie.setRating("86");
        mItems.add(movie);

        movie = new Movie();
        movie.setName("Noah");
        movie.setRating("86");
        mItems.add(movie);

        movie = new Movie();
        movie.setName("Ironman");
        movie.setRating("86");
        mItems.add(movie);

        movie = new Movie();
        movie.setName("Ironman 2");
        movie.setRating("86");
        mItems.add(movie);

        movie = new Movie();
        movie.setName("Ironman 3");
        movie.setRating("86");
        mItems.add(movie);
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
        View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.recycler_view_card_item, viewGroup, false);
        return new ViewHolder(v);
    }

    @Override
    public void onBindViewHolder(ViewHolder viewHolder, int i) {
        Movie movie = mItems.get(i);
        viewHolder.tvMovie.setText(movie.getName());
        viewHolder.tvMovieRating.setText(movie.getRating());
    }

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

    class ViewHolder extends RecyclerView.ViewHolder{

        public TextView tvMovie;
        public TextView tvMovieRating;

        public ViewHolder(View itemView) {
            super(itemView);
            tvMovie = (TextView)itemView.findViewById(R.id.movieName);
            tvMovieRating = (TextView)itemView.findViewById(R.id.movieRating);
        }
    }
}
Жак Краузе
источник

Ответы:

913

Введение

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

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

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

демонстрационное изображение

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

Получить его в Google Play

В любом случае, давайте начнем.


Настройка SearchView

В папке res/menuсоздайте новый файл с именем main_menu.xml. В него добавьте элемент и установите actionViewClassдля android.support.v7.widget.SearchView. Поскольку вы используете библиотеку поддержки, вы должны использовать пространство имен библиотеки поддержки для установки actionViewClassатрибута. Ваш XML-файл должен выглядеть примерно так:

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto">

    <item android:id="@+id/action_search"
          android:title="@string/action_search"
          app:actionViewClass="android.support.v7.widget.SearchView"
          app:showAsAction="always"/>

</menu>

В вашем Fragmentили Activityвы должны наполнить это меню xml как обычно, тогда вы можете найти то, MenuItemчто содержит SearchViewи реализовать то, OnQueryTextListenerчто мы собираемся использовать, чтобы прослушать изменения в тексте, введенном в SearchView:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.menu_main, menu);

    final MenuItem searchItem = menu.findItem(R.id.action_search);
    final SearchView searchView = (SearchView) searchItem.getActionView();
    searchView.setOnQueryTextListener(this);

    return true;
}

@Override
public boolean onQueryTextChange(String query) {
    // Here is where we are going to implement the filter logic
    return false;
}

@Override
public boolean onQueryTextSubmit(String query) {
    return false;
}

И теперь SearchViewон готов к использованию. Мы внедрим логику фильтра позже, onQueryTextChange()когда закончим реализацию Adapter.


Настройка Adapter

Прежде всего, это класс модели, который я собираюсь использовать для этого примера:

public class ExampleModel {

    private final long mId;
    private final String mText;

    public ExampleModel(long id, String text) {
        mId = id;
        mText = text;
    }

    public long getId() {
        return mId;
    }

    public String getText() {
        return mText;
    }
}

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

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <variable
            name="model"
            type="com.github.wrdlbrnft.searchablerecyclerviewdemo.ui.models.ExampleModel"/>

    </data>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="?attr/selectableItemBackground"
        android:clickable="true">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="8dp"
            android:text="@{model.text}"/>

    </FrameLayout>

</layout>

Как видите, я использую привязку данных. Если вы никогда не работали с привязкой данных, не расстраивайтесь! Это очень просто и мощно, однако я не могу объяснить, как это работает в рамках этого ответа.

Это ViewHolderдля ExampleModelкласса:

public class ExampleViewHolder extends RecyclerView.ViewHolder {

    private final ItemExampleBinding mBinding;

    public ExampleViewHolder(ItemExampleBinding binding) {
        super(binding.getRoot());
        mBinding = binding;
    }

    public void bind(ExampleModel item) {
        mBinding.setModel(item);
    }
}

Опять ничего особенного. Он просто использует привязку данных для привязки класса модели к этому макету, как мы определили в макете XML выше.

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

Но сначала нам нужно поговорить об одном: о SortedListклассе.


SortedList

Это SortedListсовершенно удивительный инструмент, который является частью RecyclerViewбиблиотеки. Он заботится об уведомлении Adapterоб изменениях в наборе данных и делает это очень эффективным способом. Единственное, что от вас требуется, это указать порядок элементов. Вы должны сделать это, реализуя compare()метод, который сравнивает два элемента, SortedListкак 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()то ни одно из ваших изменений не будет применено!

Ксавер Капеллер
источник
4
@TiagoOliveira Ну, это сделано, чтобы просто работать из коробки: D Привязка данных является препятствием для людей, незнакомых с этим, но я все равно включил это, потому что это удивительно, и я хочу продвигать это. По какой-то причине, кажется, мало кто знает об этом ...
Ксавер Капеллер
78
Я еще не прочитал весь ответ, мне пришлось приостановить чтение где-то пополам, чтобы написать этот комментарий - это один из лучших ответов, которые я нашел здесь, на SO! Спасибо!
Даниэджела
16
Мне просто нравится, как ты выглядишь: «Из твоего вопроса непонятно, с чем у тебя проблемы, так что вот полноценный пример, который я только что сделал»: D
Фред
7
+1 только для того, чтобы показать нам, что привязка данных существует в Android! Я никогда не слышал об этом и, похоже, я начну им пользоваться. Спасибо
Хорхе
6
Это решение смехотворно длинное и, вообще говоря, изощренное. Перейти на второй.
Энрико Казини
194

Все, что вам нужно сделать, это добавить filterметод в RecyclerView.Adapter:

public void filter(String text) {
    items.clear();
    if(text.isEmpty()){
        items.addAll(itemsCopy);
    } else{
        text = text.toLowerCase();
        for(PhoneBookItem item: itemsCopy){
            if(item.name.toLowerCase().contains(text) || item.phone.toLowerCase().contains(text)){
                items.add(item);
            }
        }
    }
    notifyDataSetChanged();
}

itemsCopyинициализируется в конструкторе адаптера, как itemsCopy.addAll(items).

Если вы это сделаете, просто позвоните filterиз OnQueryTextListener:

searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
    @Override
    public boolean onQueryTextSubmit(String query) {
        adapter.filter(query);
        return true;
    }

    @Override
    public boolean onQueryTextChange(String newText) {
        adapter.filter(newText);
        return true;
    }
});

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

Klimat
источник
11
Я думаю, что это должен быть принятый ответ. Это проще и просто работает
Jose_GD
6
Просто и эффективно!
AlxDroidDev
11
Обратите внимание, что вы потеряете анимацию, если будете следовать этому подходу вместо ответа @Xaver Kapeller.
humazed
23
Не пробовал принятый ответ, потому что он слишком длинный. Этот ответ работает и прост в реализации. Не забудьте добавить "app: actionViewClass =" android.support.v7.widget.SearchView "в ваш элемент меню XML.
SajithK
3
Что именно предметы и предметы копируют здесь?
Lucky_girl
82

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

public abstract class GenericRecycleAdapter<E> extends RecyclerView.Adapter implements Filterable
{
    protected List<E> list;
    protected List<E> originalList;
    protected Context context;

    public GenericRecycleAdapter(Context context,
    List<E> list)
    {
        this.originalList = list;
        this.list = list;
        this.context = context;
    }

    ...

    @Override
    public Filter getFilter() {
        return new Filter() {
            @SuppressWarnings("unchecked")
            @Override
            protected void publishResults(CharSequence constraint, FilterResults results) {
                list = (List<E>) results.values;
                notifyDataSetChanged();
            }

            @Override
            protected FilterResults performFiltering(CharSequence constraint) {
                List<E> filteredResults = null;
                if (constraint.length() == 0) {
                    filteredResults = originalList;
                } else {
                    filteredResults = getFilteredResults(constraint.toString().toLowerCase());
                }

                FilterResults results = new FilterResults();
                results.values = filteredResults;

                return results;
            }
        };
    }

    protected List<E> getFilteredResults(String constraint) {
        List<E> results = new ArrayList<>();

        for (E item : originalList) {
            if (item.getName().toLowerCase().contains(constraint)) {
                results.add(item);
            }
        }
        return results;
    }
} 

E здесь является универсальным типом, вы можете расширить его, используя ваш класс:

public class customerAdapter extends GenericRecycleAdapter<CustomerModel>

Или просто измените E на тип, который вы хотите ( <CustomerModel>например)

Затем из searchView (виджет, который вы можете поместить в menu.xml):

searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
    @Override
    public boolean onQueryTextSubmit(String text) {
        return false;
    }

    @Override
    public boolean onQueryTextChange(String text) {
        yourAdapter.getFilter().filter(text);
        return true;
    }
});
sagits
источник
Я использую что-то вроде этого! Прекрасно работает и общий образец!
Матеус
Здравствуйте, кто может помочь мне шаг за шагом с этим: stackoverflow.com/questions/40754174/…
Торвальд Олавсен
Самый чистый ответ!
adalpari
4
Это намного лучше, чем ответ «за», потому что операция выполняется в рабочем потоке в методе executeFiltering.
Хм
1
Но вы назначаете ссылку на один и тот же список для разных переменных. Например, this.originalList = list; Вместо этого вы должны использовать addAll или передать список в конструкторе ArrayList
Флориан Вальтер,
5

просто создайте два списка в адаптере один оригинальный и один временный и реализует фильтруемый .

    @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;
    }
Zar E Ahmer
источник
Хорошее решение. Я создал два списка и использовал простой метод фильтрации. Я не могу передать правильную позицию адаптера для элемента к следующему действию. Я был бы признателен за любые мысли или идеи, которые вы могли бы предложить для этого: stackoverflow.com/questions/46027110/…
AJW
4

В адаптере:

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

В деятельности:

searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
            @Override
            public boolean onQueryTextSubmit(String query) {
                return false;
            }

            @Override
            public boolean onQueryTextChange(String newText) {
                newText = newText.toLowerCase();
                ArrayList<Channel> newList = new ArrayList<>();
                for (Channel channel: channels){
                    String channelName = channel.getmChannelName().toLowerCase();
                    if (channelName.contains(newText)){
                        newList.add(channel);
                    }
                }
                mAdapter.setFilter(newList);
                return true;
            }
        });
Фироз Ахмед
источник
3

С помощью компонентов архитектуры Android с помощью LiveData это можно легко реализовать с помощью любого типа адаптера. . Вы просто должны сделать следующие шаги:

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

@Dao
public interface CustomDAO{

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

2. Создайте объект ViewModel для обновления ваших данных в реальном времени с помощью метода, который соединит ваш DAO и ваш пользовательский интерфейс.

public class CustomViewModel extends AndroidViewModel {

    private final AppDatabase mAppDatabase;

    public WordListViewModel(@NonNull Application application) {
        super(application);
        this.mAppDatabase = AppDatabase.getInstance(application.getApplicationContext());
    }

    public LiveData<List<Word>> searchQuery(String query) {
        return mAppDatabase.mWordDAO().searchFor(query);
    }

}

3. Вызовите данные из ViewModel на лету, передав запрос через onQueryTextListener, как показано ниже:

Внутри onCreateOptionsMenuустановите ваш слушатель следующим образом

searchView.setOnQueryTextListener(onQueryTextListener);

Настройте прослушиватель запросов где-нибудь в своем классе SearchActivity следующим образом

private android.support.v7.widget.SearchView.OnQueryTextListener onQueryTextListener =
            new android.support.v7.widget.SearchView.OnQueryTextListener() {
                @Override
                public boolean onQueryTextSubmit(String query) {
                    getResults(query);
                    return true;
                }

                @Override
                public boolean onQueryTextChange(String newText) {
                    getResults(newText);
                    return true;
                }

                private void getResults(String newText) {
                    String queryText = "%" + newText + "%";
                    mCustomViewModel.searchQuery(queryText).observe(
                            SearchResultsActivity.this, new Observer<List<Word>>() {
                                @Override
                                public void onChanged(@Nullable List<Word> words) {
                                    if (words == null) return;
                                    searchAdapter.submitList(words);
                                }
                            });
                }
            };

Примечание . Шаги (1.) и (2.) являются стандартными реализациями AAC ViewModel и DAO , единственное действительное «волшебство», происходящее здесь, заключается в OnQueryTextListener, который будет динамически обновлять результаты вашего списка при изменении текста запроса.

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

Панос Гр
источник
1

Это мой взгляд на расширение ответа @klimat, чтобы не потерять фильтрацию анимации.

public void filter(String query){
    int completeListIndex = 0;
    int filteredListIndex = 0;
    while (completeListIndex < completeList.size()){
        Movie item = completeList.get(completeListIndex);
        if(item.getName().toLowerCase().contains(query)){
            if(filteredListIndex < filteredList.size()) {
                Movie filter = filteredList.get(filteredListIndex);
                if (!item.getName().equals(filter.getName())) {
                    filteredList.add(filteredListIndex, item);
                    notifyItemInserted(filteredListIndex);
                }
            }else{
                filteredList.add(filteredListIndex, item);
                notifyItemInserted(filteredListIndex);
            }
            filteredListIndex++;
        }
        else if(filteredListIndex < filteredList.size()){
            Movie filter = filteredList.get(filteredListIndex);
            if (item.getName().equals(filter.getName())) {
                filteredList.remove(filteredListIndex);
                notifyItemRemoved(filteredListIndex);
            }
        }
        completeListIndex++;
    }
}

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

AhmadF
источник
0

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

public void addItem(int position, ExampleModel model) {
    if(position >= mModel.size()) {
        mModel.add(model);
        notifyItemInserted(mModel.size()-1);
    } else {
        mModels.add(position, model);
        notifyItemInserted(position);
    }
}

И изменить также в функциональности moveItem

public void moveItem(int fromPosition, int toPosition) {
    final ExampleModel model = mModels.remove(fromPosition);
    if(toPosition >= mModels.size()) {
        mModels.add(model);
        notifyItemMoved(fromPosition, mModels.size()-1);
    } else {
        mModels.add(toPosition, model);
        notifyItemMoved(fromPosition, toPosition); 
    }
}

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

toidv
источник
Это совсем не обязательно.
Ксавер Капеллер
Для оригинального ответа, если вы не сделаете этого, возникнет исключение IndexOutOfBoundsException, так почему же это не нужно? Вы хотите журнал? @XaverKapeller
toidv
Никакое исключение не произойдет, только если вы реализуете Adapterнеправильный путь. Не видя ваш код, я думаю, что наиболее вероятная проблема заключается в том, что вы не передаете копию списка со всеми элементами в Adapter.
Ксавер Капеллер
Журнал ошибок: W / System.err: java.lang.IndexOutOfBoundsException: недопустимый индекс 36, размер 35 Вт / System.err: в java.util.ArrayList.throwIndexOutOfBoundsException (ArrayList.java:255) W / System.err: at java.util.ArrayList.add (ArrayList.java:147) W / System.err: at com.quomodo.inploi.ui.adapter.MultipleSelectFilterAdapter.addItem (MultipleSelectFilterAdapter.java:125) W / System.err: com .quomodo.inploi.ui.adapter.MultipleSelectFilterAdapter.applyAndAnimateAdditions (MultipleSelectFilterAdapter.java:78)
toidv
Пожалуйста, помогите проверить исходный код ниже @XaverKapeller gist.github.com/toidv/fe71dc45169e4138271b52fdb29420c5
toidv
0

Recyclerview с поиском просмотра и clicklistener

Добавьте интерфейс в свой адаптер.

public interface SelectedUser{

    void selectedUser(UserModel userModel);

}

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

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



}

Полный учебник и исходный код: Recyclerview с searchview и onclicklistener

Ричард Камере
источник
-1

Я решил ту же проблему, используя ссылку с некоторыми изменениями в ней. Фильтр поиска в RecyclerView с картами. Это вообще возможно?(надеюсь это поможет).

Вот мой класс адаптера

public class ContactListRecyclerAdapter extends RecyclerView.Adapter<ContactListRecyclerAdapter.ContactViewHolder> implements Filterable {

Context mContext;
ArrayList<Contact> customerList;
ArrayList<Contact> parentCustomerList;


public ContactListRecyclerAdapter(Context context,ArrayList<Contact> customerList)
{
    this.mContext=context;
    this.customerList=customerList;
    if(customerList!=null)
    parentCustomerList=new ArrayList<>(customerList);
}

   // other overrided methods

@Override
public Filter getFilter() {
    return new FilterCustomerSearch(this,parentCustomerList);
}
}

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

import android.widget.Filter;
import java.util.ArrayList;


public class FilterCustomerSearch extends Filter
{
private final ContactListRecyclerAdapter mAdapter;
ArrayList<Contact> contactList;
ArrayList<Contact> filteredList;

public FilterCustomerSearch(ContactListRecyclerAdapter mAdapter,ArrayList<Contact> contactList) {
    this.mAdapter = mAdapter;
    this.contactList=contactList;
    filteredList=new ArrayList<>();
}

@Override
protected FilterResults performFiltering(CharSequence constraint) {
    filteredList.clear();
    final FilterResults results = new FilterResults();

    if (constraint.length() == 0) {
        filteredList.addAll(contactList);
    } else {
        final String filterPattern = constraint.toString().toLowerCase().trim();

        for (final Contact contact : contactList) {
            if (contact.customerName.contains(constraint)) {
                filteredList.add(contact);
            }
            else if (contact.emailId.contains(constraint))
            {
                filteredList.add(contact);

            }
            else if(contact.phoneNumber.contains(constraint))
                filteredList.add(contact);
        }
    }
    results.values = filteredList;
    results.count = filteredList.size();
    return results;
}

@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
    mAdapter.customerList.clear();
    mAdapter.customerList.addAll((ArrayList<Contact>) results.values);
    mAdapter.notifyDataSetChanged();
}

}

// Класс деятельности

public class HomeCrossFadeActivity extends AppCompatActivity implements View.OnClickListener,OnFragmentInteractionListener,OnTaskCompletedListner
{
Fragment fragment;
 protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_homecrossfadeslidingpane2);CardView mCard;
   setContentView(R.layout.your_main_xml);}
   //other overrided methods
  @Override
   public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.

    MenuInflater inflater = getMenuInflater();
    // Inflate menu to add items to action bar if it is present.
    inflater.inflate(R.menu.menu_customer_view_and_search, menu);
    // Associate searchable configuration with the SearchView
    SearchManager searchManager =
            (SearchManager) getSystemService(Context.SEARCH_SERVICE);
    SearchView searchView =
            (SearchView) menu.findItem(R.id.menu_search).getActionView();
    searchView.setQueryHint("Search Customer");
    searchView.setSearchableInfo(
            searchManager.getSearchableInfo(getComponentName()));

    searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
        @Override
        public boolean onQueryTextSubmit(String query) {
            return false;
        }

        @Override
        public boolean onQueryTextChange(String newText) {
            if(fragment instanceof CustomerDetailsViewWithModifyAndSearch)
                ((CustomerDetailsViewWithModifyAndSearch)fragment).adapter.getFilter().filter(newText);
            return false;
        }
    });



    return true;
}
}

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

Шрути Камоджи
источник