这篇文章我们将详细的介绍如何实现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. public View getView(int position, View convertView, ViewGroup par) {
  2. ......
  3. if(selectedItems.contains((long)position)){
  4. holder.cBox.setChecked(true);
  5. }else{
  6. holder.cBox.setChecked(false);
  7. }
  8. if(selectedMode==AppContext.MULTI_SELECTED){
  9. holder.cBox.setVisibility(View.VISIBLE);
  10. holder.check_box_wraper.setVisibility(View.VISIBLE);
  11. }else{
  12. holder.cBox.setVisibility(View.GONE);
  13. holder.check_box_wraper.setVisibility(View.GONE);
  14. }
  15. .....

Activity中:

主要是处理onItemClick事件,在不同模式下,做不同的处理。

  1. @Override
  2. public void onItemClick(AdapterView<?> a, View v, int position, long id) {
  3. //普通模式 :直接打开一个activity
  4. if(itemClickActionMode==AppContext.VIEW_NOTE){
  5. Long mId=Long.parseLong(idText.getText().toString());
  6. Uri uri = ContentUris.withAppendedId(getIntent().getData(), mId);
  7. startActivity(new Intent(Intent.ACTION_VIEW, uri));
  8. }
  9. //多选模式:更新adapter中selectedItems 集合的值,同时 让adapter在getView中改变item的外观。
  10. else{
  11. ViewHolder vHollder = (ViewHolder) v.getTag();
  12. if(mAdapter.selectedItems.contains((long)position)){                                                                                                                                                    mAdapter.selectedItems.remove((long)position);
  13. }else{
  14. mAdapter.selectedItems.add((long)position);
  15. }
  16. mAdapter.notifyDataSetChanged();
  17. onItemSelected(getSelectedCount());
  18. }
  19. }

上面的做法其实用的很普遍。但是我们不提倡。

二、使用ListViiew的CHOICE_MODE_MULTIPLE模式

ListView有四种模式:

  1. /**
  2. * Normal list that does not indicate choices
  3. */
  4. public static final int CHOICE_MODE_NONE = 0;
  5. /**
  6. * The list allows up to one choice
  7. */
  8. public static final int CHOICE_MODE_SINGLE = 1;
  9. /**
  10. * The list allows multiple choices
  11. */
  12. public static final int CHOICE_MODE_MULTIPLE = 2;
  13. /**
  14. * The list allows multiple choices in a modal selection mode
  15. */
  16. public static final int CHOICE_MODE_MULTIPLE_MODAL = 3;

其中CHOICE_MODE_NONE是普通模式,CHOICE_MODE_SINGLE是单选模式,不常用,CHOICE_MODE_MULTIPLECHOICE_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实战

 

先看看效果图

ListView多选操作模式详解CHOICE_MODE_MULTIPLE与CHOICE_MODE_MULTIPLE_MODAL-LMLPHP

  1. package com.example.listmultichoise;
  2. import android.os.Bundle;
  3. import android.app.ActionBar;
  4. import android.app.Activity;
  5. import android.util.Log;
  6. import android.view.ActionMode;
  7. import android.view.LayoutInflater;
  8. import android.view.Menu;
  9. import android.view.MenuInflater;
  10. import android.view.MenuItem;
  11. import android.view.View;
  12. import android.view.ViewGroup;
  13. import android.view.Window;
  14. import android.widget.AdapterView;
  15. import android.widget.AdapterView.OnItemClickListener;
  16. import android.widget.ListView;
  17. import android.widget.TextView;
  18. import android.widget.Toast;
  19. public class ChoiceModeMultipleActivity extends Activity {
  20. ListView mListView = null;
  21. MyListAdapter mAdapter;
  22. private View mMultiSelectActionBarView;
  23. private TextView mSelectedCount;
  24. @Override
  25. protected void onCreate(Bundle savedInstanceState) {
  26. super.onCreate(savedInstanceState);
  27. getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
  28. setContentView(R.layout.activity_list);
  29. mListView = (ListView)findViewById(R.id.list);
  30. mAdapter = new MyListAdapter(this,mListView);
  31. mListView.setAdapter(mAdapter);
  32. mListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
  33. mListView.setOnItemClickListener(new OnItemClickListener() {
  34. public void onItemClick(AdapterView<?> parent, View view, int position,
  35. long id) {
  36. mAdapter.notifyDataSetChanged();
  37. updateSeletedCount();
  38. }
  39. });
  40. if (mMultiSelectActionBarView == null) {
  41. mMultiSelectActionBarView = LayoutInflater.from(ChoiceModeMultipleActivity.this)
  42. .inflate(R.layout.list_multi_select_actionbar, null);
  43. mSelectedCount =
  44. (TextView)mMultiSelectActionBarView.findViewById(R.id.selected_conv_count);
  45. }
  46. getActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM,
  47. ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_HOME |
  48. ActionBar.DISPLAY_SHOW_TITLE);
  49. getActionBar().setCustomView(mMultiSelectActionBarView);
  50. ((TextView)mMultiSelectActionBarView.findViewById(R.id.title)).setText(R.string.select_item);
  51. }
  52. @Override
  53. public boolean onCreateOptionsMenu(Menu menu) {
  54. getMenuInflater().inflate(R.menu.multi_select_menu, menu);
  55. return true;
  56. }
  57. @Override
  58. public boolean onPrepareOptionsMenu(Menu menu) {                                                                                                                                              MenuItem mItem = menu.findItem(R.id.action_slelect);
  59. if(mListView.getCheckedItemCount() == mAdapter.getCount()){
  60. mItem.setTitle(R.string.action_deselect_all);
  61. }else{
  62. mItem.setTitle(R.string.action_select_all);
  63. }
  64. return super.onPrepareOptionsMenu(menu);
  65. }
  66. @Override
  67. public boolean onOptionsItemSelected(MenuItem item) {
  68. switch (item.getItemId()) {
  69. case R.id.action_slelect:
  70. if(mListView.getCheckedItemCount() == mAdapter.getCount()){
  71. unSelectedAll();
  72. }else{
  73. selectedAll();
  74. }
  75. mAdapter.notifyDataSetChanged();
  76. break;
  77. default:
  78. break;
  79. }
  80. return super.onOptionsItemSelected(item);
  81. }
  82. public void selectedAll(){
  83. for(int i= 0; i< mAdapter.getCount(); i++){
  84. mListView.setItemChecked(i, true);
  85. }
  86. updateSeletedCount();
  87. }
  88. public void unSelectedAll(){
  89. mListView.clearChoices();
  90. updateSeletedCount();
  91. }
  92. public void updateSeletedCount(){
  93. mSelectedCount.setText(Integer.toString(mListView.getCheckedItemCount()));
  94. }
  95. }

