如何使用SearchView过滤RecyclerView

372

我正在尝试实现来自支持库的SearchView。 我希望用户能使用SearchViewRecyclerView中过滤List电影。

到目前为止,我已经按照一些教程添加了SearchViewActionBar,但我不太确定接下来该怎么做。我看过一些示例,但没有一个可以在键入时显示结果。

这是我的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);
    }
}

这是我的适配器

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);
        }
    }
}
13个回答

1016

介绍

由于您的问题并没有明确指出您具体遇到了什么问题,因此我编写了这个快速教程,说明如何实现此功能;如果您仍有问题,请随时询问。

我在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文件,然后可以查找包含SearchView的MenuItem并实现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已经可以使用了。一旦我们完成了Adapter的实现,我们将在onQueryTextChange()中实现筛选逻辑。


设置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>

如您所见,我使用了数据绑定。如果您以前没有使用过数据绑定,请不要灰心!它非常简单而且强大,但是在此答案的范围内我无法解释其工作原理。

这是ExampleModel类的ViewHolder

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

没有什么特别的。它只是使用数据绑定将模型类与我们在上面定义的布局的绑定。

现在我们终于可以来到真正有趣的部分:编写适配器。我要跳过Adapter的基本实现,而是专注于对这个答案有关联的部分。

但首先我们得谈一谈:SortedList类。


SortedList

SortedList是一个非常神奇的工具,它是RecyclerView库的一部分。它负责通知Adapter关于数据集的更改,并以非常高效的方式实现。它唯一需要你做的就是指定元素的顺序。你需要通过实现一个compare()方法来实现,该方法就像一个Comparator一样比较SortedList中的两个元素。但是,它不是用于排序List,而是用于排序RecyclerView的项目!

SortedList通过一个Callback类与Adapter进行交互,你必须实现这个类:

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

在回调方法的顶部,例如onMovedonInserted等,您必须调用Adapter相应的通知方法。底部的三个方法compareareContentsTheSameareItemsTheSame,则根据您想要显示的对象类型以及这些对象在屏幕上出现的顺序进行实现。

让我们逐一介绍这些方法:

@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() 实现,则通常可以像上面那样实现它。如果我们向 ExampleModel 类添加一个 equals()hashCode() 实现,则应该如下所示:

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中,以及是否需要播放添加、移动或更改动画。如果您的模型具有ID,则通常仅在此方法中比较ID。如果没有,您需要想出其他方法来检查它,但是您最终实现这种方法的方式取决于您特定的应用程序。通常,将所有模型都赋予一个ID是最简单的选择 - 如果您从数据库查询数据,则可以使用主键字段作为ID。

正确实现SortedList.Callback后,我们可以创建SortedList的实例:

final SortedList<ExampleModel> list = new SortedList<>(ExampleModel.class, mCallback);

SortedList的构造函数中,第一个参数需要传递你的模型类。另一个参数只是我们上面定义的SortedList.Callback

现在让我们开始实际操作:如果我们使用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.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 自动完成了这个过程!除此之外,这些方法的实现非常简单,唯一需要注意的是删除方法会移除一个模型列表。由于 SortedList 只有一个删除方法可以删除单个对象,我们需要循环遍历列表并逐个删除模型。在开始时调用 beginBatchedUpdates() 将所有要对 SortedList 进行的更改捆绑在一起,以提高性能。当我们调用 endBatchedUpdates() 时,RecyclerView 会一次性通知所有更改。

此外,你需要理解的是,如果你向 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();
}

这个方法将所有的更新批处理在一起,以提高性能。第一个循环是反向的,因为从开头移除一个项目会破坏后面所有项目的索引,这可能会导致数据不一致等问题。之后,我们只需使用addAll()List添加到SortedList中,以添加所有尚未在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。在本例中,我从电影数组中创建了一个ExampleModel实例的List

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。之后,我们从MOVIES数组中的电影名称创建一个模型List,然后将所有模型添加到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()并传入ExampleModelList以及查询字符串。然后在Adapter上调用replaceAll()并将filter()返回的过滤后的List传入其中。我们还需要在RecyclerView上调用scrollToPosition(0),以确保用户在搜索内容时始终可以看到所有项目。否则,在过滤和随后隐藏一些项目时,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及以上版本上运行,从API级别11开始,您可以免费获得项目动画效果!

