[英]How to filter a RecyclerView with a SearchView
我正在尝试从支持库中实现SearchView
。 我希望用户使用SearchView
过滤RecyclerView
中的电影List
。
到目前为止,我已经学习了一些教程,并将SearchView
添加到了ActionBar
,但我不确定从这里去哪里。 我已经看过一些示例,但是当您开始输入时,它们都没有显示结果。
这是我的MainActivity
:
public class MainActivity extends ActionBarActivity {
RecyclerView mRecyclerView;
RecyclerView.LayoutManager mLayoutManager;
RecyclerView.Adapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recycler_view);
mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
mRecyclerView.setHasFixedSize(true);
mLayoutManager = new LinearLayoutManager(this);
mRecyclerView.setLayoutManager(mLayoutManager);
mAdapter = new CardAdapter() {
@Override
public Filter getFilter() {
return null;
}
};
mRecyclerView.setAdapter(mAdapter);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
SearchView searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
}
这是我的Adapter
:
public abstract class CardAdapter extends RecyclerView.Adapter<CardAdapter.ViewHolder> implements Filterable {
List<Movie> mItems;
public CardAdapter() {
super();
mItems = new ArrayList<Movie>();
Movie movie = new Movie();
movie.setName("Spiderman");
movie.setRating("92");
mItems.add(movie);
movie = new Movie();
movie.setName("Doom 3");
movie.setRating("91");
mItems.add(movie);
movie = new Movie();
movie.setName("Transformers");
movie.setRating("88");
mItems.add(movie);
movie = new Movie();
movie.setName("Transformers 2");
movie.setRating("87");
mItems.add(movie);
movie = new Movie();
movie.setName("Transformers 3");
movie.setRating("86");
mItems.add(movie);
movie = new Movie();
movie.setName("Noah");
movie.setRating("86");
mItems.add(movie);
movie = new Movie();
movie.setName("Ironman");
movie.setRating("86");
mItems.add(movie);
movie = new Movie();
movie.setName("Ironman 2");
movie.setRating("86");
mItems.add(movie);
movie = new Movie();
movie.setName("Ironman 3");
movie.setRating("86");
mItems.add(movie);
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.recycler_view_card_item, viewGroup, false);
return new ViewHolder(v);
}
@Override
public void onBindViewHolder(ViewHolder viewHolder, int i) {
Movie movie = mItems.get(i);
viewHolder.tvMovie.setText(movie.getName());
viewHolder.tvMovieRating.setText(movie.getRating());
}
@Override
public int getItemCount() {
return mItems.size();
}
class ViewHolder extends RecyclerView.ViewHolder{
public TextView tvMovie;
public TextView tvMovieRating;
public ViewHolder(View itemView) {
super(itemView);
tvMovie = (TextView)itemView.findViewById(R.id.movieName);
tvMovieRating = (TextView)itemView.findViewById(R.id.movieRating);
}
}
}
由于从您的问题中并不清楚您到底遇到了什么问题,因此我编写了有关如何实现此功能的快速演练; 如果您仍有问题,请随时提出。
我在这个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
您必须像往常一样OnQueryTextListener
此菜单 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()
方法来做到这一点,该方法像Comparator
一样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
两个模型的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()
实现! 所以你不必自己实现它们。 在 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
:如果我们使用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
做到了这一点! 除此之外,这些方法的实现非常简单,只有一个例外:删除模型List
的 remove 方法。 由于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
。 在这个例子中,我从一个电影数组中创建了一个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
子类,它定义了Adapter
可以用来自动绑定模型的bind()
方法。 这允许我们只通过实现模型和相应的ViewHolder
实现来专注于应该在RecyclerView
显示的内容。 使用这个基类,我们不必担心Adapter
及其SortedList
的复杂细节。
由于 StackOverflow 上答案的字符限制,我无法完成实现此基类的每个步骤,甚至无法在此处添加完整的源代码,但您可以找到此基类的完整源代码 - 我将其称为SortedListAdapter
- 在这个GitHub 要点。
为了让您的生活更简单,我在 jCenter 上发布了一个包含SortedListAdapter
的库! 如果您想使用它,那么您需要做的就是将此依赖项添加到您的应用程序的 build.gradle 文件中:
compile 'com.github.wrdlbrnft:sorted-list-adapter:0.2.0.1'
您可以在图书馆主页上找到有关该图书馆的更多信息。
要使用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()
则不会应用任何更改!
您需要做的就是在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;
}
});
这是按姓名和电话号码过滤我的电话簿的示例。
以更简洁的方式关注@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;
}
});
在适配器中:
public void setFilter(List<Channel> newList){
mChannels = new ArrayList<>();
mChannels.addAll(newList);
notifyDataSetChanged();
}
在活动中:
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
newText = newText.toLowerCase();
ArrayList<Channel> newList = new ArrayList<>();
for (Channel channel: channels){
String channelName = channel.getmChannelName().toLowerCase();
if (channelName.contains(newText)){
newList.add(channel);
}
}
mAdapter.setFilter(newList);
return true;
}
});
只需在适配器中创建两个列表,一个 orignal 和一个 temp 并实现 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;
}
通过使用LiveData 的Android 架构组件,这可以通过任何类型的Adapter轻松实现。 您只需执行以下步骤:
1.设置您的数据从房间数据库作为LiveData返回,如下例所示:
@Dao
public interface CustomDAO{
@Query("SELECT * FROM words_table WHERE column LIKE :searchquery")
public LiveData<List<Word>> searchFor(String searchquery);
}
2.创建一个ViewModel对象,通过连接DAO和UI的方法实时更新数据
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 ViewModel和DAO实现,这里唯一真正的“魔法”是在OnQueryTextListener 中,它会随着查询文本的变化动态更新列表的结果。
如果您需要有关此事的更多说明,请随时提出。 我希望这有帮助:)。
我已经使用链接解决了同样的问题,并对其进行了一些修改。 RecyclerView with Cards 上的搜索过滤器。 甚至有可能吗? (希望这可以帮助)。
这是我的适配器类
public class ContactListRecyclerAdapter extends RecyclerView.Adapter<ContactListRecyclerAdapter.ContactViewHolder> implements Filterable {
Context mContext;
ArrayList<Contact> customerList;
ArrayList<Contact> parentCustomerList;
public ContactListRecyclerAdapter(Context context,ArrayList<Contact> customerList)
{
this.mContext=context;
this.customerList=customerList;
if(customerList!=null)
parentCustomerList=new ArrayList<>(customerList);
}
// other overrided methods
@Override
public Filter getFilter() {
return new FilterCustomerSearch(this,parentCustomerList);
}
}
//过滤器类
import android.widget.Filter;
import java.util.ArrayList;
public class FilterCustomerSearch extends Filter
{
private final ContactListRecyclerAdapter mAdapter;
ArrayList<Contact> contactList;
ArrayList<Contact> filteredList;
public FilterCustomerSearch(ContactListRecyclerAdapter mAdapter,ArrayList<Contact> contactList) {
this.mAdapter = mAdapter;
this.contactList=contactList;
filteredList=new ArrayList<>();
}
@Override
protected FilterResults performFiltering(CharSequence constraint) {
filteredList.clear();
final FilterResults results = new FilterResults();
if (constraint.length() == 0) {
filteredList.addAll(contactList);
} else {
final String filterPattern = constraint.toString().toLowerCase().trim();
for (final Contact contact : contactList) {
if (contact.customerName.contains(constraint)) {
filteredList.add(contact);
}
else if (contact.emailId.contains(constraint))
{
filteredList.add(contact);
}
else if(contact.phoneNumber.contains(constraint))
filteredList.add(contact);
}
}
results.values = filteredList;
results.count = filteredList.size();
return results;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
mAdapter.customerList.clear();
mAdapter.customerList.addAll((ArrayList<Contact>) results.values);
mAdapter.notifyDataSetChanged();
}
}
//活动类
public class HomeCrossFadeActivity extends AppCompatActivity implements View.OnClickListener,OnFragmentInteractionListener,OnTaskCompletedListner
{
Fragment fragment;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_homecrossfadeslidingpane2);CardView mCard;
setContentView(R.layout.your_main_xml);}
//other overrided methods
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
MenuInflater inflater = getMenuInflater();
// Inflate menu to add items to action bar if it is present.
inflater.inflate(R.menu.menu_customer_view_and_search, menu);
// Associate searchable configuration with the SearchView
SearchManager searchManager =
(SearchManager) getSystemService(Context.SEARCH_SERVICE);
SearchView searchView =
(SearchView) menu.findItem(R.id.menu_search).getActionView();
searchView.setQueryHint("Search Customer");
searchView.setSearchableInfo(
searchManager.getSearchableInfo(getComponentName()));
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
if(fragment instanceof CustomerDetailsViewWithModifyAndSearch)
((CustomerDetailsViewWithModifyAndSearch)fragment).adapter.getFilter().filter(newText);
return false;
}
});
return true;
}
}
在 OnQueryTextChangeListener() 方法中使用您的适配器。 我已将它转换为片段,因为我的适配器在片段中。 如果适配器在您的活动类中,您可以直接使用它。
这是我对扩展@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++;
}
}
基本上它所做的是查看一个完整的列表并将项目一项一项地添加/删除到过滤列表中。
在您的适配器中添加一个接口。
public interface SelectedUser{
void selectedUser(UserModel userModel);
}
在您的主要活动中实现接口并覆盖该方法。 @Override public void selectedUser(UserModel userModel) {
startActivity(new Intent(MainActivity.this, SelectedUserActivity.class).putExtra("data",userModel));
}
我不知道为什么每个人都使用同一个列表的 2 个副本来解决这个问题。 这使用了太多的内存...
为什么不直接隐藏未找到的元素,并简单地将它们的索引存储在一个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());
}
});
结果:
我建议使用以下 2 件事修改 @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);
}
}
希望它可以帮助你!
如果你想搜索按钮点击然后这工作正常。
filterIcon.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String strCHR = homeSearchEdit.getText().toString();
if (homeSearchEdit.getText().toString().length() > 0) {
ArrayList<ServiceModel> listNew = new ArrayList<>();
for (int l = 0; l < arrayList.size(); l++) {
String serviceName = arrayList.get(l).getServiceName().toLowerCase();
if (serviceName.contains(strCHR.toLowerCase())) {
listNew.add(arrayList.get(l));
}
}
recyclerView.setVisibility(View.VISIBLE);
adapter = new ServiceAdapter(HomeActivity.this, listNew);
recyclerView.setAdapter(adapter);
} else {
recyclerView.setVisibility(View.VISIBLE);
adapter = new ServiceAdapter(HomeActivity.this, arrayList);
recyclerView.setAdapter(adapter);
}
}
});
其中, filterIcon是按钮, homeSearchEdit是 editText(我们申请搜索的地方)。
Android 提供了DiffUtil.Callback()
和DiffUtil.ItemCallback<T>
,它们帮助我们很好地过滤我们的回收器视图
DiffUtil 是一个实用程序类,它计算两个列表之间的差异并输出一个更新操作列表,将第一个列表转换为第二个列表。
https://developer.android.com/reference/androidx/recyclerview/widget/DiffUtil
DiffUtil.Callback() 与 RecyclerView.Adapter 一起使用
和
DiffUtil.ItemCallback 与 ListAdapter 一起使用
创建你的 RecyclerView 就像你通常会覆盖
onCreateViewHolder
onBindViewHolder
getItemCount
并扩展RecyclerView.ViewHolder Class
就像您所做的一样(这是您的代码片段的 Kotlin 版本)
override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): ViewHolder? {
val v: View = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.recycler_view_card_item, viewGroup, false)
return ViewHolder(v)
}
fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
val movie: Movie = mItems.get(i)
viewHolder.tvMovie.setText(movie.getName())
viewHolder.tvMovieRating.setText(movie.getRating())
}
override fun getItemCount(): Int {
return mItems.size()
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var tvMovie: TextView
var tvMovieRating: TextView
init {
tvMovie = itemView.findViewById<View>(R.id.movieName) as TextView
tvMovieRating = itemView.findViewById<View>(R.id.movieRating) as TextView
}
}
现在创建另一个将实现 DiffUtil.Callback() 的类
此类将帮助将 recyclerviews currentlist 转换为过滤后的列表
class MoviesDiffUtilCallback(private val oldList: List<Movies>, private val newList: List<Movies>) : DiffUtil.Callback() {
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = oldList[oldItemPosition].aUniqueId == newList[newItemPosition]. aUniqueId
//aUniqueId-> a field that is unique to each item in your listItems
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = oldList[oldItemPosition] == newList[newItemPosition]
}
在您的 Activity 或 Fragment 类中设置您的适配器和过滤器
private fun setupAdapter() {
//mItems is the list you will pass to the adapter
adapter = CardAdapter(mItems)
recyclerView.adapter = adapter
}
fun filter(searchText : String){
val newFilter = mItems.filter {
it.name.lowercase().contains(text.lowercase()) //filterlogic
}
//Calculate the list of update operations that can covert one list into the other one
val diffResult = DiffUtil.calculateDiff(PostsDiffUtilCallback(mItems,newFilter))
mItems.clear()
mItems.addAll(newFilter)
//dispatch all updates to the RecyclerView
diffResult.dispatchUpdatesTo(adapter)
}
我们将使用 filterable 接口来帮助我们进行过滤(仍在弄清楚为什么我不应该只使用过滤器函数来获取过滤列表和 submitList(filteredLists) 直接)
创建您的 ListAdapter 类
class CardAdapter (
private val mItems : List<Movies>) : ListAdapter<Movies, CardAdapter.BillsPackageViewHolder>(MoviesDiffCallback()),
Filterable {
override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): ViewHolder? {
val v: View = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.recycler_view_card_item, viewGroup, false)
return ViewHolder(v)
}
fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
val movie: Movie = mItems.get(i)
viewHolder.tvMovie.setText(movie.getName())
viewHolder.tvMovieRating.setText(movie.getRating())
}
override fun getFilter(): Filter {
return object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
return FilterResults().apply {
values = if (constraint.isNullOrEmpty())
mItems
else
onFilter(mItems, constraint.toString())
}
}
@Suppress("UNCHECKED_CAST")
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
submitList(results?.values as? List<Movies>)
}
}
}
fun onFilter(list: List<Movies>, constraint: String) : List<Movies>{
val filteredList = list.filter {
it.name.lowercase().contains(constraint.lowercase())
}
return filteredList
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var tvMovie: TextView
var tvMovieRating: TextView
init {
tvMovie = itemView.findViewById<View>(R.id.movieName) as TextView
tvMovieRating = itemView.findViewById<View>(R.id.movieRating) as TextView
}
}
}
现在创建另一个将实现 DiffUtil.ItemCallback 的类
class MoviesDiffCallback : DiffUtil.ItemCallback<Movies>() {
override fun areItemsTheSame(oldItem: Movies, newItem: Movies): Boolean {
return oldItem.someUniqueid == newItem.someUniqueid
}
override fun areContentsTheSame(oldItem: Movies, newItem: Movies): Boolean {
return oldItem == newItem
}
}
在您的 MainActivity 或 Fragment Setup 中,您的适配器和过滤器
private fun setupAdapter() {
adapter = CardAdapter(mItems)
recyclerView.adapter = adapter
}
fun filter(searchString : String){
adapter.filter.filter(searchString)
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.