2015年8月19日水曜日

Android RecyclerView Multi Selection, expand list items and change list items color



For a few days, I consider how to implement multi selection on RecyclerView
and  how to change color of list items.

But, I got stuck several times.
For example, when I click one of the views to change its color, the attempt was success
but when I scrolled up and down, other views also seem to be changed.
That happends because RecyclerView  literally recycles views  and keep their property.
So, once the view change its color, that is recycled again and again, causing other views seem to be changed.
(That is not a good explanation sorry...)

Anyway, I find a way to do implement them.
This implementation may not be a better way  than other sophisticated apps

I show the way of it step by step,  or you can check my github repository.

https://github.com/omzi43kf/RecyclerViewMulti

1, create layout 


Fragment layout 

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
             xmlns:tools="http://schemas.android.com/tools"
             xmlns:app="http://schemas.android.com/apk/res-auto"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             tools:context="com.omzi43kf.lgaji.recyclerviewmulti.fragment.MultipleSelectionFragment">

    <!-- TODO: Update blank fragment layout -->
    <android.support.v7.widget.RecyclerView
        android:id="@+id/fragment_multiple_selection_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_centerHorizontal="true"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        />

</FrameLayout>


list item layout

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
    android:id="@+id/card_view"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="10dp"
    android:layout_marginLeft="10dp"
    android:layout_marginRight="10dp"
    android:layout_marginTop="10dp"
    android:background="?android:attr/selectableItemBackground"
    android:paddingBottom="16dp"
    android:paddingTop="16dp"
    card_view:cardCornerRadius="4dp"
    card_view:cardElevation="8dp"

    >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        >


        <TextView
            android:id="@+id/list_item_primary_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingBottom="16dp"
            android:paddingLeft="16dp"
            android:paddingRight="16dp"
            android:paddingTop="24dp"
            android:text="Hello World!"
            android:textColor="@color/black"
            android:textSize="24sp"
            />


        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"

            android:paddingBottom="6dp"
            android:paddingLeft="8dp"
            android:paddingRight="8dp"
            android:paddingTop="6dp"
            >


            <Button
                android:id="@+id/list_item_action1_button"
                style="?android:attr/borderlessButtonStyle"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"

                android:layout_alignParentLeft="true"
                android:text="Actoin1"
                android:textColor="@color/blue_500"
                />

            <Button
                android:id="@+id/list_item_action2_button"
                style="?android:attr/borderlessButtonStyle"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_toRightOf="@+id/list_item_action1_button"
                android:text="Action2"

                android:textColor="@color/green_500"/>


            <ImageButton
                android:id="@+id/list_item_expand_button"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_alignParentRight="true"
                android:background="@android:color/transparent"

                android:src="@drawable/ic_expand_more_black_24dp"
                />


        </RelativeLayout>


        <TextView
            android:id="@+id/list_item_supporting_text"
            android:layout_width="match_parent"
            android:layout_height="138dp"
            android:autoText="true"
            android:background="@color/yellow_500"
            android:paddingBottom="24dp"
            android:paddingTop="16dp"
            android:paddingLeft="16dp"
            android:paddingRight="16dp"
            android:text="Hello World from Supporting text!"
            android:textColor="@color/black"
            android:textSize="14sp"
            android:visibility="visible"
            />


    </LinearLayout>


</android.support.v7.widget.CardView>

layout may look like this



































2, Create Model

package com.omzi43kf.lgaji.recyclerviewmulti;

public class ItemForMultipleSelection {


    private String mPrimaryText;
    private String mSupportingText;
    private int mId;
    private boolean mChecked;
    private boolean mActivateExpansion;
    

    public String getPrimaryText() {
        return mPrimaryText;
    }

    public void setPrimaryText(String primaryText) {
        mPrimaryText = primaryText;
    }

    public String getSupportingText() {
        return mSupportingText;
    }

    public void setSupportingText(String supportingText) {
        mSupportingText = supportingText;
    }

    public int getId() {
        return mId;
    }

    public void setId(int id) {
        mId = id;
    }

    public boolean isChecked() {
        return mChecked;
    }

    public void setChecked(boolean checked) {
        mChecked = checked;
    }

