GridView多选错误范例解析

本文针对使用GridView实现图片多选功能时出现的问题进行了深入探讨。分析了自定义选中状态管理的弊端,并提出直接利用GridView内置机制进行优化的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近在写一个程序,需要使用GridView显示很多图片的缩略图。想要实现的效果是长按
进入多选状态,在多选状态点击各个图片能够勾选,并得到所有选择的图片。
最初参考的是这篇文章
http://blog.youkuaiyun.com/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.youkuaiyun.com/detail/glorydream2015/8975721

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值