这篇文章我们将详细的介绍如何实现ListView的多选操作,文中将会纠正在使用ListViewCHOICE_MODE_MULTIPLE或者CHOICE_MODE_MULTIPLE_MODAL时容易犯的错误,以及
CHOICE_MODE_MULTIPLE与CHOICE_MODE_MULTIPLE_MODAL的区别。最后我们将给出一个demo来演示两种多选操作的实现。
一、在不使用ListView多选模式的情况下
注:我认为这一节可以不看,因为我觉得不使用ListView的多选模式有点愚蠢。
如果我们不知道ListView自带多选模式,那么我们一般是通过维护一个保存被选择position集合来实现多选的,通常情况下这个集合类型我们选择HashSet。
实现的大致框架如下:
Adapter中:
保存被选择的position
1 | public HashSet<Long> selectedItems = new HashSet<Long>(); |
getView中判断当前Position是否在集合中,从而显示不同的外观
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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事件,在不同模式下,做不同的处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | @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有四种模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | /** * 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实战
先看看效果图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | 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模式:
1 | mListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); |
定义一个adapter,当ListView的某个item被选中之后,将该Item的背景设置为蓝色,以标记为选中。不然虽然ListView知道该item被选中,但是界面上没表现出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | ...... 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以显示最新的选中状态。
1 2 3 4 5 6 7 | mListView.setOnItemClickListener( new OnItemClickListener() { public void onItemClick(AdapterView<?> parent, View view, int position, long id) { mAdapter.notifyDataSetChanged(); updateSeletedCount(); } }); |
其中mSelectedCount()作用是在actionbar中更新选中的数目。
1 2 3 | 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,定义如下:
1 | 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的状态,代码如下:
1 2 3 4 | if (item.getItemId() == R.id.action_choice){ mListView.setItemChecked(0, true ); mListView.clearChoices(); } |
有些人可能会问,按照上面的思路,为什么不这样实现呢:
1 2 3 4 5 | 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类中:
1 2 3 4 5 6 7 8 9 | @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模式下的多选操作:
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | 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中设置的:
1 | mListView.setMultiChoiceModeListener(mCallback); |
而且这个回调方法比一般的actionmode回调方法多了个onItemCheckedStateChanged
1 2 3 4 5 | @Override public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { .... } |
demo我已经上传到了csdn:
这是我的博客链接 http://jcodecraeer.com/a/anzhuokaifa/androidkaifa/2014/1105/1906.html