RecyclerView Part 2:选择模式


原文链接    作者:Bill Phillips    译者:赵峰

不要错过第一部分,地址是:为ListView专家写的基础

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

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

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

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

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

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

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

01 listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
02  
03  listView.setMultiChoiceModeListener(new MultiChoiceModeListener() {
04  public boolean onCreateActionMode(ActionMode mode, Menu menu) { ... }
05  public void onItemCheckedStateChanged(ActionMode mode, int position,
06  long id, boolean checked) { ... }
07  public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
08  switch (item.getItemId()) {
09  case R.id.menu_item_delete_crime:
10  CrimeAdapter adapter = (CrimeAdapter)getListAdapter();
11  CrimeLab crimeLab = CrimeLab.get(getActivity());
12  for (int i = adapter.getCount() - 1; i >= 0; i--) {
13  if (getListView().isItemChecked(i)) {
14  crimeLab.deleteCrime(adapter.getItem(i));
15  }
16  }
17  mode.finish();
18  adapter.notifyDataSetChanged();
19  return true;
20  default:
21  return false;
22  }
23  public boolean onPrepareActionMode(ActionMode mode, Menu menu) { ... }
24  public void onDestroyActionMode(ActionMode mode) { ... }
25  });

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

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

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

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

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

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

保持跟踪状态

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

1 // Check item 0
2 mListView.setItemChecked(0true);
3  
4 // Returns true
5 mListView.isItemChecked(0);
6  
7 // Says what the choice mode currently is
8 mListView.getChoiceMode();

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

01 private SparseBooleanArray mSelectedPositions = new SparseBooleanArray();
02  private mIsSelectable = false;
03  
04  private void setItemChecked(int position, boolean isChecked) {
05  mSelectedPositions.put(position, isChecked);
06  }
07  
08  private boolean isItemChecked(int position) {
09  return mSelectedPositions.get(position);
10  }
11  
12  private void setSelectable(boolean selectable) {
13  mIsSelectable = selectable;
14  }
15  
16  private boolean isSelectable() {
17  return mIsSelectable;
18  }

现在程序不会像ListView.setItemChecked()那样更新用户接口,但它现在将会那样做。
当然,你可以用自己喜欢的方式去追踪。对象集合是一个不错的选择。
我把这个想法放到一个叫做MultiSelector的对象中:

1 MultiSelector selector = new MultiSelector();
2  selector.setSelected(0true);
3  selector.isSelected(0);
4  selector.setSelectable(true);
5  selector.isSelectable();

显示选项状态

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

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

01 private class CrimeHolder extends ViewHolder {
02  ...
03  public void bindCrime(Crime crime) {
04  mCrime = crime;
05  mSolvedCheckBox.setChecked(crime.isSolved());
06  
07  boolean isSelected = mMultiSelector.isSelected(getPosition());
08  itemView.setActivated(isSelected);
09  }
10  }

当然,如果你想用其它方式实现选择,你可以。你潜力无限。尽管,Drawable和state list动画做激活状态是默认的好选择。
如果仅仅是这些,我就不用花费那么多时间了。但是我花费了那么多时间,因为我固执的要实现一些我想要的视觉效果。

Material animations

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

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

le-drawables

你每次点击它的时候选择中状态都会有反应。所以,当你点击视图关闭激活状态时,你同样会得到波纹效果。这对我没有意义。在我心里,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)。像这样:

lection-view

SwappingHolder

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

1 public Drawable getSelectionModeBackgroundDrawable();
2  public Drawable getDefaultModeBackgroundDrawable();
3  
4  public StateListAnimator getSelectionModeStateListAnimator();
5  public StateListAnimator getDefaultModeStateListAnimator();
6  
7  public boolean isSelectable();
8  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()中添加更多的代码:

01 private class CrimeHolder extends SwappingHolder {
02  ...
03  
04  public void bindCrime(Crime crime) {
05  mCrime = crime;
06  mSolvedCheckBox.setChecked(crime.isSolved());
07  
08  setSelectable(mMultiSelector.isSelectable());
09  setActivated(mMultiSelector.isSelected(getPosition()));
10  }
11  }

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

