Expandable RecyclerView List

The example below will create an expandable list which can expand and collapse when one of the list item is clicked.

The expand and collapse utility class.

import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.Transformation;

public class ExpandableUtil {
    /**
     * Expand the view to its normal height
     * @param v The view to be expanded
     * @param from The initial height before it is expanded
     * @param to The final height after it is expanded
     */
    public static void expand(final View v, Integer from, Integer to) {
        if (v == null) return;

        final int initialHeight = from == null ? 0 : from.intValue();
        final int finalHeight = to;
        final int expandAmount = finalHeight - initialHeight;


        v.measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        v.getLayoutParams().height = initialHeight;


        if (v.getVisibility() != View.VISIBLE) v.setVisibility(View.VISIBLE);
        Animation a = new Animation() {
            @Override
            protected void applyTransformation(float interpolatedTime, Transformation t) {
                if (interpolatedTime == 1) {
                    v.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
                } else {
                    v.getLayoutParams().height = initialHeight + (int)(expandAmount * interpolatedTime);
                }
                v.requestLayout();
            }

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

        a.setDuration((int)(finalHeight / v.getContext().getResources().getDisplayMetrics().density));
        v.startAnimation(a);
    }

    /**
     * Collapse the view by the collapseAmount value
     * @param v The view to be collapsed
     * @param baseHeight The height to be maintained
     */
    public static void collapse(final View v, final Integer collapseAmount) {
        if (v == null) return;

        final int initialHeight = v.getMeasuredHeight();

        Animation a = new Animation() {
            @Override
            protected void applyTransformation(float interpolatedTime, Transformation t) {
                Log.d("pye", "collapse collapseAmount: " + collapseAmount + " initialHeight: " + initialHeight);

                if (interpolatedTime == 1) {
                    if (collapseAmount == initialHeight) {
                        v.setVisibility(View.INVISIBLE);
                    }
                }
                v.getLayoutParams().height = initialHeight - (int)(collapseAmount * interpolatedTime);
                v.requestLayout();
            }

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

        a.setDuration((int)(initialHeight / v.getContext().getResources().getDisplayMetrics().density));
        v.startAnimation(a);
    }

    /**
     * Collapse the view by the collapseAmount value with no animation
     * @param v The view to be collapsed
     * @param collapseAmount The height amount to be collapsed
     */
    public static void collapseWithNoAnimation(final View v, Integer collapseAmount) {
        if (v == null) return;
        final int initialHeight = v.getMeasuredHeight();
        v.getLayoutParams().height = initialHeight - collapseAmount;
        v.requestLayout();
    }
}

The layout activity_expandable_list.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/card_background"
    android:layout_height="wrap_content"
    android:layout_width="match_parent"
    android:clipChildren="true"
    android:clipToPadding="true"
    app:cardCornerRadius="5dp"
    app:cardElevation="3dp"
    app:cardUseCompatPadding="true">
    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
    </android.support.v7.widget.RecyclerView>
</android.support.v7.widget.CardView>

The layout for the items on the list, activity_place_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="@dimen/place_item_height">
    <TextView
        android:id="@+id/row_item"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"/>
</LinearLayout>

The Activity, ExpandableListActivity.java

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.CardView;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import com.example.expandablerecyclerview.R;

import java.util.ArrayList;

public class ExpandableListActivity extends AppCompatActivity implements ListItemListener {
    ArrayList<Place> places;
    CardView cvContainer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_expandable_list);

        places = new ArrayList<>();
        for (int i=0; i<6; i++) {
            places.add(new Place("place " + i));
        }

        cvContainer = (CardView) findViewById(R.id.card_background);
        RecyclerView rvList = (RecyclerView) findViewById(R.id.rv_list);
        PlaceArrayAdapter placeArrayAdapter = new PlaceArrayAdapter(places);
        rvList.setLayoutManager(new LinearLayoutManager(this));
        rvList.setItemAnimator(new DefaultItemAnimator());
        rvList.setAdapter(placeArrayAdapter);

        placeArrayAdapter.setListItemListener(this);


        rvList.post(new Runnable() {
            @Override
            public void run() {
                int baseHeight =  Math.round(getResources().getDimension(R.dimen.place_item_height));
                int collapseAmount = baseHeight * (places.size() - 1);
                ExpandableUtil.collapseWithNoAnimation(cvContainer, collapseAmount);
            }
        });

    }
    @Override
    public void onExpand() {
        int from =  Math.round(getResources().getDimension(R.dimen.place_item_height));
        int to = from * places.size();
        ExpandableUtil.expand(cvContainer, from, to);
    }

    @Override
    public void onCollapse() {
        int baseHeight =  Math.round(getResources().getDimension(R.dimen.place_item_height));
        int collapseAmount = baseHeight * (places.size() - 1);
        ExpandableUtil.collapse(cvContainer, collapseAmount);
    }
}

The ListItemListener.java

public interface ListItemListener {
    void onExpand();
    void onCollapse();
}

The RecyclerView Adapter

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.TextView;
import com.example.expandablerecyclerview.R;
import java.util.ArrayList;

public class PlaceArrayAdapter extends RecyclerView.Adapter<PlaceArrayAdapter.ViewHolder> {
    private ArrayList<Place> places;
    private ListItemListener listItemListener;
    private boolean isExpanded = false;

    public PlaceArrayAdapter(ArrayList<Place> places) {
        this.places = places;
    }

    public void setListItemListener(ListItemListener listItemListener) {
        this.listItemListener = listItemListener;
    }


    // get the size of the list
    @Override
    public int getItemCount() {
        return places == null ? 0 : places.size();
    }


    // specify the row layout file and click for each row
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.activity_place_item, parent, false);
        ViewHolder myViewHolder = new ViewHolder(view);
        return myViewHolder;
    }

    // load data in each row element
    @Override
    public void onBindViewHolder(final ViewHolder holder, final int listPosition) {
        TextView item = holder.item;
        item.setText(places.get(listPosition).getName());


        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (isExpanded) {
                    listItemListener.onCollapse();
                    isExpanded = false;
                } else {
                    listItemListener.onExpand();
                    isExpanded = true;
                }
            }
        });

    }

    // Static inner class to initialize the views of rows
    class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
        public TextView item;
        public ViewHolder(View itemView) {
            super(itemView);
            itemView.setOnClickListener(this);
            item = (TextView) itemView.findViewById(R.id.row_item);
        }
        @Override
        public void onClick(View view) {
            Log.d("onclick", "onClick " + getLayoutPosition() + " " + item.getText());
        }
    }
}

The Place.java

public class Place {
    private String name;

    public Place(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Complete example in Github

Search within Codexpedia

Custom Search

Search the entire web

Custom Search