Adapter / Animation / RecyclerView

RecyclerView Animations – Moving Items

RecyclerView is a really useful way of displaying content in list form, particularly when the content is dynamic and / or there are large numbers of items. One thing that can be really useful is that we get some really nice animations for free provided we implement our Adapter correctly. For those that have converted from ListView there is a tendency to follow the same usage patterns when updating the data, but this will not get the best out of RecyclerView. In this short series we’ll take a look at the right way to modify the contents of a RecyclerView.Adapter in order to get these animations for free.

Previously we saw that by being specific about which items we have added and removed from the data backing our RecyclerView.Adapter instance we not only get some nice animations virtually for free, but we also make our RecyclerView more efficient by doing so.

Before we continue let’s discuss the effect that making more specific updates has on RecyclerView. It should be fairly obvious (I hope!) that if RecyclerView is only having to make updates to a small number of individual item Views then it is going to operate far more efficiently than if is is having to update everything. Also, RecyclerView will not attempt to infer the changes if you call notifyDataSetChanged(). If the changes are well known, as in the examples we’ve looked at so far, then calling the finer-grained functions to precisely specify what has changed is easy enough. However, sometimes an updated data list may come from an external source and the changes are not obvious. In such cases you can use DiffUtil to calculate the deltas between two lists and update an Adapter appropriately. Taking a look at how this update is actually performed internally we can see that it will use the finer-grained notify* functions to actually trigger the updates, and so we’ll get the same nice animations by using this tool. Hat-tip to Ash Davies for asking about DiffUtil, which prompted the inclusion in this article.

So let’s continue with another example which demonstrates the power of accurately specifying the changes to the list data. We can add another couple of buttons to each item to move it up and down in the list.

class MyAdapter(private val string: String) : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
    .
    .
    .
    inner class ViewHolder(
            itemView: View,
            private val textView: TextView = itemView.findViewById(android.R.id.text1),
            upButton: View = itemView.findViewById(R.id.up),
            downButton: View = itemView.findViewById(R.id.down),
            addButton: View = itemView.findViewById(R.id.add),
            removeButton: View = itemView.findViewById(R.id.remove)
    ) : RecyclerView.ViewHolder(itemView) {

        init {
            addButton.setOnClickListener(insert())
            removeButton.setOnClickListener(remove())
            upButton.setOnClickListener(moveUp())
            downButton.setOnClickListener(moveDown())
        }

        private fun insert(): (View) -> Unit = {
            layoutPosition.also { currentPosition ->
                items.add(currentPosition, uniqueString(string))
                notifyItemInserted(currentPosition)
            }
        }

        private fun remove(): (View) -> Unit = {
            layoutPosition.also { currentPosition ->
                items.removeAt(currentPosition)
                notifyItemRemoved(currentPosition)
            }
        }

        private fun moveUp(): (View) -> Unit = {
            layoutPosition.takeIf { it > 0 }?.also { currentPosition ->
                items.removeAt(currentPosition).also {
                    items.add(currentPosition - 1, it)
                }
                notifyItemMoved(currentPosition, currentPosition - 1)
            }
        }

        private fun moveDown(): (View) -> Unit = {
            layoutPosition.takeIf { it < items.size - 1 }?.also { currentPosition ->
                items.removeAt(currentPosition).also {
                    items.add(currentPosition + 1, it)
                }
                notifyItemMoved(currentPosition, currentPosition + 1)
            }
        }

        fun bind(text: String) {
            textView.text = text
        }
    }
}

The moveUp() and moveDown() functions are doing all of the work and, while there’s a little bit more going on here than with adding and deleting items, we still get some nice behaviour relatively cheaply. Most of the work is actually updating the list which backs the Adapter, which we’d need to do anyway. The first line of each function limits when we’ll attempt to make a change. In moveUp() we don’t bother to move anything if the item is already the first item in the list; and in moveDown() we don’t bother to move anything if the item is already the last item in the list. The next two lines swap the item with the one either above or below it. Finally we call notifyItemMoved() giving it the starting and ending positions of the item. The RecyclerView does the rest:

So far we’ve looked at how notifying the Adapter about changes the list itself can create some nice animations for us and in the concluding article in this series we’ll take this further and see how we can actually make changes to individual data items.

The source code for this article is available here.

© 2018, Mark Allison. All rights reserved.

Copyright © 2018 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

1 Comment

  1. Thank you for another great article. I was wondering if we could make the functions a bit more readable by moving parts into extension functions. I could not figure out yet a great solution but still want to leave two functions here to be discussed by you and the readers:

    fun RecyclerView.ViewHolder.isFirstItem() : Boolean = layoutPosition == 0
    fun RecyclerView.ViewHolder.isLastItem(size: Int) : Boolean = layoutPosition == size – 1

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.