介绍
由于您的问题并没有明确指出您具体遇到了什么问题,因此我编写了这个快速教程,说明如何实现此功能;如果您仍有问题,请随时询问。
我在GitHub存储库中提供了我所讲解内容的完整示例。
无论如何,结果应该是这样的:
![演示图像](https://istack.dev59.com/qDtH7.gif)
如果您首先想尝试演示应用程序,则可以从Play Store安装它:
![在Google Play上获取](https://developer.android.com/images/brand/en_generic_rgb_wo_60.png)
设置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) {
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();
}
}
在回调方法的顶部,例如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()
实现,则通常可以像上面那样实现它。如果我们向 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()
并传入
ExampleModel
的
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
实现更简单。
概括问题并简化适配器
在本节中,我不会进入太多细节 - 部分原因是我已经接近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()
,那么您所做的任何更改都不会被应用!