    public boolean isActivateExpansion() {
        return mActivateExpansion;
    }

    public void setActivateExpansion(boolean activateExpansion) {
        mActivateExpansion = activateExpansion;
    }
}

Important things in this class are 'mChecked' and 'mActivateExpansion' fields
They work for changing the appearance of list item.
You can change other fields like mPrimaryText into whatever you like


3, Create Adapter

package com.omzi43kf.lgaji.recyclerviewmulti.adapter;

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;

import com.omzi43kf.lgaji.recyclerviewmulti.ItemForMultipleSelection;
import com.omzi43kf.lgaji.recyclerviewmulti.R;
import com.omzi43kf.lgaji.recyclerviewmulti.util.ResourceUtil;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by lgaji on 2015/08/19.
 */
public class MultipleSelectAdapter extends RecyclerView.Adapter<MultipleSelectAdapter.ViewHolder>{


    private static final String LOG_TAG = MultipleSelectAdapter.class.getSimpleName();
    private Context mContext;
    private List<ItemForMultipleSelection> mItemForMultipleSelectionList;
    private MultipleSelectAdapterCallback mMultipleSelectAdapterCallback;

    public MultipleSelectAdapter(Context context) {
        mContext = context;
    }

    public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener{


        public TextView mPrimaryTextView;
        public TextView mSupportingTextView;
        public ImageButton mExpandButton;


        public ViewHolder(View itemView) {
            super(itemView);


            mPrimaryTextView = (TextView) itemView.findViewById(R.id.list_item_primary_text);
            mSupportingTextView = (TextView) itemView.findViewById(R.id.list_item_supporting_text);

            mExpandButton = (ImageButton) itemView.findViewById(R.id.list_item_expand_button);
            mExpandButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {

                    if (getAdapterPosition() != RecyclerView.NO_POSITION) {

                        if (mItemForMultipleSelectionList.get(getAdapterPosition()).isActivateExpansion()) {

                            mItemForMultipleSelectionList.get(getAdapterPosition()).setActivateExpansion(false);
                        } else {
                            mItemForMultipleSelectionList.get(getAdapterPosition()).setActivateExpansion(true);
                        }
                        notifyItemChanged(getAdapterPosition());
                    }

                }
            });

