0

I'm trying to make a GridView full of checkboxes. I'm following the ViewHolder pattern to recycle views and attempt to set checkboxes to their correct values, but for some reason I can't seem to make them not duplicate. After my debugging efforts, I've discovered that my SparseBooleanArray IS being correctly updated - it's just that applying it to the view seems to turn on the wrong checkboxes even when I'm applying them to the right positions. Here's my GridView's entire adapter - relevant portions being getView and onCheckedChanged

// Our image adapter takes our list of thumbnails and creates a selectable grid full of asychroniously loaded images.
public class ThumbnailImageAdapter extends BaseAdapter implements CompoundButton.OnCheckedChangeListener {
    Context mContext;
    LayoutInflater mInflater;
    SparseBooleanArray mCheckedStates;
    private int mItemHeight = 0;
    private int mNumColumns = 0;
    private RelativeLayout.LayoutParams mImageViewLayoutParams;
    private ArrayList<String> mList;
    private ArrayList<String> mImageFullResUrls;
    private String nextURL;

    public ThumbnailImageAdapter(Context context) {
        super();
        mContext = context;
        mInflater = LayoutInflater.from(mContext);
        mImageViewLayoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);
        clear();
    }

    public ArrayList<String> getCheckedItems() {
        ArrayList<String> checkedItems = new ArrayList<String>();
        for (int i = 0; i < mList.size(); i++) {
            if (mCheckedStates.get(i)) {
                checkedItems.add(mImageFullResUrls.get(i));
            }
        }
        return checkedItems;
    }

    public boolean hasSelectedItems() {
        return mCheckedStates.indexOfValue(true) >= 0;
    }

    @Override
    public int getCount() {
        // If columns have yet to be determined, return no items
        if (getNumColumns() == 0) {
            return 0;
        }

        return mList.size();
    }

    @Override
    public Object getItem(int position) {
        return mImageFullResUrls.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public boolean hasStableIds() {
        return true;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup container) {
        if(position >= getCount()) {
            return null;
        }
        final ThumbnailViewHolder holder;
        if (convertView == null) {
            convertView = mInflater.inflate(R.layout.selectable_imageview, null);
            holder = new ThumbnailViewHolder();
            holder.checkbox = (CheckBox) convertView.findViewById(R.id.photo_checkbox);
            holder.imageView = (SmartImageView) convertView.findViewById(R.id.photo_imageview);
            convertView.setTag(holder);
        } else {
            holder = (ThumbnailViewHolder) convertView.getTag();
        }

        holder.checkbox.setTag(position);
        holder.checkbox.setOnCheckedChangeListener(this);
        holder.checkbox.setChecked(mCheckedStates.get(position, false));


        holder.imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
        holder.imageView.setLayoutParams(mImageViewLayoutParams);
        // Finally load the image asynchronously into the ImageView, this also takes care of
        // setting a placeholder image while the background thread runs
        holder.imageView.setImageUrl(mList.get(position));

        final View finalView = convertView;
        convertView.post(new Runnable() {
            @Override
            public void run() {
                Rect delegateArea = new Rect();
                holder.imageView.getHitRect(delegateArea);
                finalView.setTouchDelegate(new TouchDelegate(delegateArea, holder.checkbox));
            }
        });

        return convertView;
    }

    public void setItemHeight(int height) {
        if (height == mItemHeight) {
            return;
        }
        mItemHeight = height;
        mImageViewLayoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, mItemHeight);
        notifyDataSetChanged();
    }

    public int getNumColumns() {
        return mNumColumns;
    }

    public void setNumColumns(int numColumns) {
        mNumColumns = numColumns;
    }

    public void appendImage(String thumbUrl, String fullResUrl) {
        if (!mList.contains(thumbUrl)) {
            mList.add(thumbUrl);
            mImageFullResUrls.add(fullResUrl);
            if (getActivity() != null) {
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        mAdapter.notifyDataSetChanged();
                    }
                });
            }
        }
    }

    public void prependImage(String thumbUrl, String fullResUrl) {
        if (!mList.contains(thumbUrl)) {
            mList.add(0, thumbUrl);
            mImageFullResUrls.add(0, fullResUrl);
            SparseBooleanArray shiftedArray = new SparseBooleanArray(mList.size());
            // Shift our entire array one down (there has to be a better way to do this)
            for (int i = 0; i < mCheckedStates.size(); i++) {
                shiftedArray.put(mCheckedStates.keyAt(i) + 1, mCheckedStates.valueAt(i));
            }
            mCheckedStates = shiftedArray;
            getActivity().runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mAdapter.notifyDataSetChanged();
                }
            });
        }
    }

    public void clear() {
        mList = new ArrayList<String>();
        mImageFullResUrls = new ArrayList<String>();
        mCheckedStates = new SparseBooleanArray();
    }

    public String getNextURL() {
        return this.nextURL;
    }

    public void setNextURL(String nextURL) {
        this.nextURL = nextURL;
    }

    public ArrayList<String> getThumbnailURLs() {
        return mList;
    }

    @Override
    public void onCheckedChanged(CompoundButton checkBoxView, boolean isChecked) {
        SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(getActivity());
        final int numPhotos = Integer.parseInt(settings.getString("num_photos", null));
        if (mAdapter.getCheckedItems().size() > numPhotos) {
            checkBoxView.setChecked(false); // Don't allow the user to select more than numPhotos photos
        }
        // This animates our image "pressed" and "unpressed" states
        mCheckedStates.put((Integer) checkBoxView.getTag(), isChecked);
        final ImageView image = (ImageView) ((ViewGroup) checkBoxView.getParent()).findViewById(R.id.photo_imageview);
        int dpi = getResources().getDisplayMetrics().densityDpi;
        int _12dp = (int) (12 * (dpi / 160f));
        ValueAnimator animator;

        if (isChecked) {
            animator = ValueAnimator.ofInt(image.getPaddingRight(), _12dp);
        } else {
            animator = ValueAnimator.ofInt(image.getPaddingRight(), 0);
        }

        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                int v = (Integer) valueAnimator.getAnimatedValue();
                image.setPadding(v, v, v, v);
            }
        });
        animator.setDuration(100);
        animator.start();

        mOnImageSelectionChangeListener.onImageSelectionChange(getCheckedItems());
        getActivity().invalidateOptionsMenu();
    }
}