我知道这是一个非常详细的描述,可能使整个过程看起来比实际复杂,但我们有一种方法可以概括这个问题并使基于SortedListAdapter实现更简单。


概括问题并简化适配器

在本节中,我不会进入太多细节 - 部分原因是我已经接近Stack Overflow答案的字符限制,还因为大部分已经在上面解释了 - 但是总结一下更改:我们可以实现一个基类Adapter,它已经处理了与SortedList的交互以及将模型绑定到ViewHolder实例并提供方便的方法来基于SortedList实现Adapter。为此,我们必须做两件事:

  • 我们需要创建一个 ViewModel接口,所有模型类都必须实现它
  • 我们需要创建一个ViewHolder子类,它定义了一个 bind()方法,该Adapter可以使用它自动绑定模型。

这使我们只需专注于在RecyclerView中显示的内容,只需实现模型和其对应的ViewHolder实现即可。使用此基类,我们不必担心Adapter及其SortedList的复杂细节。

SortedListAdapter

由于StackOverflow答案的字符限制,我无法逐步介绍实现此基类的每个步骤,甚至无法在此处添加完整的源代码,但是您可以在此GitHub Gist中找到此基类的完整源代码 - 我称其为SortedListAdapter

为了让您的生活更简单,我发布了一个包含SortedListAdapter的库在jCenter上!如果要使用它,则只需将此依赖项添加到应用程序的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并删除我们不再需要的所有内容。类型参数应该是你正在使用的模型的类型 - 在这种情况下为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 没有我们原来的 ExampleAdapter 有的 add()remove()replaceAll() 方法。它使用单独的 Editor 对象来修改列表中的项目,可以通过 edit() 方法访问。因此,如果你想删除或添加项目,必须调用 edit(),然后在这个 Editor 实例上添加和删除项目,在完成后,在它上面调用 commit() 来应用更改到 SortedList

mAdapter.edit()
        .remove(modelToRemove)
        .add(listOfModelsToAdd)
        .commit();

通过这种方式所做的所有更改都被批处理在一起,以提高性能。我们在前面章节中实现的 replaceAll() 方法也存在于此 Editor 对象中:

mAdapter.edit()
        .replaceAll(mModels)
        .commit();

如果您忘记调用commit(),那么您所做的任何更改都不会被应用!


6
@TiagoOliveira 好的,它被制作成开箱即用:D数据绑定对于不熟悉它的人来说是一道障碍,但我还是包含了它,因为它很棒,我想推广它。由于某种原因,似乎并不是很多人知道它... - Xaver Kapeller
102
我还没有完全读完答案,我在一半的时候停下来写下这个评论 - 这是我在SO上找到的最好的答案之一!谢谢! - daneejela
24
我很喜欢你的回答:「从你的问题中不清楚你遇到了什么问题,所以这里是我刚刚做的一个完整的例子」:D - Fred
8
+1只是为了向我们展示Android中的数据绑定存在!我以前从未听说过它,看起来我将开始使用它。谢谢 - Jorge Casariego
19
第一个方案过于冗长,一般来说是过度设计的。选择第二个方案。 - Enrico Casini
显示剩余26条评论

242

你只需要在 RecyclerView.Adapter 中添加 filter 方法:

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)

如果您这样做,只需从OnQueryTextListener中调用filter

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

这是一个通过姓名和电话号码筛选电话簿的示例。


16
我认为这应该是被接受的答案。它更简单,并且只需要运行即可。 - Jose_GD
7
简单高效! - AlxDroidDev
13
请注意,如果您使用这种方法而不是@Xaver Kapeller的答案,则会失去动画效果。 - humazed
26
因为接受的答案太冗长,所以没有尝试过。这个答案可行且易于实现。不要忘记在菜单项XML中添加 "app:actionViewClass="android.support.v7.widget.SearchView"。 - SajithK
8
这里的items和itemsCopy具体是什么? - Lucky_girl
显示剩余18条评论