01 private class CrimeHolder extends SwappingHolder
02  implements View.OnClickListener {
03  ...
04  
05  public CrimeHolder(View itemView) {
06  super(itemView);
07  
08  mSolvedCheckBox = (CheckBox) itemView
09  .findViewById(R.id.crime_list_item_solvedCheckBox);
10  itemView.setOnClickListener(this);
11  }
12  
13  @Override
14  public void onClick(View view) {
15  if (mMultiSelector.isSelectable()) {
16  // Selection is active; toggle activation
17  setActivated(!isActivated());
18  mMultiSelector.setSelected(getPosition(), isActivated());
19  else {
20  // Selection not active
21  }
22  }
23  }

对于单选,onClick()的实现要比这个复杂,因为它需要在点击一个时把其它的选项取消。
这并不是完整的代码,但是你需要在用的时候自己实现。我已经在MultiSelector中做一些工作,可以代替样板。

打开关闭一切

最后一步:打开关闭它。你必须为CHOICE_MODE_MULTIPLE_MODAL做这些,当你需要别的选择模式时你同样要去实现。
添加notifyDataSetChanged()是最简单的增强你的setSelectable()的方法:

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

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

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

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

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

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

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

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

1 public void setSelectable(boolean isSelectable) {
2  mIsSelectable = isSelectable;
3  for (int i = 0; i < mRecyclerView.getAdapter().getItemCount(); i++) {
4  RecyclerView.ViewHolder holder = mRecyclerView.findViewHolderForPosition(i);
5  if (holder != null) {
6  ((SwappingHolder)holder).setSelectable(isSelectable);
7  }
8  }
9  }

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

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

1 mMultiSelector.setSelectable(true);

使用上下文操作模式

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

01 private ActionMode.Callback mDeleteMode = new ActionMode.Callback() {
02  @Override
03  public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
04  setSelectable(true);
05  return false;
06  }
07  
08  @Override
09  public void onDestroyActionMode(ActionMode actionMode) {
10  setSelectable(false);
11  }
12  
13  @Override
14  public boolean onCreateActionMode(ActionMode actionMode, Menu menu) { ... }
15  
16  @Override
17  public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) { ... }
18  }

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

01 private class CrimeHolder extends SwappingHolder
02  implements View.OnClickListener, View.OnLongClickListener {
03  
04  ...
05  
06  public CrimeHolder(View itemView) {
07  
08  ...
09  
10  itemView.setOnClickListener(this);
11  itemView.setOnLongClickListener(this);
12  itemView.setLongClickable(true);
13  }
14  
15  @Override
16  public boolean onLongClick(View v) {
17  
18  ActionBarActivity activity = (ActionBarActivity)getActivity();
19  activity.startSupportActionMode(deleteMode);
20  setSelected(thistrue);
21  return true;
22  }
23  }

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

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

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

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

MultiSelector:基础

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

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

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

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

1 public class CrimeListFragment extends Fragment {
2  private MultiSelector mMultiSelector = new MultiSelector();
3  
4  ...
5  }

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

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

01 private class CrimeHolder extends SwappingHolder
02  implements View.OnClickListener, View.OnLongClickListener {
03  private final CheckBox mSolvedCheckBox;
04  private Crime mCrime;
05  
06  public CrimeHolder(View itemView) {
07  super(itemView, mMultiSelector);
08  
09  mSolvedCheckBox = (CheckBox) itemView.findViewById(R.id.crime_list_item_solvedCheckBox);
10  itemView.setOnClickListener(this);
11  }
12  
13  @Override
14  public void onClick(View v) {
15  if (mCrime == null) {
16  return;
17  }
18  if (!mMultiSelector.tapSelection(this)) {
19  // start an instance of CrimePagerActivity
20  Intent i = new Intent(getActivity(), CrimePagerActivity.class);
21  i.putExtra(CrimeFragment.EXTRA_CRIME_ID, c.getId());
22  startActivity(i);
23  }
24  }
25  }

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

1 mMultiSelector.setSelectable(true);

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

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

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

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

1 public class CrimeListFragment extends Fragment {
2  private MultiSelector mMultiSelector = new SingleSelector();
3  
4  ...
5  }

通过长按模式化多选

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

01 private ActionMode.Callback mDeleteMode = newModalMultiSelectorCallback(mMultiSelector) {
02  @Override
03  public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
04  getActivity().getMenuInflater().inflate(R.menu.crime_list_item_context, menu);
05  return true;
06  }
07  
08  @Override
09  public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
10  switch (menuItem.getItemId()) {
11  case R.id.menu_item_delete_crime:
12  // Delete crimes from model
13  
14  mMultiSelector.clearSelections();
15  return true;
16  
17  default:
18  break;
19  }
20  return false;
21  }
22  };

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

01 private class CrimeHolder extends SwappingHolder
02  implements View.OnClickListener, View.OnLongClickListener {
03  
04  public CrimeHolder(View itemView) {
05  
06  ...
07  
08  itemView.setOnLongClickListener(this);
09  
10  itemView.setLongClickable(true);
11  }
12  
13  @Override
14  public boolean onLongClick(View v) {
15  
16  ActionBarActivity activity = (ActionBarActivity)getActivity();
17  
18  activity.startSupportActionMode(mDeleteMode);
19  mMultiSelector.setSelected(thistrue);
20  return true;
21  }
22  }

自定义选择视觉效果

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

1 public void setSelectionModeBackgroundDrawable(Drawable drawable);
2  public void setDefaultModeBackgroundDrawable(Drawable drawable);
3  public void setSelectionModeStateListAnimator(int resId);
4  public void setDefaultModeStateListAnimator(int resId);

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

定制关闭标签

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

01 public class MyCustomHolder extends MultiSelectorBindingHolder {
02  @Override
03  public void setSelectable(boolean selectable) { ... }
04  
05  @Override
06  public boolean isSelectable() { ... }
07  
08  @Override
09  public void setActivated(boolean activated) { ... }
10  
11  @Override
12  public boolean isActivated() { ... }
13  }

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

足够了吗?

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

原创文章,转载请注明: 转载自并发编程网 – ifeve.com本文链接地址: RecyclerView Part 2:选择模式

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值