Adding Swipe Gestures to RecyclerViews
A big part of Material Design is the way users get to interact with the visual elements of an app. Therefore, in addition to taps and long presses, a well-made Android app today is expected to handle more complex touch gestures such as swipes and drags. This is especially important if the app uses lists to display its data.
By using the RecyclerView
widget, and a few other Android Jetpack components, you can handle a wide variety of list-related swipe gestures in your apps. Furthermore, in just a few lines of code, you can associate Material Motion animations with those gestures.
In this tutorial, I’ll show you how to add a few common swipe gestures, complete with intuitive animations, to your lists.
Prerequisites
To be able to make the most of this tutorial, you’ll need:
- Android Studio 3.2.1 or higher
- a phone or tablet running Android API level 23 or higher
1. Creating a List
To keep this tutorial short, let’s use one of the templates available in Android Studio to generate our list.
Start by launching Android Studio and creating a new project. In the project creation wizard, make sure you choose the Empty Activity option.
Instead of the Support library, we’ll be using Android Jetpack in this project. So, once the project has been generated, go to Refactor > Migrate to AndroidX. When prompted, press the Migrate button.
Next, to add a list to the project, go to File > New > Fragment > Fragment (List). In the dialog that pops up, go ahead and press the Finish button without making any changes to the default values.
At this point, Android Studio will create a new fragment containing a fully configured RecyclerView
widget. It will also generate dummy data to display inside the widget. However, you’ll still have to add the fragment to your main activity manually.
To do so, first add the OnListFragmentInteractionListener
interface to your main activity and implement the only method it contains.
override fun onListFragmentInteraction( item: DummyContent.DummyItem?) { // leave empty }
Next, embed the fragment inside the activity by adding the following tag to the activity_main.xml file:
At this point, if you run your app, you should be able to see a list that looks like this:
2. Adding the Swipe-to-Remove Gesture
Using the ItemTouchHelper
class, you can quickly add swipe and drag gestures to any RecyclerView
widget. The class also provides default animations that run automatically whenever a valid gesture is detected.
The ItemTouchHelper
class needs an instance of the abstract ItemTouchHelper.Callback
class to be able to detect and handle gestures. Although you can use it directly, it’s much easier to use a wrapper class called SimpleCallback
instead. It’s abstract too, but you’ll have fewer methods to override.
Create a new instance of the SimpleCallback
class inside the onCreateView()
method of the ItemFragment
class. As an argument to its constructor, you must pass the direction of the swipe you want it to handle. For now, pass RIGHT
to it so that it handles the swipe-right gesture.
val myCallback = object: ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT) { // More code here }
The class has two abstract methods, which you must override: the onMove()
method, which detects drags, and the onSwiped()
method, which detects swipes. Because we won’t be handling any drag gestures today, make sure you return false
inside the onMove()
method.
override fun onMove( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder ): Boolean = false override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { // More code here }
Inside the onSwiped()
method, you can use the adapterPosition
property to determine the index of the list item that was swiped. Because we are implementing the swipe-to-remove gesture now, pass the index to the removeAt()
method of the dummy list to remove the item.
DummyContent.ITEMS.removeAt(viewHolder.adapterPosition)
Additionally, you must pass the same index to the notifyItemRemoved()
method of the RecyclerView
widget’s adapter to make sure that the item is not rendered anymore. Doing so also runs the default item removal animation.
adapter?.notifyItemRemoved(viewHolder.adapterPosition)
At this point, the SimpleCallback
object is ready. All you need to do now is create an ItemTouchHelper
object with it and attach the RecyclerView
widget to it.
val myHelper = ItemTouchHelper(myCallback) myHelper.attachToRecyclerView(this)
If you run the app now, you’ll be able to swipe items out of the list.
3. Revealing a Background View
Although the swipe-to-remove gesture is very intuitive, some users may not be sure what happens when they perform the gesture. Therefore, Material Design guidelines say that the gesture must also progressively reveal a view hidden behind the item, which clearly indicates what’s going to happen next. Usually, the background view is simply an icon displaying a trash bin.
To add the trash bin icon to your project, go to File > New > Vector Asset and select the icon named delete.
You can now get a reference to the icon in your Kotlin code by calling the getDrawable()
method. So add the following line to the onCreateView()
method of the ItemFragment
class:
val trashBinIcon = resources.getDrawable( R.drawable.ic_delete_black_24dp, null )
Displaying a view behind a list item is slightly complicated because you need to draw it manually while also making sure that its bounds match the bounds of the region that’s progressively revealed.
Override the onChildDraw()
method of your SimpleCallback
implementation to start drawing.
override fun onChildDraw( c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean ) { // More code here super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) }
In the above code, the call to the onChildDraw()
method of the superclass is important. Without it, your list items will not move when they are swiped.
Because we are handling only the swipe-right gesture, the X coordinates of the upper left and bottom left corners of the background view will always be zero. The X coordinates of the upper right and bottom right corners, on the other hand, should be equal to the dX
parameter, which indicates how much the list item has been moved by the user.
To determine the Y coordinates of all the corners, you’ll have to use the top
and bottom
properties of one of the views present inside the viewHolder
object.
Using all these coordinates, you can now define a rectangular clip region. The following code shows you how to use the clipRect()
method of the Canvas
object to do so:
c.clipRect(0f, viewHolder.itemView.top.toFloat(), dX, viewHolder.itemView.bottom.toFloat())
Although you don’t have to, it’s a good idea to make the clip region visible by giving it a background color. Here’s how you can use the drawColor()
method to make the clip region gray when the swipe distance is small and red when it’s larger.
if(dX < width / 3) c.drawColor(Color.GRAY) else c.drawColor(Color.RED)
You’ll now have to specify the bounds of the trash bin icon. These bounds must include a margin that matches that of the text shown in the list items. To determine the value of the margin in pixels, use the getDimension()
method and pass text_margin
to it.
val textMargin = resources.getDimension(R.dimen.text_margin) .roundToInt()
You can reuse the coordinates of the clip region’s upper left corner as the coordinates of the icon’s upper left corner. They must, however, be offset by the textMargin
. To determine the coordinates of its bottom right corner, use its intrinsic width and height. Here’s how:
trashBinIcon.bounds = Rect( textMargin, viewHolder.itemView.top + textMargin, textMargin + trashBinIcon.intrinsicWidth, viewHolder.itemView.top + trashBinIcon.intrinsicHeight + textMargin )
Finally, draw the icon by calling its draw()
method.
trashBinIcon.draw(c)
If you run the app again, you should be able to see the icon when you swipe to remove a list item.
4. Adding the Swipe-to-Refresh Gesture
The swipe-to-refresh gesture, also known as the pull-to-refresh gesture, has become so popular these days that Android Jetpack has a dedicated component for it. It’s called SwipeRefreshLayout
, and it allows you to quickly associate the gesture with any RecyclerView
, ListView
, or GridView
widget.
To support the swipe-to-refresh gesture in your RecyclerView
widget, you must make it a child of a SwipeRefreshLayout
widget. So open the fragment_item_list.xml file, add a tag to it, and move the
tag inside it. After you do so, the file’s contents should like this:
The list fragment assumes that the RecyclerView
widget is the root element of its layout. Because this is not true anymore, you need to make a few changes in the onCreateView()
method of the ItemFragment
class. First, replace the first line of the method, which inflates the layout, with the following code:
val srLayout: SwipeRefreshLayout = inflater.inflate( R.layout.fragment_item_list, container, false ) as SwipeRefreshLayout val view = srLayout.findViewById(R.id.list)
Next, change the last line of the method to return the SwipeRefreshLayout
widget instead of the RecyclerView
widget.
return srLayout
If you try running the app now, you’ll be able to perform a vertical swipe gesture and get visual feedback. The contents of the list won’t change, though. To actually refresh the list, you must associate an OnRefreshListener
object with the SwipeRefreshLayout
widget.
srLayout.setOnRefreshListener { // More code here }
Inside the listener, you are free to modify the data the list displays based on your requirements. For now, because we’re working with dummy data, let’s just empty the list of dummy items and reload it with 25 new dummy items. The following code shows you how to do so:
DummyContent.ITEMS.clear() for(i in 1..25) { DummyContent.ITEMS.add( DummyContent.DummyItem("$i", "Item $i", "") ) }
After updating the data, you must remember to call the notifyDataSetChanged()
method to let the adapter
of the RecyclerView
widget know that it must redraw the list.
view.adapter?.notifyDataSetChanged()
By default, as soon as the user performs the swipe-to-refresh gesture, the SwipeRefreshLayout
widget displays an animated progress indicator. Therefore, after you have updated the list, you must remember to remove the indicator by setting the isRefreshing
property of the widget to false.
srLayout.isRefreshing = false
If you run the app now, remove a few list items, and perform the swipe-to-refresh gesture, you’ll see the list reset itself.
Conclusion
Material Design has been around for a few years now, and most users these days expect you to handle many of the gestures it mentions. Fortunately, doing so doesn’t take too much effort. In this tutorial, you learned how to implement two very common swipe gestures. You also learned how to progressively reveal a view hidden behind a list item being swiped.