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)去實現支持激活狀態,但是它最終的結果看起來是這樣的:
你每次點擊它的時候選擇中狀態都會有反應。所以,當你點擊視圖關閉激活狀態時,你同樣會得到波紋效果。這對我沒有意義。在我心裏,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。如果你不在乎,你更喜歡選擇一種更直接的實現方案。
我注意到一個現成的解決方案: Lucas Rocha實現的library,叫做TwoWayView。我沒有足夠的時間研究其中的細節,但是我可以告訴你它複製了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中關閉和打開所有東西
最後更新:2017-05-23 10:02:33