閱讀714 返回首頁    go 阿裏雲 go 技術社區[雲棲]


RecyclerView Part 2:選擇模式

一位非常有名的人曾經說過,

此生的事情永遠比後世還容易。因為,此生自己做主。

這是真的嗎?或許這值的去討論。當去選擇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中關閉和打開所有東西

最後更新:2017-05-23 10:02:33

  上一篇:go  Java 7與偽共享的新仇舊恨
  下一篇:go  Netty源碼注釋翻譯-Channel類