Expanding/Collapsing RecyclerView row smoothly in Android

    When using some shopping application, we can see that when products have been sorted into a list view, we can expand any list row by clicking on it to read more details about this product, and of course, we can collapse it by clicking again, too! This expanding/collapsing process is very smoothly because of animations when visible and gone view.

Reality of Visible/Gone View

    In order to show/hide layout, we are used to using setVisibility() method of View, by setting the parameter is View.VISIBLE or View.GONE, the layout will be shown or hidden! If this process has an animation, we will see expanding/collapsing view effect. The fact that, adding animation when show/hide views is not easy as we imagine, you can read this discussion on StackOverflow.
    So, fortunately, a lot of experienced programmers has developed many libraries to help us making this effect. In this post, I would like to present one of them: ExpandableLayout, which very useful to expanding/collapsing view when user click on it!
    DEMO VIDEO:


Importing library

    Go to the library page on Github, like another libraries, you can add it's dependency to you application level build.gradle, but the smarter way is adding 2 files: ExpandableLayout.java and attr.xml to your Android Studio Project (put ExpandableLayout.java to your src/main/java/[your_package] folder and attr.xml to  res/values folder respectively).

Expandable layout usage

    As it's description, ExpandableLayout has two children views, the first is visible layout and the seconds one is hidden layout. It's structure in xml file:
<com.silencedut.expandablelayout.ExpandableLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:expWithParentScroll="true"
    app:expDuration = "300"
    app:expExpandScrollTogether = "false"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    
    <!-- The visible layout -->
    <layout1
    ...
    />

    <!-- The hidden layout -->
    <layout2
    ...
    />

</com.silencedut.expandablelayout.ExpandableLayout>
    In my sample project, I will make a list view using RecyclerView and each list row is an ExpandableLayout, so I have these xml files for odd and even row:
item_odd.xml
<?xml version="1.0" encoding="utf-8"?>
<info.devexchanges.smoothexpandcollapserecyclerview.ExpandableLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/expandable_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    app:expWithParentScroll="true">

    <!-- The visible layout -->
    <RelativeLayout
        android:id="@+id/first_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#00bcd4">

        <ImageView
            android:id="@+id/image_view"
            android:layout_width="80dp"
            android:layout_height="80dp"
            android:layout_margin="5dp"
            android:contentDescription="@null"
            android:scaleType="fitXY"
            android:src="@drawable/one" />

        <TextView
            android:id="@+id/text_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="5dp"
            android:layout_toRightOf="@+id/image_view"
            android:text="@string/ezreal"
            android:textColor="@android:color/white"
            android:textSize="18sp" />

        <TextView
            android:id="@+id/show_info"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/text_view"
            android:layout_centerVertical="true"
            android:layout_marginLeft="5dp"
            android:drawablePadding="5dp"
            android:layout_marginTop="10dp"
            android:drawableRight="@drawable/arrow_down"
            android:layout_toRightOf="@+id/image_view"
            android:text="Show description"
            android:textColor="@android:color/white" />

    </RelativeLayout>

    <!-- The hidden layout -->

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_green_dark"
        android:orientation="vertical"
        android:padding="10dp">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/ezreal_story"
            android:textColor="@android:color/white" />

    </LinearLayout>

</info.devexchanges.smoothexpandcollapserecyclerview.ExpandableLayout>
item_even.xml
<?xml version="1.0" encoding="utf-8"?>
<info.devexchanges.smoothexpandcollapserecyclerview.ExpandableLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/expandable_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    app:expWithParentScroll="true">

    <RelativeLayout
        android:id="@+id/firstLayer"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#6889ff">

        <ImageView
            android:id="@+id/image_view"
            android:layout_width="80dp"
            android:layout_height="80dp"
            android:layout_margin="5dp"
            android:scaleType="fitXY"
            android:src="@drawable/two" />

        <TextView
            android:id="@+id/text_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="5dp"
            android:layout_toRightOf="@+id/image_view"
            android:text="@string/title"
            android:textColor="@android:color/white"
            android:textSize="18sp" />

        <TextView
            android:id="@+id/show_info"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/text_view"
            android:layout_centerVertical="true"
            android:layout_marginLeft="5dp"
            android:layout_marginTop="10dp"
            android:layout_toRightOf="@+id/image_view"
            android:drawablePadding="5dp"
            android:drawableRight="@drawable/arrow_down"
            android:text="Show description"
            android:textColor="@android:color/white" />

    </RelativeLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimary"
        android:orientation="vertical"
        android:padding="10dp">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/jinx_story"
            android:textColor="@android:color/white"
            android:textSize="12sp" />

    </LinearLayout>