static class ThumbnailViewHolder {
    CheckBox checkbox;
    SmartImageView imageView;
}
Ari Lotter
  • 605
  • 8
  • 23

1 Answers1

0

This is due to the way grid view recycles view's.

Implement the below for your adapter

implements CompoundButton.OnCheckedChangeListener

Then have a SpareBooleanArray to keep track of checked stated

SparseBooleanArray mCheckStates; 

Then

mCheckStates = new SparseBooleanArray(your data size);

In getView

holder.chkSelect.setTag(position);
holder.chkSelect.setChecked(mCheckStates.get(position, false));
holder.chkSelect.setOnCheckedChangeListener(this); 

Then override

public boolean isChecked(int position) {
        return mCheckStates.get(position, false);
    }

public void setChecked(int position, boolean isChecked) {
    mCheckStates.put(position, isChecked);

}

public void toggle(int position) {
    setChecked(position, !isChecked(position));

}
@Override
public void onCheckedChanged(CompoundButton buttonView,
    boolean isChecked) {

 mCheckStates.put((Integer) buttonView.getTag(), isChecked);    

}

Also move this

 holder.imageView = (SmartImageView) convertView.findViewById(R.id.photo_imageview);

to the if part in getView

and change this

  imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
  imageView.setLayoutParams(mImageViewLayoutParams);
    // Finally load the image asynchronously into the ImageView, this also takes care of
    // setting a placeholder image while the background thread runs
 imageView.setImageUrl(mList.get(position));

to

  holder.imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
  holder.imageView.setLayoutParams(mImageViewLayoutParams);
    // Finally load the image asynchronously into the ImageView, this also takes care of
    // setting a placeholder image while the background thread runs
  holder.imageView.setImageUrl(mList.get(position));

You can find a similar exmaple @

How to get checkbox state in a gridview

Community
  • 1
  • 1
Raghunandan
  • 132,755
  • 26
  • 225
  • 256
  • I already have a SparseBooleanArray and I keep track of checked items. I've implemented the lower part of your changes, but it's still randomly moving checkbox states on scrolling out of the view. – Ari Lotter Jul 28 '15 at 06:14
  • @aerobit it should work fine you can see a working exmaple posted in the link below. try it yourself – Raghunandan Jul 28 '15 at 06:15
  • That's where I'm stuck - It should work fine, but my code is still throwing checkboxes everywhere. Any idea where my implementation could possibly differ? I've implemented everything suggested in your post. – Ari Lotter Jul 28 '15 at 06:16
  • @aerobit if you follow my post it should work. If not post the whole adapter code – Raghunandan Jul 28 '15 at 06:32
  • @aerobit why do you have `onClick` for the check box. You have two listeners for the same check box. Remove onClickListenet – Raghunandan Jul 28 '15 at 06:36
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/84426/discussion-between-aerobit-and-raghunandan). – Ari Lotter Jul 28 '15 at 06:38