简介
由于您的问题并不清楚您到底遇到了什么问题,所以我写了这个关于如何实现此功能的快速演练;如果您仍有疑问,请随时提出。
我在GitHub Repository 中提供了我在这里谈论的所有内容的工作示例。
无论如何,结果应该是这样的:
如果您首先想试用演示应用,可以从 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);
}
}
同样没有什么特别的。它只是使用数据绑定将模型类绑定到这个布局,就像我们在上面的布局 xml 中定义的那样。
现在我们终于可以进入真正有趣的部分了:编写适配器。我将跳过Adapter 的基本实现,而是专注于与此答案相关的部分。
但首先我们要谈一件事:SortedList 类。
排序列表
SortedList 是一个非常棒的工具,它是RecyclerView 库的一部分。它负责通知Adapter 关于数据集的更改,这是一种非常有效的方式。它唯一需要您做的就是指定元素的顺序。您需要通过实现一个compare() 方法来做到这一点,该方法比较SortedList 中的两个元素,就像Comparator 一样。但不是对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();
}
}
在回调顶部的方法中,如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() 实现的功能,只需按一下按钮!所以你不必自己实现它们。在 Internet 上查找它在 IDE 中的工作原理!
现在我们来看看最后一种方法:
@Override
public boolean areItemsTheSame(ExampleModel item1, ExampleModel item2) {
return item1.getId() == item2.getId();
}
SortedList 使用此方法来检查两个项目是否引用同一事物。简单来说(不解释SortedList 的工作原理),它用于确定对象是否已包含在List 中,以及是否需要播放添加、移动或更改动画。如果您的模型有 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 执行此操作!除此之外,这些方法的实现非常简单,只有一个例外:remove 方法删除了List 的模型。由于SortedList 只有一个可以删除单个对象的 remove 方法,我们需要遍历列表并逐个删除模型。在开始时调用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();
}
此方法再次将所有更新批处理在一起以提高性能。第一个循环是相反的,因为在开始时删除一个项目会弄乱它之后出现的所有项目的索引,这在某些情况下会导致数据不一致等问题。之后,我们只需使用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。对于这个例子,我从一组电影中创建了一个 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。之后,我们从 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() 并传入ExampleModels 的List 以及查询字符串。然后我们在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 开始,您可以免费获得项目动画!
我意识到这是一个非常详细的描述,这可能会使整个事情看起来比实际更复杂,但是有一种方法可以概括整个问题并基于 SortedList 实现 Adapter简单得多。
概括问题并简化Adapter
在本节中,我不会详细介绍 - 部分原因是我在 Stack Overflow 上遇到了字符数限制,但也因为上面已经解释了大部分内容 - 但总结一下变化:我们可以实现一个基本的Adapter 类,它已经负责处理SortedList 以及将模型绑定到ViewHolder 实例,并提供了一种基于SortedList 实现Adapter 的便捷方法。为此,我们必须做两件事:
- 我们需要创建一个所有模型类都必须实现的
ViewModel 接口
- 我们需要创建一个
ViewHolder 子类,它定义了一个bind() 方法,Adapter 可以用来自动绑定模型。
这允许我们只关注应该在RecyclerView 中显示的内容,只需实现模型和相应的ViewHolder 实现即可。使用这个基类,我们不必担心Adapter 及其SortedList 的复杂细节。
排序列表适配器
由于 StackOverflow 上答案的字符数限制,我无法完成实现此基类的每一步,甚至无法在此处添加完整的源代码,但您可以找到此基类的完整源代码 - 我称之为SortedListAdapter - 在这个GitHub Gist.
为了让您的生活更简单,我在 jCenter 上发布了一个库,其中包含 SortedListAdapter!如果您想使用它,那么您需要做的就是将此依赖项添加到您应用的 build.gradle 文件中:
compile 'com.github.wrdlbrnft:sorted-list-adapter:0.2.0.1'
你可以在on the library homepage找到更多关于这个库的信息。
使用 SortedListAdapter
要使用SortedListAdapter,我们必须进行两项更改:
-
更改ViewHolder,使其扩展SortedListAdapter.ViewHolder。类型参数应该是应该绑定到这个ViewHolder的模型——在这种情况下是ExampleModel。您必须将数据绑定到 performBind() 而不是 bind() 中的模型。
public class ExampleViewHolder extends SortedListAdapter.ViewHolder<ExampleModel> {
private final ItemExampleBinding mBinding;
public ExampleViewHolder(ItemExampleBinding binding) {
super(binding.getRoot());
mBinding = binding;
}
@Override
protected void performBind(ExampleModel item) {
mBinding.setModel(item);
}
}
-
确保所有模型都实现ViewModel 接口:
public class ExampleModel implements SortedListAdapter.ViewModel {
...
}
之后,我们只需更新ExampleAdapter 以扩展SortedListAdapter 并删除我们不再需要的所有内容。 type 参数应该是您正在使用的模型的类型 - 在本例中为 ExampleModel。但如果您使用不同类型的模型,请将 type 参数设置为 ViewModel。
public class ExampleAdapter extends SortedListAdapter<ExampleModel> {
public ExampleAdapter(Context context, Comparator<ExampleModel> comparator) {
super(context, ExampleModel.class, comparator);
}
@Override
protected ViewHolder<? extends ExampleModel> onCreateViewHolder(LayoutInflater inflater, ViewGroup parent, int viewType) {
final ItemExampleBinding binding = ItemExampleBinding.inflate(inflater, parent, false);
return new ExampleViewHolder(binding);
}
@Override
protected boolean areItemsTheSame(ExampleModel item1, ExampleModel item2) {
return item1.getId() == item2.getId();
}
@Override
protected boolean areItemContentsTheSame(ExampleModel oldItem, ExampleModel newItem) {
return oldItem.equals(newItem);
}
}
之后我们就完成了!然而最后一件事要提到:SortedListAdapter 没有我们原来的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(),则您的任何更改都不会应用!