Search code examples
javaandroidandroid-recyclerviewmultipleselection

How to select multiple items in recycler view android?


I want to select multiple items in recycler view and when it is selected I want to set visibility as visible of a checkbox of that item. So, long I am able to set onlongClickListner using interface and handling onLongClick event in a fragment.

Whenever user performes long click on any item, app's onCLick logic is changed. Before long click app is opening the item in another activity but, after long click onClick's logic is changed and can be set whatever I want. I want to check the checkbox corresponding to the item after long click. And want to add that from an arrayList which is loaded in recycler view.

Fragment

...
@Override
    public void onclick(int position) {
        if (!isSelectionMode) {
            Intent intent = new Intent(getActivity(), FullPhoto.class);
            intent.putExtra("uri", arrayList.get(position).getUri());
            startActivity(intent);
        }
    }

            //Support fun to turn selectionMode on, onLongClick event.

    @Override
    public void onLongClick() {
        isSelectionMode = true;
    }
...

Adapter

...

public static class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener,View.OnLongClickListener {


        private final ImageView img;
        public CheckBox selection;
        OnImageClickListner listner;
        OnImageLongClickListener longClickListener;
        public MyViewHolder(@NonNull View itemView, OnImageClickListner listner,OnImageLongClickListener longClickListener) {
            super(itemView);
            this.listner = listner;
            this.longClickListener = longClickListener;
            itemView.setOnLongClickListener(this);          //onLongClickListener is set to all of the RecyclerView Items for once rather than setting on each item in BindViewHolder for repeated times
            itemView.setOnClickListener(this);          //onClickListener is set to all of the RecyclerView Items for once rather than setting on each item in BindViewHolder for repeated times
            img = itemView.findViewById(R.id.img);
            selection = itemView.findViewById(R.id.checkbox);

        }

        @Override
        public void onClick(View v) {
            listner.onclick(getAdapterPosition());            //Returning the current clicked position
        }

        @Override
        public boolean onLongClick(View v) {
            longClickListener.onLongClick();          
            return  true;
        }
    }
// inner class ends


    public  interface  OnImageClickListner{         //Interface to generate call back when user clicked an image.
         void onclick(int position);
    }

    public interface OnImageLongClickListener{          //Interface to generate call back when user long clicked an image.
        void onLongClick();
    }

...

I this scenario, i am unable to understand how to implement a selection tracker. I can get the adapter position with getAdapterPosition() and then I can remove the element from arrayList on that index. But, I want to highlight the checkbox at that position. In this case i am unable to implement the code.

Things I tried

I did try to pass the View v from onLongClick(View v) and then passing the selection checkbox to onCLick() event. But, It didn't work.

I want to select the items from recycler view and set visibility as VISIBLE for the selected Items.

------ Update ------

I am now able to set visibility of the checkbox as visible with the help of few edits in event methods.

Fragment

@Override
    public void onclick(int position, CheckBox selection) {
        if (!isSelectionMode) {
            Intent intent = new Intent(getActivity(), FullPhoto.class);
            intent.putExtra("uri", arrayList.get(position).getUri());
            startActivity(intent);
        }
        else
        {
            selection.setVisibility(View.VISIBLE);
            selection.setChecked(true);
        }
    }

Where selection is a checkbox passed from MyViewHolder class from adapter. But, because nature of the recycler view i am getting double selections. and one weird issues that after selecting items if I scroll down, selections will be changed randomly.

Issue

In this image as you can see, I only selected 4 images but, when I scrolled down, other images were selected as well and when I scrolled up again it messed my selected items.

PhotosAdapter

public class PhotosAdapter extends RecyclerView.Adapter<PhotosAdapter.MyViewHolder> {


    public Context context;
    ArrayList<ImageModel> arrayList;
    Activity activity;
    OnImageClickListner listener;
    OnImageLongClickListener longClickListener;

    /*===============================================================   CONSTRUCTOR   ===============================================================*/

    public PhotosAdapter(Context context, ArrayList<ImageModel> arrayList, Activity activity, OnImageClickListner listner, OnImageLongClickListener longClickListener) {
        this.context = context;
        this.arrayList = arrayList;
        this.activity = activity;
        this.listener = listner;
        this.longClickListener = longClickListener;

    }

    /*===============================================================   OVERRIDDEN METHODS   ===============================================================*/