            itemView.setOnClickListener(this);

        }

        @Override
        public void onClick(View v) {

            Log.d(LOG_TAG,"Clicked");

            if (mMultipleSelectAdapterCallback != null && getAdapterPosition() != RecyclerView.NO_POSITION) {

                int clickedPosition = getAdapterPosition();
                ItemForMultipleSelection clickedItem = mItemForMultipleSelectionList.get(clickedPosition);

                if (clickedItem.isChecked()) {
                    clickedItem.setChecked(false);
                } else {
                    clickedItem.setChecked(true);
                }

                notifyItemChanged(clickedPosition);

                SelectedItem selectedItem = getSelectedItem();
                Log.d(LOG_TAG, "count selected item: " + selectedItem.getCount() + " selected item ID list: " + selectedItem.getSelectedItemIds());


                mMultipleSelectAdapterCallback.itemClicked(selectedItem.getCount(), selectedItem.getSelectedItemIds());

            }

//            mExpandButton.setBackground(ResourceUtil.acquireDrawable(mContext, R.color.pink_500));
//            mPrimaryTextView.setBackground(ResourceUtil.acquireDrawable(mContext, R.color.blue_500));

        }
    }

    private SelectedItem getSelectedItem() {
        SelectedItem selectedItem = new SelectedItem();
        int counter = 0;
        if (mItemForMultipleSelectionList != null) {
            for (ItemForMultipleSelection item : mItemForMultipleSelectionList) {
                if (item.isChecked()) {
                    counter++;
                    selectedItem.addItemIds(item.getId());
                }
            }
        }

        selectedItem.setCount(counter);

        return selectedItem;

    }

    public interface MultipleSelectAdapterCallback {
        public void itemClicked(int count, List<Integer> selectedItemId);

    }

    public MultipleSelectAdapterCallback getMultipleSelectAdapterCallback() {
        return mMultipleSelectAdapterCallback;
    }

    public void setMultipleSelectAdapterCallback(MultipleSelectAdapterCallback multipleSelectAdapterCallback) {
        mMultipleSelectAdapterCallback = multipleSelectAdapterCallback;
    }



    @Override
    public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
        Log.d(LOG_TAG, "ON create view holder " + i);

        View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.default_list_item_layout, viewGroup, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(ViewHolder viewHolder, int i) {

        viewHolder.mPrimaryTextView.setText(mItemForMultipleSelectionList.get(i).getPrimaryText());
        viewHolder.mSupportingTextView.setText(mItemForMultipleSelectionList.get(i).getSupportingText());

        // change primary text view background color 
        
        if (mItemForMultipleSelectionList.get(i).isChecked()) {
            viewHolder.mPrimaryTextView.setBackground(ResourceUtil.acquireDrawable(mContext,R.color.orange_500));

        } else {
            viewHolder.mPrimaryTextView.setBackground(ResourceUtil.acquireDrawable(mContext,R.color.green_500));
        }

        // expand supporting text view
        
        if (mItemForMultipleSelectionList.get(i).isActivateExpansion()) {
            viewHolder.mSupportingTextView.setVisibility(View.VISIBLE);
        } else {
            viewHolder.mSupportingTextView.setVisibility(View.GONE);
        }

        Log.d(LOG_TAG, "on Bind View Holder: " +  i);
        
    }

    @Override
    public int getItemCount() {

        if (mItemForMultipleSelectionList == null) return 0;
        else return mItemForMultipleSelectionList.size();
    }

    @Override
    public int getItemViewType(int position) {
        return super.getItemViewType(position);
    }

    public void swapData(List<ItemForMultipleSelection> itemForMultipleSelectionList) {
        mItemForMultipleSelectionList = itemForMultipleSelectionList;
    }

    public List<ItemForMultipleSelection> getData() {
        if (mItemForMultipleSelectionList != null){
            return mItemForMultipleSelectionList;
        } else return null;
    };

    // That class only for return # of selected items and their ids 
    
    private class SelectedItem {

        private int mCount;
        private List<Integer> mSelectedItemIds = new ArrayList<>();

        private void addItemIds(int id) {
            mSelectedItemIds.add(id);
        }

        public int getCount() {
            return mCount;
        }

        public void setCount(int count) {
            mCount = count;
        }

        public List<Integer> getSelectedItemIds() {
            return mSelectedItemIds;
        }
    }

}



//////////////////////////////////////////////////////////////////


This is a long code, so I break it into small pieces.
What is the most important part of the code is ViewHolder

    public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener{


        public TextView mPrimaryTextView;
        public TextView mSupportingTextView;
        public ImageButton mExpandButton;


        public ViewHolder(View itemView) {
            super(itemView);


            mPrimaryTextView = (TextView) itemView.findViewById(R.id.list_item_primary_text);
            mSupportingTextView = (TextView) itemView.findViewById(R.id.list_item_supporting_text);


            mExpandButton = (ImageButton) itemView.findViewById(R.id.list_item_expand_button);
            mExpandButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {

                    if (getAdapterPosition() != RecyclerView.NO_POSITION) {

                        if (mItemForMultipleSelectionList.get(getAdapterPosition()).isActivateExpansion()) {

                            mItemForMultipleSelectionList.get(getAdapterPosition()).setActivateExpansion(false);
                        } else {
                            mItemForMultipleSelectionList.get(getAdapterPosition()).setActivateExpansion(true);
                        }
                        notifyItemChanged(getAdapterPosition());
                    }

                }
            });

            itemView.setOnClickListener(this);

        }

        @Override
        public void onClick(View v) {

            Log.d(LOG_TAG,"Clicked");

            if (mMultipleSelectAdapterCallback != null && getAdapterPosition() != RecyclerView.NO_POSITION) {

                int clickedPosition = getAdapterPosition();
                ItemForMultipleSelection clickedItem = mItemForMultipleSelectionList.get(clickedPosition);

                if (clickedItem.isChecked()) {
                    clickedItem.setChecked(false);
                } else {
                    clickedItem.setChecked(true);
                }

                notifyItemChanged(clickedPosition);

                SelectedItem selectedItem = getSelectedItem();
                Log.d(LOG_TAG, "count selected item: " + selectedItem.getCount() + " selected item ID list: " + selectedItem.getSelectedItemIds());


                mMultipleSelectAdapterCallback.itemClicked(selectedItem.getCount(), selectedItem.getSelectedItemIds());

            }

//            mExpandButton.setBackground(ResourceUtil.acquireDrawable(mContext, R.color.pink_500));
//            mPrimaryTextView.setBackground(ResourceUtil.acquireDrawable(mContext, R.color.blue_500));

        }
    }