代码解释:

 

首先设置ListView模式:

  1. mListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);

定义一个adapter,当ListView的某个item被选中之后,将该Item的背景设置为蓝色,以标记为选中。不然虽然ListView知道该item被选中,但是界面上没表现出来。

  1. ......
  2. public View getView(int position, View convertView, ViewGroup parent) {
  3. TextView tv;
  4. if (convertView == null) {
  5. tv = (TextView) LayoutInflater.from(mContext).inflate(
  6. android.R.layout.simple_expandable_list_item_1, parent,
  7. false);
  8. } else {
  9. tv = (TextView) convertView;
  10. }
  11. tv.setText(mStrings[position]);
  12. updateBackground(position , tv);
  13. return tv;
  14. }
  15. @SuppressLint("NewApi")
  16. public void updateBackground(int position, View view) {
  17. int backgroundId;
  18. if (mListView.isItemChecked(position)) {
  19. backgroundId = R.drawable.list_selected_holo_light;
  20. } else {
  21. backgroundId = R.drawable.conversation_item_background_read;
  22. }
  23. Drawable background = mContext.getResources().getDrawable(backgroundId);
  24. view.setBackground(background);
  25. }
  26. ......

在item每被点击一次中通知adapter,这样做的目的是为了让更新Ui以显示最新的选中状态。

  1. mListView.setOnItemClickListener(new OnItemClickListener() {
  2. public void onItemClick(AdapterView<?> parent, View view, int position,
  3. long id) {
  4. mAdapter.notifyDataSetChanged();
  5. updateSeletedCount();
  6. }
  7. });

