Expanding/Collapsing RecyclerView row smoothly with Animation in Android

    As you can see, I had a post about this topic: creating expansible/collapsible layout with animation by using a third-party library and apply it as list view row to show more/hide apart item details. Basically, like another libraries, it work very well and this is the best solution. But, by another way, you can use Animation and AnimationUtils in Android SDK, declaring your animations from your own xml files.
    In this post, I will present this way to create an expandable layout with animation and apply it as a RecyclerView row.

Creating animation from xml file

    Create an xml file which defines type of animation to perform. This file should be located under anim folder under res directory. If you don’t have anim folder in your res directory create one.
    Because of these are expanding and collapsing animations, so I have 2 xml files called slide_down.xml and slide_up.xml.
   Sliding animation uses <scale> tag only. Slide down is exactly opposite to slide down (expanding) animation. Just interchange android:fromYScale and android:toYScale values:
slide_down.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true">

    <scale
        android:duration="500"
        android:fromXScale="1.0"
        android:fromYScale="0.0"
        android:interpolator="@android:anim/linear_interpolator"
        android:toXScale="1.0"
        android:toYScale="1.0" />

</set>
Slide up can be achieved by setting android:fromYScale="1.0" and android:toYScale="0.0":
slide_up.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true" >

    <scale
        android:duration="500"
        android:fromXScale="1.0"
        android:fromYScale="1.0"
        android:interpolator="@android:anim/linear_interpolator"
        android:toXScale="1.0"
        android:toYScale="0.0" />

</set>

Usage in Activity

    With Animation and AnimationUtils, we can create an animation for our layout easily. Suppose we have this activity layout:
activity_ex_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/content_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_green_dark"
        android:clickable="true"
        android:onClick="toggle_contents"
        android:padding="10dp"
        android:text="@string/title"
        android:textColor="@android:color/white" />

    <!--content to hide/show -->
    <TextView
        android:id="@+id/title_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_blue_dark"
        android:padding="10dp"
        android:text="@string/long_content"
        android:textColor="@android:color/white" />

</LinearLayout>
    And we have this activity Java code:
ExpandableLayoutActivity.java
package info.devexchanges.expandablelayout;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.TextView;

public class ExpandableLayoutActivity extends AppCompatActivity {

    private TextView txtContent;
    private Animation animationUp;
    private Animation animationDown;

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

        txtContent = (TextView) findViewById(R.id.title_text);
        TextView txtTitle = (TextView) findViewById(R.id.content_text);
        txtContent.setVisibility(View.GONE);

        animationUp = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.slide_up);
        animationDown = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.slide_down);

        txtTitle.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if(txtContent.isShown()){
                    txtContent.setVisibility(View.GONE);
                    txtContent.startAnimation(animationUp);
                }
                else{
                    txtContent.setVisibility(View.VISIBLE);
                    txtContent.startAnimation(animationDown);
                }
            }
        });
    }
}
    Running this activity, we'll have this output:

Use as a RecyclerView row

    As applying these animations in each RecyclerView row, there are some problems with this widget structure, the collapsing animation will not work if you only call setVisibility(GONE) and startAnimation(animationUp) continuously like above, the layout will gone immediately and we won't see the animation. To avoid this matter, providing a CountdownTimer and gone the view when it finished:

holder.contentLayout.startAnimation(animationUp);
CountDownTimer countDownTimerStatic = new CountDownTimer(COUNTDOWN_RUNNING_TIME, 16) {
                        @Override
                        public void onTick(long millisUntilFinished) {
                        }

                        @Override
                        public void onFinish() {
                            holder.contentLayout.setVisibility(View.GONE);
                        }
                    };
                    countDownTimerStatic.start();
    Our RecyclerView adapter is simple like this:
RecyclerAdapter.java
package info.devexchanges.expandablelayout;

import android.content.Context;
import android.os.CountDownTimer;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.widget.ImageView;
import android.widget.TextView;

