↩ home



Animate your list of items with physic-based animation

21 Jun 2016

I explain here how to animate your items within a RecyclerView, with a slide-in from bottom animation and a rebound effect.

The technique is simple and well-known but still may not documented enough. I also show how to use Facebook Rebound which provide a bit of real-world physics in your apps.

A good library to implement RecyclerView animations can be found here https://github.com/wasabeef/recyclerview-animators.

Videos


Start with a basic photo list app

Firstly I need a simple app with a RecyclerView. I assume your already know how to do this, no need to explain more precisly this step. My app displays some photos from Internet within cards.

MainActivity.java

    public class MainActivity extends AppCompatActivity {
        private RecyclerView mRecyclerView;
        private RecyclerView.Adapter mAdapter;
        private RecyclerView.LayoutManager mLayoutManager;

        @Override
        protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Only contains a RecyclerView.
        setContentView(R.layout.activity_main);

        mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        mRecyclerView.setHasFixedSize(true);
        mLayoutManager = new LinearLayoutManager(this);
        mRecyclerView.setLayoutManager(mLayoutManager);

        // Our classic custom adapter.
        mAdapter = new MyAdapter(mRecyclerView);
        mRecyclerView.setAdapter(mAdapter);
        }
    }

MyAdapter.java

    /**
     * Classic custom adapter.
     */
    public class MyAdapter extends RecyclerView.Adapter {

        /**
         * Classic ViewHolder.
         */
        public static class ViewHolder extends RecyclerView.ViewHolder {
        public TextView mTextView;
        public ImageView mBackground;

        public ViewHolder(View v) {
            super(v);
            mTextView = (TextView) v.findViewById(R.id.text);
            mBackground = (ImageView) v.findViewById(R.id.image);
        }
        }

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.view_item, parent, false);
        ViewHolder vh = new ViewHolder(v);
        return vh;
        }

        @Override
        public void onBindViewHolder(ViewHolder holder, int position) {
        holder.mTextView.setText(getTitle(position));

        Glide.with(holder.mBackground.getContext())
                .load(getBackgroundImage(position))
                .centerCrop()
                .into(holder.mBackground);
        }

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

        /**
         * Random data.
         */
        private String getTitle(int position) {
        if (position % 5 == 0) {
            return "Shoreline of Algarve Portugal";
        } else if (position % 4 == 0) {
            return "Houses and buildings in Porto";
        } else if (position % 3 == 0) {
            return "Pavilhão Rosa Mota";
        } else if (position % 2 == 0) {
            return "Tower at dusk in Lisbon";
        } else {
            return "Dramatic clouds over Lisbon";
        }
        }

        /**
         * Random data.
         */
        private String getBackgroundImage(int position) {
        if (position % 5 == 0) {
            return "https://www.goodfreephotos.com/albums/portugal/other-portugal/shoreline-of-algarve-portugal.jpg";
        } else if (position % 4 == 0) {
            return "https://www.goodfreephotos.com/albums/portugal/porto/houses-and-buildings-in-porto-portugal.jpg";
        } else if (position % 3 == 0) {
            return "https://www.goodfreephotos.com/albums/portugal/porto/Pavilh%C3%A3o-Rosa-Mota-in-porto-portugal.jpg";
        } else if (position % 2 == 0) {
            return "https://www.goodfreephotos.com/albums/portugal/lisbon/tower-at-dusk-in-lisbon-portugal.jpg";
        } else {
            return "https://www.goodfreephotos.com/albums/portugal/lisbon/dramatic-clouds-over-lisbon-portugal.jpg";
        }
        }
    }

First initialization animation

The animation consists to move item view from bottom to initial y-coordinate. Using translationY is perfect for that.

move item view outside → translationY = height of screen.
animate entrance from bottom → decrease translationY until 0.

Let's play with Facebook Rebound to get this animation nice (https://github.com/facebook/rebound).

Slide-in from bottom animation with rebound:

    final int height = mRecyclerView.getResources().getDisplayMetrics().heightPixels;

    // Init physics system
    SpringSystem springSystem = SpringSystem.create();

    // Move item to bottom outside recyclerview
    itemView.setTranslationY(height);

    // Configure new spring
    SpringConfig config = new SpringConfig(250, 18);
    Spring spring = springSystem.createSpring();
    spring.setSpringConfig(config);

    spring.addListener(new SimpleSpringListener() {
        @Override
        public void onSpringUpdate(Spring spring) {
        /**
        * Decrease translationY until 0.
        */
        float val = (float) (height - spring.getCurrentValue());
        itemView.setTranslationY(val);
        }
    });

    // Set the spring in motion; moving from 0 to height
    spring.setEndValue(height);

250 is the tension and 18 is the friction : change these values and see what's going on.

As a starting point for this animation, we will use onCreateViewHolder which is called for views creation (or inflation). We must also take care animations will not be called after first initialization.

Integration in our RecyclerView:

    private boolean mFirstViewInit = true;

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.view_item, parent, false);
        ViewHolder vh = new ViewHolder(v);

        if (mFirstViewInit) {

           /**
           Perform animation.
           Add in SimpleSpringListener :

              @Override
              public void onSpringEndStateChange(Spring spring) {
                 mFirstViewInit = false;
              }

           **/

        return vh;
    }

Note : to improve the effect I added a delay of 70ms between each item animation in the final code.

Animate items when scrolling

I want to animate item's entrance when I scroll, items must enter from bottom with the same slide-in effect. We can see this animation on Google Plus app (and has been popularized by Instagram app so it's sometimes called the "instagram effect").

This time we use onBindViewHolder event which is called when an item's view is recycled for displaying. For a better experience, we won't animate everytime items we display when scrolling but once by item.

    private int mLastPosition;

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        holder.mTextView.setText(getTitle(position));

        Glide.with(holder.mBackground.getContext())
        .load(getBackgroundImage(position))
        .centerCrop()
        .into(holder.mBackground);

        if (!mFirstViewInit && position > mLastPosition) {
        /**
         * Slide view from bottom - same animation code to use here
         **/

        mLastPosition = position; // Animate only one time each item.
        }
    }

Final source code

You can find final source code with more organized methods and classes on Github : https://github.com/anthony-skr/RecyclerViewReboundEntrance



Top