90

跟随 @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;
    }
});

我使用类似这样的东西!它运行良好且是通用示例! - Mateus
5
这个答案比得到赞同票的答案好得多,因为它在performFiltering方法中在一个工作线程上执行操作。 - Hmmm
1
但是你将同一个List分配给不同的变量引用。例如,this.originalList = list; 你应该使用addAll或者在ArrayList构造函数中传递列表。 - Florian Walther
最佳答案,完美运作。简洁、明了、切中要点。 - amira
1
想知道在 getFilteredResults 方法中,如何使用泛型 E 的 item.getName()。 - crack_head
显示剩余5条评论

9

在适配器中:

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

在Activity中:

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

非常容易和简单,谢谢Firoz。 - Amirhussein

6
只需要在适配器中创建两个列表,一个是原始列表,一个是临时列表,并实现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;
    }

不错的解决方案。我创建了两个列表并使用了简单的过滤方法。我似乎无法将项目的正确适配器位置传递到下一个活动。如果您有任何想法或建议,我将不胜感激:https://stackoverflow.com/questions/46027110/recyclerview-wrong-position-in-filtered-list - AJW

4
通过使用Android Architecture Components中的LiveData,可以轻松地使用任何类型的适配器实现此功能。您只需按照以下步骤操作:
1. 设置您的数据从Room数据库返回为LiveData,如下例所示:
@Dao
public interface CustomDAO{

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

2. 创建一个ViewModel对象,通过一个方法连接你的DAOUI来实时更新你的数据。

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. 通过在 onQueryTextListener 中传递查询,即可实时从 ViewModel 中调用数据:

请在 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 ViewModelDAO实现,这里唯一真正的“魔力”在于OnQueryTextListener,它会随着查询文本改变动态更新您列表中的结果。

如果您需要更多澄清,请不要犹豫问我。 希望这有所帮助:)


3

我不知道为什么大家都在使用两个相同的列表来解决这个问题。这将使用过多的 RAM...

为什么不只是隐藏没有找到的元素,并简单地将它们的索引存储在一个Set中,以便稍后恢复它们呢?如果您的对象非常大,这样会占用更少的RAM。

public class MyRecyclerViewAdapter extends RecyclerView.Adapter<MyRecyclerViewAdapter.SampleViewHolders>{
    private List<MyObject> myObjectsList; //holds the items of type MyObject
    private Set<Integer> foundObjects; //holds the indices of the found items

    public MyRecyclerViewAdapter(Context context, List<MyObject> myObjectsList)
    {
        this.myObjectsList = myObjectsList;
        this.foundObjects = new HashSet<>();
        //first, add all indices to the indices set
        for(int i = 0; i < this.myObjectsList.size(); i++)
        {
            this.foundObjects.add(i);
        }
    }