public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerAdapter.ReyclerViewHolder> {
    private LayoutInflater layoutInflater;
    private Animation animationUp, animationDown;
    private Context context;
    private final int COUNTDOWN_RUNNING_TIME = 500;

    public RecyclerAdapter(Context context, Animation animationUp, Animation animationDown) {
        this.layoutInflater = LayoutInflater.from(context);
        this.animationDown = animationDown;
        this.animationUp = animationUp;
        this.context = context;
    }

    @Override
    public ReyclerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View item = layoutInflater.inflate(R.layout.item_recycler_view, parent, false);

        return new ReyclerViewHolder(item);
    }

    @Override
    public void onBindViewHolder(final ReyclerViewHolder holder, int position) {
        if (position % 3 == 0) {
            holder.image.setImageResource(R.drawable.girl_1);
        } else if (position % 3 == 1) {
            holder.image.setImageResource(R.drawable.girl_2);
        } else {
            holder.image.setImageResource(R.drawable.girl_3);
        }

        holder.showMore.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (holder.contentLayout.isShown()) {
                    holder.contentLayout.startAnimation(animationUp);

                    CountDownTimer countDownTimerStatic = new CountDownTimer(COUNTDOWN_RUNNING_TIME, 16) {
                        @Override
                        public void onTick(long millisUntilFinished) {
                        }

                        @Override
                        public void onFinish() {
                            holder.contentLayout.setVisibility(View.GONE);
                        }
                    };
                    countDownTimerStatic.start();

                    holder.showMore.setText(context.getString(R.string.show));
                    holder.showMore.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.arrow_down, 0);
                } else {
                    holder.contentLayout.setVisibility(View.VISIBLE);
                    holder.contentLayout.startAnimation(animationDown);

                    holder.showMore.setText(context.getString(R.string.hide));
                    holder.showMore.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.arrow_up, 0);
                }
            }
        });
    }

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

    class ReyclerViewHolder extends RecyclerView.ViewHolder {
        private ImageView image;
        private TextView showMore;
        private TextView contentLayout;

        private ReyclerViewHolder(final View v) {
            super(v);

            image = (ImageView) v.findViewById(R.id.image);
            contentLayout = (TextView) v.findViewById(R.id.content);
            showMore = (TextView) v.findViewById(R.id.show_more);
        }
    }
}
Important Note: COUNTDOWN_RUNNING_TIME value must equal with animation duration in our xml file (here is 500 milliseconds).
    Layout for each RecyclerView row:
item_recycler_view.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="6dp">

    <ImageView
        android:id="@+id/image"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:contentDescription="@null"
        android:scaleType="fitXY"
        android:src="@drawable/girl_1" />

    <TextView
        android:id="@+id/title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@+id/image"
        android:padding="10dp"
        android:text="@string/app_name"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/short_content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/title"
        android:layout_toRightOf="@+id/image"
        android:paddingLeft="10dp"
        android:text="@string/short_content"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/show_more"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/short_content"
        android:layout_toRightOf="@+id/image"
        android:drawablePadding="5dp"
        android:drawableRight="@drawable/arrow_down"
        android:paddingLeft="10dp"
        android:paddingTop="10dp"
        android:text="@string/show"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/image"
        android:background="@color/colorPrimary"
        android:drawablePadding="5dp"
        android:padding="5dp"
        android:text="@string/long_content"
        android:textColor="@android:color/white"
        android:textStyle="bold"
        android:visibility="gone" />

</RelativeLayout>
    Source code for the activity:
ListViewActivity.java
package info.devexchanges.expandablelayout;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;

public class ListViewActivity extends AppCompatActivity {

    private RecyclerView recyclerView;
    private Animation animationUp, animationDown;

    @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);

        animationUp = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.slide_up);
        animationDown = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.slide_down);

        RecyclerAdapter recyclerViewAdapter = new RecyclerAdapter(this, animationUp, animationDown);
        recyclerView.setAdapter(recyclerViewAdapter);
    }
}
    Running this activity, we'll have this output:

Final thoughts

    If you are an experienced programmer, you will not like this post because it not work well like the "library way" which I posted a few days ago, you should take a glance! Through this, I hope that readers can learn the way to create animations from xml files and use it in your layouts. The fact that adding animation to VISIBLE/GONE view process in Android is not simple as we think, programmers must have many experiences in animating layout, building animations or vector drawables... Finally, you can get my source code on Github.

Share


Previous post
« Prev Post
Next post
Next Post »