RecyclerView Part 2:选择模式

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

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

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

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

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

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

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

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

我打算实现像Android Programming书中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 0
mListView.setItemChecked(0, true);

// Returns true
mListView.isItemChecked(0);

// Says what the choice mode currently is
mListView.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包括这种非常酷的波纹动画。如果你在 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功能并增加了六个属性:

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。如果你不在乎,你更喜欢选择一种更直接的实现方案。

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

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

MultiSelector:基础

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

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

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

第二步,创建一个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中关闭和打开所有东西。

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

FavoriteLoading添加本文到我的收藏
  • Trackback are closed
  • Comments (1)
    • xesam
    • 02/06. 2015 10:46am

    本站的代码显示模块应该修改一下了…
    代码格式不能直视

You must be logged in to post a comment.

return top