I write two setOnClickListener. 
First one is for expand button, right bottom of the list item.
When I click the button, it invokes onClickListner. 
It changs the field 'mActivateExpansion' to true of false and call notifyItemChanged()
Second one is for for itemview itself and works almost same as first one does.
But, it also calls
'mMultipleSelectAdapterCallback.itemClicked(selectedItem.getCount(), selectedItem.getSelectedItemIds());'
That is a callback method for invoking ActionMode, I explain it later.

note: notifyItemChanged method is extremely important. 
When notifyItemChanged is invoked, onBindViewHolder method is invoked.

From the documentation https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html#onBindViewHolder(VH, int)

onBindViewHolder does the following things
Called by RecyclerView to display the data at the specified position. This method should update the contents of the itemView to reflect the item at the given position.
So, lets see the  implementation of onBindViewHolder


  @Override
    public void onBindViewHolder(ViewHolder viewHolder, int i) {

        viewHolder.mPrimaryTextView.setText(mItemForMultipleSelectionList.get(i).getPrimaryText());
        viewHolder.mSupportingTextView.setText(mItemForMultipleSelectionList.get(i).getSupportingText());

        // change primary text view background color 
        
        if (mItemForMultipleSelectionList.get(i).isChecked()) {
            viewHolder.mPrimaryTextView.setBackground(ResourceUtil.acquireDrawable(mContext,R.color.orange_500));

        } else {
            viewHolder.mPrimaryTextView.setBackground(ResourceUtil.acquireDrawable(mContext,R.color.green_500));
        }

        // expand supporting text view
        
        if (mItemForMultipleSelectionList.get(i).isActivateExpansion()) {
            viewHolder.mSupportingTextView.setVisibility(View.VISIBLE);
        } else {
            viewHolder.mSupportingTextView.setVisibility(View.GONE);
        }

        Log.d(LOG_TAG, "on Bind View Holder: " +  i);
        
    }


mPrimaryTextView: set its background color ( orange or yellow) 
mSupportingTextView: set its visibility (visible or gone)



Next,  I want to show how to implement ActionMode when click the views.





4, Create Fragment Class

package com.omzi43kf.lgaji.recyclerviewmulti.fragment;

import android.os.Bundle;

import android.support.v4.app.Fragment;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;

import com.omzi43kf.lgaji.recyclerviewmulti.ItemForMultipleSelection;
import com.omzi43kf.lgaji.recyclerviewmulti.R;
import com.omzi43kf.lgaji.recyclerviewmulti.adapter.MultipleSelectAdapter;

import java.util.ArrayList;
import java.util.List;

/**
 * A simple {@link Fragment} subclass.
 */
public class MultipleSelectionFragment extends Fragment {

    private static final String LOG_TAG = MultipleSelectionFragment.class.getSimpleName();
    private RecyclerView mRecyclerView;
    private LinearLayoutManager mLinearLayoutManager;
    private MultipleSelectAdapter mMultipleSelectAdapter;
    private ActionMode mActionMode;
    private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() {

        @Override
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            MenuInflater inflater = mode.getMenuInflater();
            inflater.inflate(R.menu.context_menu, menu);
            return true;
        }

