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