其中mSelectedCount()作用是在actionbar中更新选中的数目。

  1. public void updateSeletedCount(){
  2. mSelectedCount.setText(Integer.toString(mListView.getCheckedItemCount()));
  3. }

上面的代码实现了多选操作,但是在我选中一个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. if(item.getItemId() == R.id.action_choice){
  2. mListView.setItemChecked(0,true);
  3. mListView.clearChoices();
  4. }

有些人可能会问,按照上面的思路,为什么不这样实现呢:

  1. if(item.getItemId() == R.id.action_choice){
  2. mListView.setItemChecked(0,true);
  3. mListView.setItemChecked(0,false);
  4. }

嘿嘿,刚刚我们提到ListView CHOICE_MODE_MULTIPLE_MODAL模式中,一旦有一个item被选中,即进入到多选状态,而他还有个相反的特性,一旦所有的Item被主动的设置为未选中,则退出多选状态,mChoiceActionMode会调用自己的finish()方法。为什么呢?在MultiChoiceModeWrapper类中:

  1. @Override
  2. public void onItemCheckedStateChanged(ActionMode mode,
  3. int position, long id, boolean checked) {
  4. mWrapped.onItemCheckedStateChanged(mode, position, id, checked);
  5. // If there are no items selected we no longer need the selection mode.
  6. if (getCheckedItemCount() == 0) {
  7. mode.finish();
  8. }
  9. }

好了我们来实现一个CHOICE_MODE_MULTIPLE_MODAL模式下的多选操作:

ListView多选操作模式详解CHOICE_MODE_MULTIPLE与CHOICE_MODE_MULTIPLE_MODAL-LMLPHP