    @NonNull
    @Override
    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {           //This methods returns single_view.xml as a view for RecyclerView.
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.single_view, parent, false);
        return new MyViewHolder(view, listener, longClickListener);
    }

    @Override
    public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {          //Binding the uris with each view depending upon the position of current view.


        activity.runOnUiThread(() -> GlideApp.with(context)
                .load(Uri.parse(arrayList.get(position).getUri()))
                .apply(RequestOptions.overrideOf(150, 150))          //It overrides the value of original image and reduces it to the visible thumbnail size.
                .diskCacheStrategy(DiskCacheStrategy.RESOURCE)          //Then it caches the reduced size thumbnail for faster loading speed.
                .into(holder.img));
    }

    @Override
    public int getItemCount() {
        return arrayList.size();
    }


    /*===============================================================   INNER VIEW HOLDER CLASS   ===============================================================*/

    public static class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {


        private final ImageView img;
        public CheckBox selection;
        OnImageClickListner listener;
        OnImageLongClickListener longClickListener;

        public final SparseBooleanArray selectedItems;              ///////////////////////////////// ADDED LINE /////////////////////////////////

        public MyViewHolder(@NonNull View itemView, OnImageClickListner listener, OnImageLongClickListener longClickListener) {
            super(itemView);
            this.listener = listener;
            this.longClickListener = longClickListener;
            itemView.setOnLongClickListener(this);          //onLongClickListener is set to all of the RecyclerView Items for once rather than setting on each item in BindViewHolder for repeated times
            itemView.setOnClickListener(this);          //onClickListener is set to all of the RecyclerView Items for once rather than setting on each item in BindViewHolder for repeated times
            img = itemView.findViewById(R.id.img);
            selection = itemView.findViewById(R.id.checkbox);

            selectedItems = new SparseBooleanArray();             ///////////////////////////////// ADDED LINE /////////////////////////////////
        }



        @Override
        public void onClick(View v) {

            listener.onclick(getAdapterPosition(), selection);            //Returning the current clicked position and selection checkbox to the implemented method.
        }

        @Override
        public boolean onLongClick(View v) {
            longClickListener.onLongClick(getAdapterPosition(), v);          //Returning the current clicked position and view to the implemented method.
            return true;
        }



        //////////////////////////////////////////////////////////////////////////// ADDED LINES ////////////////////////////////////////////////////////////////////////////



        boolean isSelected(int position) {
            return getSelectedItems().contains(position);
        }

        public void toggleSelection(int position) {


            if (selectedItems.get(position, false)) {
                selectedItems.delete(position);
            } else {

                selectedItems.put(position, true);


            }
            notifyItemChanged(position);
        }

        public void selectAll() {
            for (int i = 0; i < getItemCount(); i++) {
                if (!(selectedItems.get(i, false))) {
                    selectedItems.put(i, true);
                }
                notifyItemChanged(i);
            }
            notifyDataSetChanged();
        }

        public void clearSelection() {
            List<Integer> selection = getSelectedItems();
            selectedItems.clear();
            for (Integer i : selection) {
                notifyItemChanged(i);
            }
        }

        public int getSelectedItemCount() {
            return selectedItems.size();
        }

        public List<Integer> getSelectedItems() {
            List<Integer> items = new ArrayList<>(selectedItems.size());
            for (int i = 0; i < selectedItems.size(); ++i) {
                items.add(selectedItems.keyAt(i));
            }
            return items;
        }





    }       //INNER CLASS ENDS

    /*===============================================================   INTERFACES   ===============================================================*/

    public interface OnImageClickListner {         //Interface to generate call back when user clicked an image.
        void onclick(int position, CheckBox selection);
    }

    public interface OnImageLongClickListener {          //Interface to generate call back when user long clicked an image.
        void onLongClick(int position, View v);
    }


}

Solution

  • This is what is causing the double or multiple selections. Recyclerview works this way, that views are recycled.

    So you have to hide, check or uncheck each item everytine a view is inflated. So in the onbindViewholder you have to setChecked() to true or false depending on your scenario.

    My approach for this problem would be: Instead of passing views to the parent fragment, keep the check logic in the adapter this way:

    if (isItemSelected){
              selection.setChecked(true);
    }else{
               selection.setChecked(false);
    }
    

    By so doing, there will be perfect checking and unchecking.

    -- Update --

    Create a Selectable adapter class to provide the isSelected() method as follows

    SelectableAdapter

    public abstract class SelectableAdapter<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
    private static final String TAG = SelectableAdapter.class.getSimpleName();
    
    private final SparseBooleanArray selectedItems;
    
    SelectableAdapter() {
    
        selectedItems = new SparseBooleanArray();
    
    }
    
    boolean isSelected(int position) {
        return getSelectedItems().contains(position);
    }
    
    public void toggleSelection(int position) {
    
        
        if (selectedItems.get(position, false)) {
            selectedItems.delete(position);
        } else {
    
                selectedItems.put(position, true);
            
    
        }
        notifyItemChanged(position);
    }
    
    public void selectAll() {
        for (int i = 0; i < getItemCount(); i++) {
            if (!(selectedItems.get(i, false))) {
                selectedItems.put(i, true);
            }
            notifyItemChanged(i);
        }
        notifyDataSetChanged();
    }
    
    public void clearSelection() {
        List<Integer> selection = getSelectedItems();
        selectedItems.clear();
        for (Integer i : selection) {
            notifyItemChanged(i);
        }
    }
    
    public int getSelectedItemCount() {
        return selectedItems.size();
    }
    
    public List<Integer> getSelectedItems() {
        List<Integer> items = new ArrayList<>(selectedItems.size());
        for (int i = 0; i < selectedItems.size(); ++i) {
            items.add(selectedItems.keyAt(i));
        }
        return items;
    }
    
    }
    

    Have your Adapter extend the selectableAdapter as follows:

    Adapter Class

    public class RecyclerViewAdapter extends SelectableAdapter<RclAdapter.ViewHolder> 
    

    On the fragment use toggleSelection to set a position as selected or not Selected.

    @Override
        public void onclick(int position) {
            if (!isSelectionMode) {
                Intent intent = new Intent(getActivity(), FullPhoto.class);
                intent.putExtra("uri", arrayList.get(position).getUri());
                startActivity(intent);
            }
            else
            {
    // Use the adapter instance here
                adapter.toggleSelection(position);
        
            }
        }
    

    Remember toggleSelection notifies the adapter and calls onBindViewHolder.

    In the adapter: onBindViewHolder implement the selection logic to show and hide the checkbox. If you only set it to View.VISIBLE and don't set it to View.GONE for the ones not selected, you will still have the same problem.