天天看点

GridView多选错误范例解析

最近在写一个程序,需要使用GridView显示很多图片的缩略图。想要实现的效果是长按

进入多选状态,在多选状态点击各个图片能够勾选,并得到所有选择的图片。

最初参考的是这篇文章

http://blog.csdn.net/zhouyuanjing/article/details/8372686

文章里作者提供了源码,为分析方便,贴在下面。

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView.LayoutParams;
import android.widget.AbsListView.MultiChoiceModeListener;
import android.widget.BaseAdapter;
import android.widget.Checkable;
import android.widget.FrameLayout;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.ListAdapter;
import android.widget.TextView;

public class HomeActivity extends Activity implements MultiChoiceModeListener {

    private GridView mGridView;
    private GridAdapter mGridAdapter;
    private TextView mActionText;
    private static final int MENU_SELECT_ALL = 0;
    private static final int MENU_UNSELECT_ALL = MENU_SELECT_ALL + 1;
    private Map<Integer, Boolean> mSelectMap = new HashMap<Integer, Boolean>();

    private int[] mImgIds = new int[] { R.drawable.img_1, R.drawable.img_2,
            R.drawable.img_3};

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        mGridView = (GridView) findViewById(R.id.gridview);
        mGridView.setChoiceMode(GridView.CHOICE_MODE_MULTIPLE_MODAL);
        mGridAdapter = new GridAdapter(this);
        mGridView.setAdapter(mGridAdapter);
        mGridView.setMultiChoiceModeListener(this);
    }

    /** Override MultiChoiceModeListener start **/
    @Override
    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
        // TODO Auto-generated method stub
        View v = LayoutInflater.from(this).inflate(R.layout.actionbar_layout,
                null);
        mActionText = (TextView) v.findViewById(R.id.action_text);
        mActionText.setText(formatString(mGridView.getCheckedItemCount()));
        mode.setCustomView(v);
        getMenuInflater().inflate(R.menu.action_menu, menu);
        return true;
    }

    @Override
    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
        // TODO Auto-generated method stub
        menu.getItem(MENU_SELECT_ALL).setEnabled(
                mGridView.getCheckedItemCount() != mGridView.getCount());
        return true;
    }

    @Override
    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
        // TODO Auto-generated method stub
        switch (item.getItemId()) {
        case R.id.menu_select:
            for (int i = 0; i < mGridView.getCount(); i++) {
                mGridView.setItemChecked(i, true);
                mSelectMap.put(i, true);
            }
            break;
        case R.id.menu_unselect:
            for (int i = 0; i < mGridView.getCount(); i++) {
                mGridView.setItemChecked(i, false);
                mSelectMap.clear();
            }
            break;
        }
        return true;
    }

    @Override
    public void onDestroyActionMode(ActionMode mode) {
        // TODO Auto-generated method stub
        mGridAdapter.notifyDataSetChanged();
    }

    @Override
    public void onItemCheckedStateChanged(ActionMode mode, int position,
            long id, boolean checked) {
        // TODO Auto-generated method stub
        mActionText.setText(formatString(mGridView.getCheckedItemCount()));
        mSelectMap.put(position, checked);
        mode.invalidate();
    }

    /** Override MultiChoiceModeListener end **/

    private String formatString(int count) {
        return String.format(getString(R.string.selection), count);
    }

    private class GridAdapter extends BaseAdapter {

        private Context mContext;

        public GridAdapter(Context ctx) {
            mContext = ctx;
        }

        @Override
        public int getCount() {
            // TODO Auto-generated method stub
            return mImgIds.length;
        }

        @Override
        public Integer getItem(int position) {
            // TODO Auto-generated method stub
            return Integer.valueOf(mImgIds[position]);
        }

        @Override
        public long getItemId(int position) {
            // TODO Auto-generated method stub
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            // TODO Auto-generated method stub
			Log.d(TAG, "getView: "+position+","+(mSelectMap.get(position) == null ? false
                    : mSelectMap.get(position)));		//<------(2)
            GridItem item;
            if (convertView == null) {
                item = new GridItem(mContext);
                item.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,
                        LayoutParams.FILL_PARENT));
            } else {
                item = (GridItem) convertView;
            }
            item.setImgResId(getItem(position));
            item.setChecked(mSelectMap.get(position) == null ? false
                    : mSelectMap.get(position));		//<-----(1)
            return item;
        }
    }

}
           