代码:

  1. package com.example.listmultichoise;
  2. import android.os.Bundle;
  3. import android.app.Activity;
  4. import android.util.Log;
  5. import android.view.ActionMode;
  6. import android.view.LayoutInflater;
  7. import android.view.Menu;
  8. import android.view.MenuInflater;
  9. import android.view.MenuItem;
  10. import android.view.View;
  11. import android.view.ViewGroup;
  12. import android.widget.AdapterView;
  13. import android.widget.AdapterView.OnItemClickListener;
  14. import android.widget.ListView;
  15. import android.widget.TextView;
  16. import android.widget.Toast;
  17. public class ChoiceModeMultipleModalActivity extends Activity {
  18. ListView mListView = null;
  19. MyListAdapter mAdapter;
  20. ModeCallback mCallback;
  21. @Override
  22. protected void onCreate(Bundle savedInstanceState) {
  23. super.onCreate(savedInstanceState);
  24. setContentView(R.layout.activity_list);
  25. mListView = (ListView)findViewById(R.id.list);
  26. mAdapter = new MyListAdapter(this,mListView);
  27. mListView.setAdapter(mAdapter);
  28. mCallback = new ModeCallback();
  29. mListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
  30. mListView.setMultiChoiceModeListener(mCallback);
  31. mListView.setOnItemClickListener(new OnItemClickListener() {
  32. public void onItemClick(AdapterView<?> parent, View view, int position,
  33. long id) {
  34. Toast.makeText(ChoiceModeMultipleModalActivity.this, "选择了一个item", 300).show();
  35. }
  36. });
  37. }
  38. @Override
  39. public boolean onCreateOptionsMenu(Menu menu) {
  40. // Inflate the menu; this adds items to the action bar if it is present.
  41. getMenuInflater().inflate(R.menu.main, menu);
  42. return true;
  43. }
  44. @Override
  45. public boolean onOptionsItemSelected(MenuItem item) {
  46. if(item.getItemId() == R.id.action_choice){
  47. //这里使用了一点技巧来实现处于选中状态 但是0个item 被选择
  48. mListView.setItemChecked(0,true);
  49. mListView.clearChoices();
  50. mCallback.updateSeletedCount();
  51. }
  52. return super.onOptionsItemSelected(item);
  53. }
  54. private class ModeCallback implements ListView.MultiChoiceModeListener {
  55. private View mMultiSelectActionBarView;
  56. private TextView mSelectedCount;
  57. @Override
  58. public boolean onCreateActionMode(ActionMode mode, Menu menu) {
  59. // actionmode的菜单处理
  60. MenuInflater inflater = getMenuInflater();
  61. inflater.inflate(R.menu.multi_select_menu, menu);
  62. if (mMultiSelectActionBarView == null) {
  63. mMultiSelectActionBarView = LayoutInflater.from(ChoiceModeMultipleModalActivity.this)
  64. .inflate(R.layout.list_multi_select_actionbar, null);
  65. mSelectedCount =
  66. (TextView)mMultiSelectActionBarView.findViewById(R.id.selected_conv_count);
  67. }
  68. mode.setCustomView(mMultiSelectActionBarView);
  69. ((TextView)mMultiSelectActionBarView.findViewById(R.id.title)).setText(R.string.select_item);
  70. return true;
  71. }
  72. @Override
  73. public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
  74. if (mMultiSelectActionBarView == null) {
  75. ViewGroup v = (ViewGroup)LayoutInflater.from(ChoiceModeMultipleModalActivity.this)
  76. .inflate(R.layout.list_multi_select_actionbar, null);
  77. mode.setCustomView(v);
  78. mSelectedCount = (TextView)v.findViewById(R.id.selected_conv_count);
  79. }
  80. //更新菜单的状态
  81. MenuItem mItem = menu.findItem(R.id.action_slelect);
  82. if(mListView.getCheckedItemCount() == mAdapter.getCount()){
  83. mItem.setTitle(R.string.action_deselect_all);
  84. }else{
  85. mItem.setTitle(R.string.action_select_all);
  86. }
  87. return true;
  88. }
  89. @Override
  90. public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
  91. switch (item.getItemId()) {
  92. case R.id.action_slelect:
  93. if(mListView.getCheckedItemCount() == mAdapter.getCount()){
  94. unSelectedAll();
  95. }else{
  96. selectedAll();
  97. }
  98. mAdapter.notifyDataSetChanged();
  99. break;
  100. default:
  101. break;
  102. }
  103. return true;
  104. }
  105. @Override
  106. public void onDestroyActionMode(ActionMode mode) {
  107. mListView.clearChoices();
  108. }
  109. @Override
  110. public void onItemCheckedStateChanged(ActionMode mode,
  111. int position, long id, boolean checked) {
  112. updateSeletedCount();
  113. mode.invalidate();
  114. mAdapter.notifyDataSetChanged();
  115. }
  116. public void updateSeletedCount(){
  117. mSelectedCount.setText(Integer.toString(mListView.getCheckedItemCount()));
  118. }
  119. }
  120. public void selectedAll(){
  121. for(int i= 0; i< mAdapter.getCount(); i++){
  122. mListView.setItemChecked(i, true);
  123. }
  124. mCallback.updateSeletedCount();
  125. }
  126. public void unSelectedAll(){
  127. mListView.clearChoices();
  128. mListView.setItemChecked(0,false);
  129. mCallback.updateSeletedCount();
  130. }
  131. }

这里需要提醒的是虽然ListView的mActionMode我们不能直接操作,但是actionmode的回调方法是可以在activity中设置的:

  1. mListView.setMultiChoiceModeListener(mCallback);

而且这个回调方法比一般的actionmode回调方法多了个onItemCheckedStateChanged

  1. @Override
  2. public void onItemCheckedStateChanged(ActionMode mode,
  3. int position, long id, boolean checked) {
  4. ....
  5. }

demo我已经上传到了csdn:http://download.csdn.net/detail/jianghejie123/8126071

05-11 14:00