</info.devexchanges.smoothexpandcollapserecyclerview.ExpandableLayout>
    In Java code, we can handle expand/collapse event by call setOnExpandListener(OnExpandListener listener). We'll put this code in RecyclerView adapter class, the most important file of the project:
RecyclerViewAdapter.java
package info.devexchanges.smoothexpandcollapserecyclerview;

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;

import java.util.HashSet;

public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.ReyclerViewHolder> {
    private LayoutInflater layoutInflater;
    private HashSet<Integer> expandedPositionSet;
    private Context context;

    public RecyclerViewAdapter(Context context) {
        this.layoutInflater = LayoutInflater.from(context);
        expandedPositionSet = new HashSet<>();
        this.context = context;
    }

    @Override
    public ReyclerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View item = layoutInflater.inflate(viewType == 0 ? R.layout.item_odd : R.layout.item_even, parent, false);

        return new ReyclerViewHolder(item);
    }

    @Override
    public void onBindViewHolder(ReyclerViewHolder holder, int position) {
        holder.updateItem(position);
    }

    @Override
    public int getItemViewType(int position) {
        return position % 2;
    }

    @Override
    public int getItemCount() {
        return 20;
    }

    class ReyclerViewHolder extends RecyclerView.ViewHolder {
        private ExpandableLayout expandableLayout;
        private TextView showInfo;

        private ReyclerViewHolder(final View view) {
            super(view);
            expandableLayout = (ExpandableLayout) view.findViewById(R.id.expandable_layout);
            showInfo = (TextView) view.findViewById(R.id.show_info);
        }

        private void updateItem(final int position) {
            expandableLayout.setOnExpandListener(new ExpandableLayout.OnExpandListener() {
                @Override
                public void onExpand(boolean expanded) {
                    registerExpand(position, showInfo);
                }
            });
            expandableLayout.setExpand(expandedPositionSet.contains(position));

        }
    }

    private void registerExpand(int position, TextView textView) {
        if (expandedPositionSet.contains(position)) {
            removeExpand(position);
            textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.arrow_down, 0);
            textView.setText("Show description");
            Toast.makeText(context, "Position: " + position + " collapsed!", Toast.LENGTH_SHORT).show();
        } else {
            addExpand(position);
            textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.arrow_up, 0);
            textView.setText("Hide description");
            Toast.makeText(context, "Position: " + position + " expanded!", Toast.LENGTH_SHORT).show();
        }
    }

    private void removeExpand(int position) {
        expandedPositionSet.remove(position);
    }

    private void addExpand(int position) {
        expandedPositionSet.add(position);
    }
}
    In main activity code, setting LayoutManager (vertical LinearLayout) for RecyclerView:
MainActivity.java
package info.devexchanges.smoothexpandcollapserecyclerview;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;

public class MainActivity extends AppCompatActivity {

    private RecyclerView recyclerView;

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

        recyclerView = (RecyclerView)findViewById(R.id.recycler_view);

        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(linearLayoutManager);

        RecyclerViewAdapter recyclerViewAdapter = new RecyclerViewAdapter(this);
        recyclerView.setAdapter(recyclerViewAdapter);
    }
}
    Note: Make sure that you added RecyclerView dependency to your application level build.gradle:
dependencies {
    compile 'com.android.support:recyclerview-v7:24.1.1'
    compile 'com.android.support:appcompat-v7:24.1.1'
}
    Running application, we'll have this output:

Conclusions

    Through this post, I presented a third-party library which can help us to make an expandable layout. You can apply it to your development work, especially in shopping applications. Moreover, you can use Animation from xml to make this effect, I will present this solution at next post.
    References:

Share


Previous post
« Prev Post
Next post
Next Post »