其中GridItem是列表项的View类,实现了Checkable接口

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Checkable;
import android.widget.ImageView;
import android.widget.RelativeLayout;

public class GridItem extends RelativeLayout implements Checkable {

    private Context mContext;
    private boolean mChecked;
    private ImageView mImgView = null;
    private ImageView mSecletView = null;

    public GridItem(Context context) {
        this(context, null, 0);
    }

    public GridItem(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public GridItem(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        // TODO Auto-generated constructor stub
        mContext = context;
        LayoutInflater.from(mContext).inflate(R.layout.grid_item, this);
        mImgView = (ImageView) findViewById(R.id.img_view);
        mSecletView = (ImageView) findViewById(R.id.select);
    }

    @Override
    public void setChecked(boolean checked) {
        // TODO Auto-generated method stub
		Log.d(TAG,"setChecked: "+checked);			//<---
        mChecked = checked;
        setBackgroundDrawable(checked ? getResources().getDrawable(
                R.drawable.background) : null);
        mSecletView.setVisibility(checked ? View.VISIBLE : View.GONE);
    }

    @Override
    public boolean isChecked() {
        // TODO Auto-generated method stub
        return mChecked;
    }

    @Override
    public void toggle() {
        // TODO Auto-generated method stub
        setChecked(!mChecked);
    }

    public void setImgResId(int resId) {
        if (mImgView != null) {
            mImgView.setBackgroundResource(resId);
        }
    }

}
           

作者的基本思路就是自己维护一个HashMap,用于记录哪些位置的图片被选中了,然后

根据选中状态在Adapter的getView方法中设置图片的外观。如(1)处所示。

但是经过我的试验,发现这种方法不仅费事,而且极易出错,可以说是吃力不讨好。

因为AbsListView本身就有一个变量来记录各项的选中状态:

SparseBooleanArray mCheckStates;

SparseBooleanArray是android.util包中的一个类,API说它

SparseBooleanArrays map integers to booleans。因此它也相当于是一个Map,将int

类型的key对应到boolean值上。实际上就记录了GridView中的各个position的选择状态

true或者false.

如果我们只是没有用GridView自带的机制来处理选中状态,那还好,顶多算费事点。

但是除了费事之外,自己维护选中状态会随之带来很多问题。

虽然我们在getView中调用GridItem的setChecked方法设置好了列表项的状态(这个状态

依据的是自定义的mSelectMap),但getView返回后,GridView自身还会去调用一次

GridItem的setChecked方法再次设置列表项的状态,而这次调用,依据的是mCheckState

中记录的状态。这个状态,很可能跟mSelectMap不一样。

我们可以做试验来验证。

首先在GridItem的setCheced()方法,getView方法中都添加Log代码,如(2)(3)所示

第一次长按第0项,Logcat输出为:

1、08-05 16:27:13.416: D/GridItem(1812): setChecked: false

2、08-05 16:27:13.416: D/GridItem(1812): setChecked: false

3、08-05 16:27:13.416: D/GridItem(1812): setChecked: false

4、08-05 16:27:14.036: D/HomeActivity(1812): getView: 0,true

5、08-05 16:27:14.036: D/GridItem(1812): setChecked: true

6、08-05 16:27:14.056: D/HomeActivity(1812): getView: 0,true

7、08-05 16:27:14.056: D/GridItem(1812): setChecked: true

8、08-05 16:27:14.076: D/GridItem(1812): setChecked: true

9、08-05 16:27:14.086: D/HomeActivity(1812): getView: 1,false

10、08-05 16:27:14.086: D/GridItem(1812): setChecked: false

11、08-05 16:27:14.086: D/GridItem(1812): setChecked: false

12、08-05 16:27:14.096: D/HomeActivity(1812): getView: 2,false

13、08-05 16:27:14.096: D/GridItem(1812): setChecked: false

14、08-05 16:27:14.096: D/GridItem(1812): setChecked: false

其中1~3行是GridView调用的setChecked,全为false,可以认为是建立初始状态

第5行是我们自己在getView中调用的setChecked;

第6,7行与第4、5行一样,好像每次第一项都会处理两次,不知何故;

第8行是GridView在getView(0)之后调用的setChecked;

同理,第11、14行分别是系统在每次getView之后调用的setChecked。

可以看到,迄今为止,GridView调用的setChecked和我们自己调用的setChecked状态

都还是一致的。

好,保持第0项为选中状态,我们点击返回退出多选模式,Logcat输出为:

1、08-05 16:34:30.897: D/HomeActivity(1812): getView: 0,true

2、08-05 16:34:30.897: D/GridItem(1812): setChecked: true

3、08-05 16:34:30.907: D/HomeActivity(1812): getView: 0,true

4、08-05 16:34:30.907: D/GridItem(1812): setChecked: true

5、08-05 16:34:30.907: D/GridItem(1812): setChecked: false

6、08-05 16:34:30.917: D/HomeActivity(1812): getView: 1,false

7、08-05 16:34:30.917: D/GridItem(1812): setChecked: false

8、08-05 16:34:30.917: D/GridItem(1812): setChecked: false

9、08-05 16:34:30.917: D/HomeActivity(1812): getView: 2,false

10、08-05 16:34:30.927: D/GridItem(1812): setChecked: false

11、08-05 16:34:30.937: D/GridItem(1812): setChecked: false

对于第一项,第3、4行是重复第1、2行的;

第5行就有意思了,是由GridView调用的setChecked,为false。

对比第4行和第5行,即GridView认为第0项已经取消选择了,而我们依据mSelectMap

却认为第0项还在选择状态。

至此已经出现了视图和数据模型不一致的情况。

长按第2项再次进入多选,Logcat输出如下:

1、08-05 16:39:21.666: D/GridItem(1812): setChecked: false

2、08-05 16:39:21.666: D/GridItem(1812): setChecked: false

3、08-05 16:39:21.688: D/GridItem(1812): setChecked: false

4、08-05 16:39:22.306: D/HomeActivity(1812): getView: 0,true

5、08-05 16:39:22.316: D/GridItem(1812): setChecked: true

6、08-05 16:39:22.348: D/HomeActivity(1812): getView: 0,true

7、08-05 16:39:22.348: D/GridItem(1812): setChecked: true

8、08-05 16:39:22.356: D/GridItem(1812): setChecked: false

9、08-05 16:39:22.356: D/HomeActivity(1812): getView: 1,false

10、08-05 16:39:22.366: D/GridItem(1812): setChecked: false

11、08-05 16:39:22.366: D/GridItem(1812): setChecked: false

12、08-05 16:39:22.366: D/HomeActivity(1812): getView: 2,true

13、08-05 16:39:22.409: D/GridItem(1812): setChecked: true

14、08-05 16:39:22.409: D/GridItem(1812): setChecked: true

第1、2、3行为GridView在设置初始状态

第6、7行重复第4、5行;

第8行和第7行仍旧是不一致的,代表mSelectMap和GridView在第0项是否选择上仍旧是

不一样的。在mSeletMap看来,第0项和第2项处于选择状态;而在GridView看来,只有

第2项处于选择状态。

如果保持第2项选中,退出多选模式,则对于第2项的选择状态,两者也会出现不一致的

情况。我们当然可以在退出多选模式的时候做点什么,使mSelectMap和GridView的

mCheckStates保持一致。但既然我们并不能在getView中借由自己维护的数据模型

mSelectMap来控制外观,选中状态的外观仍然取决于GridView.mCheckStates。那为什

么要多此一举自己定义一个mSelectMap并费劲地使它和GridView.mCheckStates保持一

致,而不直接使用GridView.mCheckStates呢?

所以,直接使用AbsListView的mCheckedState及相关的公共方法来设置和查询选中状态

才是正途。下面是AbsListView提供的一些与选中有关的方法。

//判断一个item是否被选中1

public boolean isItemChecked(int position);

//获得被选中item的总数

public int getCheckedItemCount();

//选中一个item

public void setItemChecked(int position, boolean value);

//清除选中的item

public void clearChoices();

详细可参考

http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2014/1105/1906.html

最后附上我修改的代码

http://download.csdn.net/detail/glorydream2015/8975721

(没积分下载别人的东西了,大家给点分意思一下吧)