    @NonNull
    @Override
    public SampleViewHolders onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View layoutView = LayoutInflater.from(parent.getContext()).inflate(
                R.layout.my_layout_for_staggered_grid, null);
        MyRecyclerViewAdapter.SampleViewHolders rcv = new MyRecyclerViewAdapter.SampleViewHolders(layoutView);
        return rcv;
    }

    @Override
    public void onBindViewHolder(@NonNull SampleViewHolders holder, int position)
    {
        //look for object in O(1) in the indices set
        if(!foundObjects.contains(position))
        {
            //object not found => hide it.
            holder.hideLayout();
            return;
        }
        else
        {
            //object found => show it.
            holder.showLayout();
        }

        //holder.imgImageView.setImageResource(...)
        //holder.nameTextView.setText(...)
    }

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

    public void findObject(String text)
    {
        //look for "text" in the objects list
        for(int i = 0; i < myObjectsList.size(); i++)
        {
            //if it's empty text, we want all objects, so just add it to the set.
            if(text.length() == 0)
            {
                foundObjects.add(i);
            }
            else
            {
                //otherwise check if it meets your search criteria and add it or remove it accordingly
                if (myObjectsList.get(i).getName().toLowerCase().contains(text.toLowerCase()))
                {
                    foundObjects.add(i);
                }
                else
                {
                    foundObjects.remove(i);
                }
            }
        }
        notifyDataSetChanged();
    }

    public class SampleViewHolders extends RecyclerView.ViewHolder implements View.OnClickListener
    {
        public ImageView imgImageView;
        public TextView nameTextView;

        private final CardView layout;
        private final CardView.LayoutParams hiddenLayoutParams;
        private final CardView.LayoutParams shownLayoutParams;

        
        public SampleViewHolders(View itemView)
        {
            super(itemView);
            itemView.setOnClickListener(this);
            imgImageView = (ImageView) itemView.findViewById(R.id.some_image_view);
            nameTextView = (TextView) itemView.findViewById(R.id.display_name_textview);

            layout = itemView.findViewById(R.id.card_view); //card_view is the id of my androidx.cardview.widget.CardView in my xml layout
            //prepare hidden layout params with height = 0, and visible layout params for later - see hideLayout() and showLayout()
            hiddenLayoutParams = new CardView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT);
            hiddenLayoutParams.height = 0;
            shownLayoutParams = new CardView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT);
        }

        @Override
        public void onClick(View view)
        {
            //implement...
        }

        private void hideLayout() {
            //hide the layout
            layout.setLayoutParams(hiddenLayoutParams);
        }

        private void showLayout() {
            //show the layout
            layout.setLayoutParams(shownLayoutParams);
        }
    }
}

我只是在我的搜索框中使用了一个EditText

cardsSearchTextView.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

            }

            @Override
            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {

            }

            @Override
            public void afterTextChanged(Editable editable) {
                myViewAdapter.findObject(editable.toString().toLowerCase());
            }
        });

结果:

搜索示例gif


1
我只是尝试了一下,当我有一个超过2000个项目的列表时,会出现延迟,因为它在主列表上搜索而不是在筛选后的列表上搜索。但无论如何,还是谢谢你,当我有小列表时我会使用它的 ;) - Nabil Ait Brahim
哦,谢谢你给它一个机会!对于这么大的列表,我没有尝试过,你可能是正确的。 - Alaa M.
它并不会使用太多的RAM,因为列表只保存对象的引用,即它不会复制整个对象。 - Cilenco

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

}

//Activity类

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()方法中使用您的适配器。我已将其转换为片段,因为我的适配器在片段中。如果适配器在您的活动类中,则可以直接使用适配器。

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

基本上它的作用是浏览完整列表,并逐一添加/删除项目到一个过滤列表中。

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

希望能对你有所帮助!


这完全不必要。 - Xaver Kapeller
如果您不这样做,就会发生IndexOutOfBoundsException异常,那么为什么不必要呢?您需要日志吗?@XaverKapeller - toidv
没有异常会发生,除非您错误地实现了“Adapter”。没有看到您的代码,我猜最可能的问题是您没有将包含所有项的列表的副本传递给“Adapter”。 - Xaver Kapeller
错误日志: W/System.err: java.lang.IndexOutOfBoundsException: Invalid index 36, size is 35 W/System.err: at 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: at com.quomodo.inploi.ui.adapter.MultipleSelectFilterAdapter.applyAndAnimateAdditions(MultipleSelectFilterAdapter.java:78) - toidv
这个Gist里有很多代码与“Adapter”无关,但是我需要帮助你的重要部分却缺失了。我猜测“MultipleSelectFilterAdapter”是导致你问题的类?更准确地说,是继承“MultipleSelectFilterAdapter”的那个类?“MultipleSelectKeySkillAdapter”中的“setItem()”是如何实现的?问题可能就出在那里。不管怎样,你是在一个生产应用中使用它吗?我的原始答案从未打算用于生产。它只是一个例子,用来解释在“RecyclerView”中如何工作的项目动画。 - Xaver Kapeller
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接