轉自: http://www.tuicool.com/articles/aANfie
這篇文章我們將詳細的介紹如何實現ListView的多選操作,文中將會糾正在使用ListViewCHOICE_MODE_MULTIPLE或者CHOICE_MODE_MULTIPLE_MODAL時容易犯的錯誤,以及
CHOICE_MODE_MULTIPLE與CHOICE_MODE_MULTIPLE_MODAL的區別。最後我們將給出一個demo來演示兩種多選操作的實現。
一、在不使用 ListView 多選模式的情況下
注:我認爲這一節可以不看,因爲我覺得不使用ListView的多選模式有點愚蠢。
如果我們不知道ListView自帶多選模式,那麼我們一般是通過維護一個保存被選擇position集合來實現多選的,通常情況下這個集合類型我們選擇HashSet。
實現的大致框架如下:
Adapter中:
保存被選擇的position
public HashSet<Long> selectedItems = new HashSet<Long>();
getView中判斷當前Position是否在集合中,從而顯示不同的外觀
public View getView(int position, View convertView, ViewGroup par) { ...... if(selectedItems.contains((long)position)){ holder.cBox.setChecked(true); }else{ holder.cBox.setChecked(false); } if(selectedMode==AppContext.MULTI_SELECTED){ holder.cBox.setVisibility(View.VISIBLE); holder.check_box_wraper.setVisibility(View.VISIBLE); }else{ holder.cBox.setVisibility(View.GONE); holder.check_box_wraper.setVisibility(View.GONE); } ..... }
Activity中:
主要是處理onItemClick事件,在不同模式下,做不同的處理。
@Override
public void onItemClick(AdapterView<?> a, View v, int position, long id) {
//普通模式 :直接打開一個activity
if(itemClickActionMode==AppContext.VIEW_NOTE){
Long mId=Long.parseLong(idText.getText().toString());
Uri uri = ContentUris.withAppendedId(getIntent().getData(), mId);
startActivity(new Intent(Intent.ACTION_VIEW, uri));
}
//多選模式:更新adapter中selectedItems 集合的值,同時 讓adapter在getView中改變item的外觀。
else{
ViewHolder vHollder = (ViewHolder) v.getTag();
if(mAdapter.selectedItems.contains((long)position)){
mAdapter.selectedItems.remove((long)position);
}else{
mAdapter.selectedItems.add((long)position);
}
mAdapter.notifyDataSetChanged();
onItemSelected(getSelectedCount());
}
}
上面的做法其實用的很普遍。但是我們不提倡。
二、使用 ListViiew 的 CHOICE_MODE_MULTIPLE 模式
ListView有四種模式:
/**
* Normal list that does not indicate choices
*/
public static final int CHOICE_MODE_NONE = 0;
/**
* The list allows up to one choice
*/
public static final int CHOICE_MODE_SINGLE = 1;
/**
* The list allows multiple choices
*/
public static final int CHOICE_MODE_MULTIPLE = 2;
/**
* The list allows multiple choices in a modal selection mode
*/
public static final int CHOICE_MODE_MULTIPLE_MODAL = 3;
其中 CHOICE_MODE_NONE
是普通模式, CHOICE_MODE_SINGLE
是單選模式,不常用, CHOICE_MODE_MULTIPLE
和 CHOICE_MODE_MULTIPLE_MODAL
都是多選模式,他們的區別稍後我們會講到。
所以ListView在設計的時候其實是考慮了多選操作的,我們沒有必要自己再像第一節描述的那樣專門維護一個HashSet來保存被選擇的position。實現ListView的多選操作的代碼在ListView直接父類AbsListView中,AbsListView已經有一個mCheckStates變量來做了保存被選擇的position這個事情。mCheckStates的定義如下:
1
|
SparseBooleanArray mCheckStates; |
AbsListView 還定義瞭如下公共方法:
// 判斷一個 item 是否被選中
1
|
public boolean isItemChecked(int
position); |
// 獲得被選中 item 的總數
1
|
public int getCheckedItemCount(); |
// 選中一個 item
1
|
public void setItemChecked(int position,
boolean value); |
// 清除選中的 item
1
|
public void clearChoices(); |
當點擊一個item的時候absListView中會調用performItemClick,如果是CHOICE_MODE_MULTIPLE,則該item點擊一次,mCheckStates中相應位置的狀態變更一次。然後我們就可以通過listView的getCheckedItemCount()方法獲取選擇了多少個;isItemChecked(int position)方法判斷一個item是不是被選中。
有了這些原生sdk的支持,難道還有什麼多選操作是不能實現的嗎?所以是不是應該考慮放棄第一節中描述的那種方法了呢?遺憾的是很多android開發者即使是用了CHOICE_MODE_MULTIPLE,仍然沒有去利用這些ListView自帶的功能,估計是根本不知道該CHOICE_MODE_MULTIPLE的 特性吧,這其實也是android程序員與ios程序員真正存在差距的地方。
CHOICE_MODE_MULTIPLE實戰
先看看效果圖
package com.example.listmultichoise;
import android.os.Bundle;
import android.app.ActionBar;
import android.app.Activity;
import android.util.Log;
import android.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
public class ChoiceModeMultipleActivity extends Activity {
ListView mListView = null;
MyListAdapter mAdapter;
private View mMultiSelectActionBarView;
private TextView mSelectedCount;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
setContentView(R.layout.activity_list);
mListView = (ListView)findViewById(R.id.list);
mAdapter = new MyListAdapter(this,mListView);
mListView.setAdapter(mAdapter);
mListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
mListView.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
mAdapter.notifyDataSetChanged();
updateSeletedCount();
}
});
if (mMultiSelectActionBarView == null) {
mMultiSelectActionBarView = LayoutInflater.from(ChoiceModeMultipleActivity.this)
.inflate(R.layout.list_multi_select_actionbar, null);
mSelectedCount =
(TextView)mMultiSelectActionBarView.findViewById(R.id.selected_conv_count);
}
getActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM,
ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_HOME |
ActionBar.DISPLAY_SHOW_TITLE);
getActionBar().setCustomView(mMultiSelectActionBarView);
((TextView)mMultiSelectActionBarView.findViewById(R.id.title)).setText(R.string.select_item);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.multi_select_menu, menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
MenuItem mItem = menu.findItem(R.id.action_slelect);
if(mListView.getCheckedItemCount() == mAdapter.getCount()){
mItem.setTitle(R.string.action_deselect_all);
}else{
mItem.setTitle(R.string.action_select_all);
}
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_slelect:
if(mListView.getCheckedItemCount() == mAdapter.getCount()){
unSelectedAll();
}else{
selectedAll();
}
mAdapter.notifyDataSetChanged();
break;
default:
break;
}
return super.onOptionsItemSelected(item);
}
public void selectedAll(){
for(int i= 0; i< mAdapter.getCount(); i++){
mListView.setItemChecked(i, true);
}
updateSeletedCount();
}
public void unSelectedAll(){
mListView.clearChoices();
updateSeletedCount();
}
public void updateSeletedCount(){
mSelectedCount.setText(Integer.toString(mListView.getCheckedItemCount()));
}
}
代碼解釋:
首先設置ListView模式:
mListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
定義一個adapter,當ListView的某個item被選中之後,將該Item的背景設置爲藍色,以標記爲選中。不然雖然ListView知道該item被選中,但是界面上沒表現出來。
...... public View getView(int position, View convertView, ViewGroup parent) { TextView tv; if (convertView == null) { tv = (TextView) LayoutInflater.from(mContext).inflate( android.R.layout.simple_expandable_list_item_1, parent, false); } else { tv = (TextView) convertView; } tv.setText(mStrings[position]); updateBackground(position , tv); return tv; } @SuppressLint("NewApi") public void updateBackground(int position, View view) { int backgroundId; if (mListView.isItemChecked(position)) { backgroundId = R.drawable.list_selected_holo_light; } else { backgroundId = R.drawable.conversation_item_background_read; } Drawable background = mContext.getResources().getDrawable(backgroundId); view.setBackground(background); } ......
在item每被點擊一次中通知adapter,這樣做的目的是爲了讓更新Ui以顯示最新的選中狀態。
mListView.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
mAdapter.notifyDataSetChanged();
updateSeletedCount();
}
});
其中mSelectedCount()作用是在actionbar中更新選中的數目。
public void updateSeletedCount(){ mSelectedCount.setText(Integer.toString(mListView.getCheckedItemCount())); }
上面的代碼實現了多選操作,但是在我選中一個item的時候,listView的onItemClick也同時觸發,而一個ListView點擊item的後續操作一般是切換到另外一個界面,所以實際應用中,我們還需要設置一個標誌位,用來區別當前是多選狀態還是普通狀態 ,如果是多選狀態,調用ListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); 如果是普通狀態調用mListView.setChoiceMode(ListView.CHOICE_MODE_NONE); CHOICE_MODE_MULTIPLE模式的特點在於他本身沒有排斥性,在能選擇item的情況下,也可以響應普通點擊事件 。爲了解決這個問題 ,在android3.0之後增加了CHOICE_MODE_MULTIPLE_MODAL模式。
二、使用 ListViiew 的 CHOICE_MODE_MULTIPLE 模式
CHOICE_MODE_MULTIPLE_MODAL和CHOICE_MODE_MULTIPLE恰恰相反,他是對普通點擊操作和多選操作是排斥的, 一旦有一個item被選中,即進入到多選狀態 ,item的onclick事件被屏蔽。這種排斥性也是他比CHOICE_MODE_MULTIPLE多了個MODAL的原因 。此外CHOICE_MODE_MULTIPLE_MODAL還結合了android3.0的actionmode,當進入多選狀態,actionbar的位置會顯示新的菜單。
我們來看看CHOICE_MODE_MULTIPLE_MODAL模式的實現原理:
如何實現兩種狀態的互斥:當點擊一個item的時候absListView中會調用performItemClick,如果是CHOICE_MODE_MULTIPLE,則該item點擊一次,mCheckStates中相應位置的狀態變更一次,但是CHOICE_MODE_MULTIPLE_MODAL模式不同,必須要mChoiceActionMode!= null
的情況下,纔會去變更mCheckStates中相應位置的狀態;不光如此,如果mChoiceActionMode!= null
,他還會阻擋ItemClick事件的繼續傳播,從而屏蔽了ListView OnItemClickListener的onItemClick方法。
如何啓用actionmode:一般我們使用actionmode都是在activity中調用startActionMode,但是如果你要使用ListView的CHOICE_MODE_MULTIPLE_MODAL,請不要這麼做, 在absListView中有一個變量mChoiceActionMode,定義如下:
ActionMode mChoiceActionMode;
當長按item 或者是調用主動調用setItemChecked方法mChoiceActionMode將被實例化,而如果你是在activity中調用startActionMode,那麼雖然actionbar上的菜單變化了,ListView 中的mChoiceActionMode卻沒有實例化,剛剛我們談到mChoiceActionMode==null 表示未進入到多選狀態,所以這時你點擊一個item其實還是普通的點擊行爲。
因此在CHOICE_MODE_MULTIPLE_MODAL模式下要啓用多選操作,只有兩種辦法:
(1)長按當長按item ;
(2)主動調用ListView的setItemChecked(int position, boolean value)方法選中一個item。
但是這兩種進入多選狀態的方法都有一個弊端,那就是進入多選狀態之後,總是有一個item是被選中的, 方法(1)中長按item,被按的item被選中,這種結果是合理的可以接受的,但是如果你想主動進入多選狀態(比如我在點擊actionbar的某個菜單的時候想進入多選狀態),就必須採用方法(2):調用setItemChecked,這就出現個問題,你該讓哪個item被選中呢?貌似最合理的該是一個都不選中吧,我只是進入到這個狀態,還沒有開始選呢。幸運的是,我們可以使用一些技巧,實現能主動進入多選狀態,且沒有一個item被選中。
思路是我們先讓第一個item被選中,這樣Listview就進入多選狀態,然後我們再清除被選中item的狀態,代碼如下:
if(item.getItemId() == R.id.action_choice){ mListView.setItemChecked(0,true); mListView.clearChoices(); }
有些人可能會問,按照上面的思路,爲什麼不這樣實現呢:
if(item.getItemId() == R.id.action_choice){ mListView.setItemChecked(0,true); mListView.setItemChecked(0,false); }
嘿嘿,剛剛我們提到ListView CHOICE_MODE_MULTIPLE_MODAL模式中,
一旦有一個item被選中,即進入到多選狀態
,而他還有個相反的特性,一旦所有的Item被主動的設置爲未選中,則退出多選狀態,mChoiceActionMode會調用自己的finish()方法。爲什麼呢?在MultiChoiceModeWrapper類中:
@Override public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { mWrapped.onItemCheckedStateChanged(mode, position, id, checked); // If there are no items selected we no longer need the selection mode. if (getCheckedItemCount() == 0) { mode.finish(); } }
好了我們來實現一個CHOICE_MODE_MULTIPLE_MODAL模式下的多選操作:
代碼:
package com.example.listmultichoise;
import android.os.Bundle;
import android.app.Activity;
import android.util.Log;
import android.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
public class ChoiceModeMultipleModalActivity extends Activity {
ListView mListView = null;
MyListAdapter mAdapter;
ModeCallback mCallback;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_list);
mListView = (ListView)findViewById(R.id.list);
mAdapter = new MyListAdapter(this,mListView);
mListView.setAdapter(mAdapter);
mCallback = new ModeCallback();
mListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
mListView.setMultiChoiceModeListener(mCallback);
mListView.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
Toast.makeText(ChoiceModeMultipleModalActivity.this, "選擇了一個item", 300).show();
}
});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if(item.getItemId() == R.id.action_choice){
//這裏使用了一點技巧來實現處於選中狀態 但是0個item 被選擇
mListView.setItemChecked(0,true);
mListView.clearChoices();
mCallback.updateSeletedCount();
}
return super.onOptionsItemSelected(item);
}
private class ModeCallback implements ListView.MultiChoiceModeListener {
private View mMultiSelectActionBarView;
private TextView mSelectedCount;
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// actionmode的菜單處理
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.multi_select_menu, menu);
if (mMultiSelectActionBarView == null) {
mMultiSelectActionBarView = LayoutInflater.from(ChoiceModeMultipleModalActivity.this)
.inflate(R.layout.list_multi_select_actionbar, null);
mSelectedCount =
(TextView)mMultiSelectActionBarView.findViewById(R.id.selected_conv_count);
}
mode.setCustomView(mMultiSelectActionBarView);
((TextView)mMultiSelectActionBarView.findViewById(R.id.title)).setText(R.string.select_item);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
if (mMultiSelectActionBarView == null) {
ViewGroup v = (ViewGroup)LayoutInflater.from(ChoiceModeMultipleModalActivity.this)
.inflate(R.layout.list_multi_select_actionbar, null);
mode.setCustomView(v);
mSelectedCount = (TextView)v.findViewById(R.id.selected_conv_count);
}
//更新菜單的狀態
MenuItem mItem = menu.findItem(R.id.action_slelect);
if(mListView.getCheckedItemCount() == mAdapter.getCount()){
mItem.setTitle(R.string.action_deselect_all);
}else{
mItem.setTitle(R.string.action_select_all);
}
return true;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.action_slelect:
if(mListView.getCheckedItemCount() == mAdapter.getCount()){
unSelectedAll();
}else{
selectedAll();
}
mAdapter.notifyDataSetChanged();
break;
default:
break;
}
return true;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
mListView.clearChoices();
}
@Override
public void onItemCheckedStateChanged(ActionMode mode,
int position, long id, boolean checked) {
updateSeletedCount();
mode.invalidate();
mAdapter.notifyDataSetChanged();
}
public void updateSeletedCount(){
mSelectedCount.setText(Integer.toString(mListView.getCheckedItemCount()));
}
}
public void selectedAll(){
for(int i= 0; i< mAdapter.getCount(); i++){
mListView.setItemChecked(i, true);
}
mCallback.updateSeletedCount();
}
public void unSelectedAll(){
mListView.clearChoices();
mListView.setItemChecked(0,false);
mCallback.updateSeletedCount();
}
}
這裏需要提醒的是雖然ListView的mActionMode我們不能直接操作,但是actionmode的回調方法是可以在activity中設置的:
mListView.setMultiChoiceModeListener(mCallback);
而且這個回調方法比一般的actionmode回調方法多了個onItemCheckedStateChanged
@Override
public void onItemCheckedStateChanged(ActionMode mode,
int position, long id, boolean checked) {
....
}