简介
因为从你的问题中不清楚你到底遇到了什么麻烦,我写了这个关于如何实现这个功能的快速演练;如果你还有问题,请提出来。
在这个GitHub知识库中,我有一个我在这里谈论的所有事情的工作示例。
在任何情况下,结果应该是这样的:
如果你想先玩一下演示应用,你可以从play Store安装它:
不管怎样,我们开始吧。
设置SearchView
在res/menu文件夹中创建一个名为main_menu.xml的新文件。在其中添加一个项目,并将actionViewClass设置为android.support.v7.widget.SearchView。因为您正在使用支持库,所以必须使用支持库的名称空间来设置actionViewClass属性。你的xml文件应该是这样的:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_search"
android:title="@string/action_search"
app:actionViewClass="android.support.v7.widget.SearchView"
app:showAsAction="always"/>
</menu>
在你的片段或活动中,你必须像往常一样膨胀这个菜单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已经可以使用了。一旦我们完成了适配器的实现,我们将在稍后的onQueryTextChange()中实现过滤器逻辑。
设置适配器
首先,这是我将在这个例子中使用的模型类:
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>
如您所见,我使用了数据绑定。如果您以前从未使用过数据绑定,请不要气馁!这是非常简单和强大的,但是我不能解释它是如何在这个答案的范围内工作的。
这是exampleemodel类的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);
}
}
同样没有什么特别的。它只是使用数据绑定将模型类绑定到这个布局,就像我们在上面的布局xml中定义的那样。
现在我们终于可以进入真正有趣的部分:编写适配器。我将跳过适配器的基本实现,而是集中讨论与这个答案相关的部分。
但首先我们必须讨论一件事:SortedList类。
SortedList
SortedList是一个非常棒的工具,它是RecyclerView库的一部分。它负责将数据集的更改通知适配器,这是一种非常有效的方式。它需要你做的唯一一件事就是指定元素的顺序。您需要通过实现一个compare()方法来做到这一点,该方法比较SortedList中的两个元素,就像Comparator一样。但它不是对List进行排序,而是用于对RecyclerView中的项目进行排序!
SortedList通过回调类与适配器交互,你必须实现:
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等,你必须调用适配器的等效notify方法。底部的三个方法比较,arecontentssame和areitemssame你必须根据你想要显示的对象类型和这些对象应该以什么顺序出现在屏幕上来实现。
让我们来逐个介绍这些方法:
@Override
public int compare(ExampleModel a, ExampleModel b) {
return mComparator.compare(a, b);
}
这就是我前面谈到的compare()方法。在这个例子中,我只是将调用传递给比较器,比较两个模型。如果您想让项目按字母顺序显示在屏幕上。这个比较器看起来是这样的:
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();
}
The SortedList uses this method to check if two items refer to the same thing. In simplest terms (without explaining how the SortedList works) this is used to determine if an object is already contained in the List and if either an add, move or change animation needs to be played. If your models have an id you would usually compare just the id in this method. If they don't you need to figure out some other way to check this, but however you end up implementing this depends on your specific app. Usually it is the simplest option to give all models an id - that could for example be the primary key field if you are querying the data from a database.
使用SortedList。我们可以创建一个SortedList的实例:
final SortedList<ExampleModel> list = new SortedList<>(ExampleModel.class, mCallback);
作为SortedList构造函数中的第一个参数,您需要传递模型的类。另一个参数是SortedList。我们上面定义的回调函数。
现在让我们进入正事:如果我们用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。
现在我们差不多完成了!但是我们首先需要一种向适配器添加或删除项的方法。为此,我们可以向适配器添加方法,允许我们向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();
}
We don't need to call any notify methods here because the SortedList already does this for through the SortedList.Callback! Aside from that the implementation of these methods is pretty straight forward with one exception: the remove method which removes a List of models. Since the SortedList has only one remove method which can remove a single object we need to loop over the list and remove the models one by one. Calling beginBatchedUpdates() at the beginning batches all the changes we are going to make to the SortedList together and improves performance. When we call endBatchedUpdates() the RecyclerView is notified about all the changes at once.
此外,你必须理解的是,如果你添加一个对象到SortedList,它已经在SortedList中,它不会再被添加。相反,SortedList使用arecontentssame()方法来确定对象是否发生了变化——如果发生了变化,则RecyclerView中的项将被更新。
无论如何,我通常更喜欢的是一种方法,它允许我一次替换RecyclerView中的所有项目。删除列表中没有的所有内容,并添加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中但已更改的所有项。
这样适配器就完成了。整个过程应该是这样的:
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实例列表:
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);
}
这里没有什么特别的事情,我们只是实例化适配器并将其设置为RecyclerView。之后,我们从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;
}
This is again pretty straight forward. We call the method filter() and pass in the List of ExampleModels as well as the query string. We then call replaceAll() on the Adapter and pass in the filtered List returned by filter(). We also have to call scrollToPosition(0) on the RecyclerView to ensure that the user can always see all items when searching for something. Otherwise the RecyclerView might stay in a scrolled down position while filtering and subsequently hide a few items. Scrolling to the top ensures a better user experience while searching.
现在唯一要做的就是实现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开始,你可以免费获得项目动画!
我意识到这是一个非常详细的描述,这可能使整个事情看起来比实际上更复杂,但有一种方法可以概括整个问题,并使基于SortedList的适配器实现更加简单。
泛化问题并简化适配器
在本节中,我不打算详细介绍——部分原因是我在Stack Overflow上遇到了回答的字符限制,但也因为上面已经解释了大部分内容——但要总结一下更改:我们可以实现一个基本Adapter类,它已经负责处理SortedList以及将模型绑定到ViewHolder实例,并提供了一种方便的方法来实现基于SortedList的Adapter。为此,我们必须做两件事:
我们需要创建一个所有模型类都必须实现的ViewModel接口
我们需要创建一个ViewHolder子类,它定义了一个bind()方法,适配器可以使用它自动绑定模型。
这允许我们只关注应该在RecyclerView中显示的内容,只实现模型和相应的ViewHolder实现。使用这个基类,我们不必担心Adapter及其SortedList的复杂细节。
SortedListAdapter
由于StackOverflow上的答案的字符限制,我不能通过实现这个基类的每个步骤,甚至在这里添加完整的源代码,但你可以在这个GitHub Gist中找到这个基类的完整源代码-我称之为SortedListAdapter。
为了让您的生活变得简单,我在jCenter上发布了一个库,其中包含SortedListAdapter!如果你想使用它,那么你所需要做的就是将这个依赖添加到应用程序的构建中。gradle文件:
compile 'com.github.wrdlbrnft:sorted-list-adapter:0.2.0.1'
你可以在图书馆主页上找到更多关于这个图书馆的信息。
使用SortedListAdapter
要使用SortedListAdapter,我们必须做两个更改:
Change the ViewHolder so that it extends SortedListAdapter.ViewHolder. The type parameter should be the model which should be bound to this ViewHolder - in this case ExampleModel. You have to bind data to your models in performBind() instead of 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);
}
}
Make sure that all your models implement the ViewModel interface:
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(),那么你的任何更改都不会被应用!