        @Override
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            return false;
        }

        @Override
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            return false;
        }

        @Override
        public void onDestroyActionMode(ActionMode mode) {
            mActionMode = null;
        }
    };


    public static MultipleSelectionFragment newInsrance() {
        MultipleSelectionFragment multipleSelectionFragment = new MultipleSelectionFragment();
        return multipleSelectionFragment;

    }



    public MultipleSelectionFragment() {
        // Required empty public constructor
    }


    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_multiple_selection, container, false);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        mRecyclerView = (RecyclerView) view.findViewById(R.id.fragment_multiple_selection_recycler_view);
        mLinearLayoutManager = new LinearLayoutManager(getActivity());
        mRecyclerView.setLayoutManager(mLinearLayoutManager);

        mMultipleSelectAdapter = new MultipleSelectAdapter(getActivity());
        mRecyclerView.setAdapter(mMultipleSelectAdapter);

        mMultipleSelectAdapter.setMultipleSelectAdapterCallback(new MultipleSelectAdapter.MultipleSelectAdapterCallback() {
            @Override
            public void itemClicked(int count, List<Integer> selectedItemId) {
                Log.d(LOG_TAG, "Clicked");
                if (count ==0 & mActionMode != null) {
                    mActionMode.finish();
                } else {
                    mActionMode = getActivity().startActionMode(mActionModeCallback);
                    mActionMode.setTitle("Selected Item #: " + count);

                }
            }
        });

        List<ItemForMultipleSelection> itemForMultipleSelectionList = generateDummyData();



        mMultipleSelectAdapter.swapData(itemForMultipleSelectionList);




    }

    private List<ItemForMultipleSelection> generateDummyData() {

        List<ItemForMultipleSelection> itemForMultipleSelectionList = new ArrayList<>();
        for (int i = 0; i < 500; i++) {

            ItemForMultipleSelection itemForMultipleSelection = new ItemForMultipleSelection();


            itemForMultipleSelection.setPrimaryText("Primary Text: " + i);
            itemForMultipleSelection.setSupportingText("Supporting Text: " + i);
            itemForMultipleSelection.setId(i);
            itemForMultipleSelection.setChecked(false);
            itemForMultipleSelection.setActivateExpansion(false);

            itemForMultipleSelectionList.add(itemForMultipleSelection);

        }
        return itemForMultipleSelectionList;
    }
}


Essential parts of this class may be following codes.

I am not going to show the details of ActionMode
This link would be helpful  http://developer.android.com/guide/topics/ui/menus.html


 private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() {

        @Override
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            MenuInflater inflater = mode.getMenuInflater();
            inflater.inflate(R.menu.context_menu, menu);
            return true;
        }

        @Override
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            return false;
        }

        @Override
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            return false;
        }

        @Override
        public void onDestroyActionMode(ActionMode mode) {
            mActionMode = null;
        }
    };


 @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        mRecyclerView = (RecyclerView) view.findViewById(R.id.fragment_multiple_selection_recycler_view);
        mLinearLayoutManager = new LinearLayoutManager(getActivity());
        mRecyclerView.setLayoutManager(mLinearLayoutManager);

        mMultipleSelectAdapter = new MultipleSelectAdapter(getActivity());
        mRecyclerView.setAdapter(mMultipleSelectAdapter);

        mMultipleSelectAdapter.setMultipleSelectAdapterCallback(new MultipleSelectAdapter.MultipleSelectAdapterCallback() {
            @Override
            public void itemClicked(int count, List<Integer> selectedItemId) {
                Log.d(LOG_TAG, "Clicked");
                if (count ==0 & mActionMode != null) {
                    mActionMode.finish();
                } else {
                    mActionMode = getActivity().startActionMode(mActionModeCallback);
                    mActionMode.setTitle("Selected Item #: " + count);

                }
            }
        });

        List<ItemForMultipleSelection> itemForMultipleSelectionList = generateDummyData();



        mMultipleSelectAdapter.swapData(itemForMultipleSelectionList);



    }

In addition to the above codes, you need to add this xml code to styles.xml
<item name="windowActionModeOverlay">true</item>

You check this link for understanding the necessity of the code
http://stackoverflow.com/questions/26443403/toolbar-and-contextual-actionbar-with-appcompat-v7


5, Wrap up

All the implementation may finish and the result would be like this

























1 件のコメント:

  1. the link is broken (404 error):
    https://github.com/omzi43kf/RecyclerViewMulti

    返信削除