博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
RecyclerView Part 2:选择模式
阅读量:6372 次
发布时间:2019-06-23

本文共 12942 字,大约阅读时间需要 43 分钟。

一位非常有名的人曾经说过,

此生的事情永远比后世还容易。因为,此生自己做主。

这是真的吗?或许这值的去讨论。当去选择RecyclerView中的item时,虽然你实际上是操作自己:RecyclerView并没有给你相关的工具去做这件事 。所以,我们应该怎么去实现它?

我想说如果你按我的方法做会很简单,现在开始。下面是我研究发现的。

(如果你喜欢,你可以看完整的项目,在这里。如果你只想很快的去使用它,可以跳过前面的部分,直接阅读后面的“TL;DR”)

回顾:选择模式和上下文操作模式(Chocie Modes和Contextual Action Modes)

我打算实现像中CriminalIntent应用中的多项选择那样的效果:通过一个上下文操作模式。下面就是它的代码实现(为了方便展示,我只展示有趣的部分——当然你可以在找到所有的代码):

listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL); listView.setMultiChoiceModeListener(new MultiChoiceModeListener() { public boolean onCreateActionMode(ActionMode mode, Menu menu) { ... } public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { ... } public boolean onActionItemClicked(ActionMode mode, MenuItem item) { switch (item.getItemId()) { case R.id.menu_item_delete_crime: CrimeAdapter adapter = (CrimeAdapter)getListAdapter(); CrimeLab crimeLab = CrimeLab.get(getActivity()); for (int i = adapter.getCount() - 1; i >= 0; i--) { if (getListView().isItemChecked(i)) { crimeLab.deleteCrime(adapter.getItem(i)); } } mode.finish(); adapter.notifyDataSetChanged(); return true; default: return false; } public boolean onPrepareActionMode(ActionMode mode, Menu menu) { ... } public void onDestroyActionMode(ActionMode mode) { ... } });

ListView中有模式选择的概念。如果ListView在一个特定的选择模式,它会通过一个显示的复选接口处理所有细节,一直跟踪检测标记和当单个item被点击时触发切换。像上面看到的那样,你通过调用ListView.setChoiceMode()来选择模式。通过ListView.isItemChecked(int)来检测item是否被先中(像你在onActionItemClicked看到的那样)。

当使用了CHOICE_MODE_MULTIPLE_MODAL,你长按list中的任何item都会自动启动多选择模式。同时,它将激活一个代表多选择交互的操作(Action)模式。上面的MultiChoiceModeListener是一个上下文操作模式的监听器——它像是一个只服务于这种模式的选择回调模式集合。

在中,我们知道了RecyclerView让我们自己实现所有的这些。所以,你需要实现三个部分。

  • 显示哪个视图被选择了
  • 监视list中所有item的被选择和未被选择状态
  • 在上下文操作模式中控制

在一个完美的世界中,将会有一些事情是你在现实世界中实际想做的。当我写这这个时候,我发现了我解决办法的缺陷。我可以想像某人在阅读这篇文章时,摇头说:“这是认真的吗?我需要自己每次实现所有这些?”

所以在这篇文章中我将解释详细,从而可以你自己轻松实现如果你需要的话。同样,我提供了一个叫做MultiSelector 的包,这是一个最直接的解决方案。

保持跟踪状态

这是最直接的,所以我们先解决它。在ListView,它是这样实现的:

// Check item 0mListView.setItemChecked(0, true);// Returns truemListView.isItemChecked(0);// Says what the choice mode currently ismListView.getChoiceMode();

我们自己的实现是这样子的:

private SparseBooleanArray mSelectedPositions = new SparseBooleanArray(); private mIsSelectable = false; private void setItemChecked(int position, boolean isChecked) { mSelectedPositions.put(position, isChecked); } private boolean isItemChecked(int position) { return mSelectedPositions.get(position); } private void setSelectable(boolean selectable) { mIsSelectable = selectable; } private boolean isSelectable() { return mIsSelectable; }

现在程序不会像ListView.setItemChecked()那样更新用户接口,但它现在将会那样做。

当然,你可以用自己喜欢的方式去追踪。对象集合是一个不错的选择。
我把这个想法放到一个叫做MultiSelector的对象中:

MultiSelector selector = new MultiSelector(); selector.setSelected(0, true); selector.isSelected(0); selector.setSelectable(true); selector.isSelectable();

显示选项状态

ListView从Honeycomb开始,item选择就已经像这样可视化了:当一个item被选中时,视图就会通过调用setActivated(true)把它设置为“激活”状态。当视图不再被选择时,它会设制为false。它是通过使用XML StateListDrawables直接开启选择模式从而突出选择模式。

你可以用ViewHolder的bindCrime做同样的事:

private class CrimeHolder extends ViewHolder { ... public void bindCrime(Crime crime) { mCrime = crime; mSolvedCheckBox.setChecked(crime.isSolved()); boolean isSelected = mMultiSelector.isSelected(getPosition()); itemView.setActivated(isSelected); } }

当然,如果你想用其它方式实现选择,你可以。你潜力无限。尽管,Drawable和state list动画做激活状态是默认的好选择。

如果仅仅是这些,我就不用花费那么多时间了。但是我花费了那么多时间,因为我固执的要实现一些我想要的视觉效果。

Material animations

Material Design包括这种非常酷的波纹动画。如果你在  中读过它,你将发现你能在任何时候使用它,当你使用?android:selectableItemBackground 做为你的背景时。

如果你要使用激活状态,虽然,这不是一个好的选择。?android:selectableItemBackground的可视化不支持激活状态。你可以试着用状态选择drawable(state selector drawable)去实现支持激活状态,但是它最终的结果看起来是这样的:

你每次点击它的时候选择中状态都会有反应。所以,当你点击视图关闭激活状态时,你同样会得到波纹效果。这对我没有意义。在我心里,list只有两种状态:正常状态和选择状态。在正常状态,一个点击能产生?android:selectableItemBackground带给我的效果。在选择状态,一个点击只能触发开启和关闭激活状态,在这当中不应该有波纹效果。在Lollipop中拥有自带的Material Design是非常好的:一个状态动画列表去把选择的item在translationZ中提升。

使用原生Android API实现这样的效果,这样做要比使用状态列表drawable和animator更明智。你需要的视图需要有两种不同的状态:其中一个使用默认的drawable和animator集合,另一个专为选择提供不同的集合(and one in which it uses a different set exclusively for selection)。像这样:

SwappingHolder

这是我写到应用中的第二个工具:一个名叫SwappingHolder的ViewHolder子类,它需要做的工作就像我之前描述的那样。SwappingHolder实现正常的ViewHolder功能并增加了六个属性:

public Drawable getSelectionModeBackgroundDrawable(); public Drawable getDefaultModeBackgroundDrawable(); public StateListAnimator getSelectionModeStateListAnimator(); public StateListAnimator getDefaultModeStateListAnimator(); public boolean isSelectable(); public boolean isActivated();

当你第一次创建它的时候,SwappingHolder将会忽略它的itemView的背景drawable和状态列表

animator,并把这些初始化值存贮在defaultModeBackgroundDrawable和defaultModeStateListAnimator。如果你设置selectable为true,则它将会切换到这两个属性的选择模式。把selectable设置为false,将会重新设置为默认值。那么激活状态呢?它会调用itemView的激活属性。

长话短说,当被选择的item被激活时,SwappingHolder使用selectionModelStateListAnimator把这个item抬高一些。并且,selectionModeBackgroundDrawable使用appcompate Material主题中的colorAccent属性。

所以使用这个。最后一点,为选择逻辑提供一种方便打开关闭的方式钩住一切。

连接选择逻辑

重复一遍,如果你喜欢你可以自己实现。这里需要两步:当绑定crime时更新ViewHolder,并且增加点击事件。绑定crime时更新,并在bindCrime()中添加更多的代码:

private class CrimeHolder extends SwappingHolder { ... public void bindCrime(Crime crime) { mCrime = crime; mSolvedCheckBox.setChecked(crime.isSolved()); setSelectable(mMultiSelector.isSelectable()); setActivated(mMultiSelector.isSelected(getPosition())); } }

所以当你每次把你的ViewHolder绑定到另一个crime时,你需要两次检查来确定:第一,当前是否在选择状态;第二,绑定的item是否被选择了。

然后绑定一个点击监听事件:

private class CrimeHolder extends SwappingHolder implements View.OnClickListener { ... public CrimeHolder(View itemView) { super(itemView); mSolvedCheckBox = (CheckBox) itemView .findViewById(R.id.crime_list_item_solvedCheckBox); itemView.setOnClickListener(this); } @Override public void onClick(View view) { if (mMultiSelector.isSelectable()) { // Selection is active; toggle activation setActivated(!isActivated()); mMultiSelector.setSelected(getPosition(), isActivated()); } else { // Selection not active } } }

对于单选,onClick()的实现要比这个复杂,因为它需要在点击一个时把其它的选项取消。

这并不是完整的代码,但是你需要在用的时候自己实现。我已经在MultiSelector中做一些工作,可以代替样板。

打开关闭一切

最后一步:打开关闭它。你必须为CHOICE_MODE_MULTIPLE_MODAL做这些,当你需要别的选择模式时你同样要去实现。

添加notifyDataSetChanged()是最简单的增强你的setSelectable()的方法:

public void setSelectable(boolean isSelectable) { mIsSelectable = isSelectable; mRecyclerView.getAdapter().notifyDataSetChanged(); }

在ListView(和ViewPager)中当你感得你做错时使用notifyDataSetChanged()往往是最好的解决办法。在RecyclerView中我也推荐你使用同样的方法。

这是原因:使用RecyclerView最大的原因是它能很容易的激活更改列表内容。例如,你想要删除列表中第一个crime,你可以这样做:

// Delete the 0th crime from your model mCrimes.remove(0); // Notify the adapter that it was removed mRecyclerView.getAdapter().notifyItemRemoved(0);

调用notifyDataSetChanged()可以打破这些,因为它能中断那些动画。

RecyclerView中的ItemAnimator将会为你推动这变化。默认的动画会使用item0淡出,然后另一个item进入。

如果你在使用itemAnimator之后立即调用notifyDataSetChanged()会发生什么?它将会杀死所有的即将发生的动画,重新查询适配器并重新展示一切。并且立即见效。通常那是正确的选择,但是注意:如果你可以使用除了notifyDataSetChanged之外的方法更新你的列表,去做!

那么其它的实现方式是怎么样的?像这样:

public void setSelectable(boolean isSelectable) { mIsSelectable = isSelectable; for (int i = 0; i < mRecyclerView.getAdapter().getItemCount(); i++) { RecyclerView.ViewHolder holder = mRecyclerView.findViewHolderForPosition(i); if (holder != null) { ((SwappingHolder)holder).setSelectable(isSelectable); } } }

我们可以遍历所有的ViewHolder,强制转化为SwappingHolder然后告诉它们现在的状态是什么。

像SwappingHolder,MultiSelector己经为你做了。MultiSelector知道哪一个ViewHolder被选择了,所以你所需要做的就是更新你的用户接口:

mMultiSelector.setSelectable(true);

使用上下文操作模式

当实现了setSelecteable(),你可以使用常用的ActionMode.Callback实现其余的CHOICE_MODE_MULTIPLE_MODAL。从相关的回调方法中调用你的setSelectable()。

private ActionMode.Callback mDeleteMode = new ActionMode.Callback() { @Override public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) { setSelectable(true); return false; } @Override public void onDestroyActionMode(ActionMode actionMode) { setSelectable(false); } @Override public boolean onCreateActionMode(ActionMode actionMode, Menu menu) { ... } @Override public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) { ... } }

然后通过长按监听打开action mode:

private class CrimeHolder extends SwappingHolder implements View.OnClickListener, View.OnLongClickListener { ... public CrimeHolder(View itemView) { ... itemView.setOnClickListener(this); itemView.setOnLongClickListener(this); itemView.setLongClickable(true); } @Override public boolean onLongClick(View v) { ActionBarActivity activity = (ActionBarActivity)getActivity(); activity.startSupportActionMode(deleteMode); setSelected(this, true); return true; } }

TL;DR:通过一个Library实现Choice Mode

现在实现了MultiSelect。如果你不在乎,你更喜欢选择一种更直接的实现方案。

我注意到一个现成的解决方案: 实现的library,叫做。我没有足够的时间研究其中的细节,但是我可以告诉你它复制了ListView中的setChoiceMode()方法,还有其它的一些方法。对于那些想用RecyclerView来代替ListVIew的人们来说,TwoWayView是一个非常棒的解决方案。如果你喜欢用,我遵从他们的文档。

当然,这时候我的同事告诉我这个,我已经实现了自己的多选,但那看起来很难。或许你会发现它有用。我会尝试实现一些更小、专注、灵活易用的代码。这并没有很多代码,只有有限的几个明智选择使用“魔法”。这是它如何实现的。

MultiSelector:基础

第一步,引入library。在你的build.gradle中加入下面这一行:

compile 'com.bignerdranch.android:recyclerview-multiselect:+'

(你可以在上找到工程,和它的)

第二步,创建一个MultiSelector实例。在我的示例app中,我在Fragment中实现:

public class CrimeListFragment extends Fragment { private MultiSelector mMultiSelector = new MultiSelector(); ... }

MultiSelector知道哪一个item被选择了,它同样是你控制item选择的接口,这个接口访问绑定的一切( and is also your interface for controlling item selection across everything it is hooked up to)。这种情况下,所有的一切都在适配器中。

为MultiSelector连接一个SwappingHolder,在构造函数传入MultiSelector,并且使用点击监听器调用MultiSelector.tapSelection():

private class CrimeHolder extends SwappingHolder implements View.OnClickListener, View.OnLongClickListener { private final CheckBox mSolvedCheckBox; private Crime mCrime; public CrimeHolder(View itemView) { super(itemView, mMultiSelector); mSolvedCheckBox = (CheckBox) itemView.findViewById(R.id.crime_list_item_solvedCheckBox); itemView.setOnClickListener(this); } @Override public void onClick(View v) { if (mCrime == null) { return; } if (!mMultiSelector.tapSelection(this)) { // start an instance of CrimePagerActivity Intent i = new Intent(getActivity(), CrimePagerActivity.class); i.putExtra(CrimeFragment.EXTRA_CRIME_ID, c.getId()); startActivity(i); } } }

MultiSelector.tapSelection()模拟点击一个选中的item;如果MultiSelector是在选择模式,它会返回true并且触发该item的选择。如果不是,它将返回false,并且不做任何事情。

打开多选模式,可以调用setSelectable(true):

mMultiSelector.setSelectable(true);

这将会触发MultiSelector上的标志,开启它和它所有的SwappingHolder。这是SwappingHolder为你做的一切——它扩展了MultiSelectorBindingHolder,并把自己绑定到你的MultiSelector上。

对于基本的多选,这就是所有需要做的工作。当你需要知道是否要选择一个item时,问问multiselector:

for (int i = mCrimes.size(); i > 0; i--) { if (mMultiSelector.isSelected(i, 0)) { Crime crime = mCrimes.get(i); CrimeLab.get(getActivity()).deleteCrime(crime); mRecyclerView.getAdapter().notifyItemRemoved(i); } }

单选

使用单选代替多选,使用SingleSelector代替MultiSelector:

public class CrimeListFragment extends Fragment { private MultiSelector mMultiSelector = new SingleSelector(); ... }

通过长按模式化多选

获得如果CHOICE_MODE_MULTIPLE_MODAL一样的效果,你同样可以向上面描述的那样实现自己的ActionMode.Callback,或者使用提供的抽象实现——ModalMultiSelectorCallback:

private ActionMode.Callback mDeleteMode = new ModalMultiSelectorCallback(mMultiSelector) { @Override public boolean onCreateActionMode(ActionMode actionMode, Menu menu) { getActivity().getMenuInflater().inflate(R.menu.crime_list_item_context, menu); return true; } @Override public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) { switch (menuItem.getItemId()) { case R.id.menu_item_delete_crime: // Delete crimes from model mMultiSelector.clearSelections(); return true; default: break; } return false; } };

ModalMultiSelectorCallback在onPrepareActionMode下将会调用MultiSelector.setSelectable(true)和clearSelections(),在onDestroyActionMode下调用setSelectable(false)。在长按监听器中像其它的action mode那样踢开它。

private class CrimeHolder extends SwappingHolder implements View.OnClickListener, View.OnLongClickListener { public CrimeHolder(View itemView) { ... itemView.setOnLongClickListener(this); itemView.setLongClickable(true); } @Override public boolean onLongClick(View v) { ActionBarActivity activity = (ActionBarActivity)getActivity(); activity.startSupportActionMode(mDeleteMode); mMultiSelector.setSelected(this, true); return true; } }

自定义选择视觉效果

SwappingDrawable为它的itemView提供了两套drawable和状态列表动画:一种是在默认模式下使用,另一种在选择模式下使用。你可以通过调用下面的方法自定义:

public void setSelectionModeBackgroundDrawable(Drawable drawable); public void setDefaultModeBackgroundDrawable(Drawable drawable); public void setSelectionModeStateListAnimator(int resId); public void setDefaultModeStateListAnimator(int resId);

这些状态列表动画设置函数在API 21以下调用也是安全的,并且将返回空操作。

定制关闭标签

如果你需要定制比SwappingHolder提供好的选择状态效果,你可以扩展MultiSelectorBindingHolder抽象类:

public class MyCustomHolder extends MultiSelectorBindingHolder { @Override public void setSelectable(boolean selectable) { ... } @Override public boolean isSelectable() { ... } @Override public void setActivated(boolean activated) { ... } @Override public boolean isActivated() { ... } }

如果这样提供的相同方法还是太局限,你可以实现SelectableHolder接口代替。它需要更多的代码:你将需要在每次调用mMultiSelector.bindHolder()时绑定你的ViewHolder到MultiSelector当onBindViewHolder被调用的时候。

足够了吗?

这篇文章中我们学习了在RecyclerView中选择item。现在你知道了怎么去显示哪个视图是被选择和未选择的,在列表中跟踪被选择和未被选择的状态,在一个上下文action mode中关闭和打开所有东西

  • 转载自 
你可能感兴趣的文章
TestLink运行环境:Redhat5+Apache2.2.17+php-5.3.5+MySQL5.5.9-1
查看>>
Get File Name from File Path in Python | Code Comments
查看>>
显示本月每一天日期
查看>>
[转]java 自动装箱与拆箱
查看>>
NET的堆和栈04,对托管和非托管资源的垃圾回收以及内存分配
查看>>
think in coding
查看>>
IdHttpServer实现webservice
查看>>
HTML的音频和视频
查看>>
Unsupported major.minor version 52.0
查看>>
面对对象之差异化的网络数据交互方式--单机游戏开发之无缝切换到C/S模式
查看>>
优酷网架构学习笔记
查看>>
把HDFS里的json数据转换成csv格式
查看>>
WEEX-EROS | 集成并使用 bindingx
查看>>
广州牵引力来告诉你学编程先学什么语言好?
查看>>
广州牵引力总结初学者怎样学好UI设计?
查看>>
使用Metrics方法级远程监控Java程序
查看>>
Spring核心系列之Bean的生命周期
查看>>
VasSonic源码之并行加载
查看>>
小程序 LRU 存储设计
查看>>
Android 多线程之阻塞队列
查看>>