Lesson Weekend

Android apps aren't just regular programs, applications, or websites designed to work especially well with Android systems; their touchscreens also offer a whole realm of user interaction other devices simply can't. We can swipe to reveal a delete button, long-press to open an edit functionality, or drag-and-drop to re-organize a folder. Let's explore the mechanics of some of these custom, touchscreen-specific interactions (also known as gestures), and begin integrating them into our projects.

We will add features that allow users to drag and drop restaurants to re-order their "Saved Restaurants" list, and swipe to delete an individual restaurant from their list. To do this, we will use a tool called ItemTouchHelper. ItemTouchHelper is an Android utility class that adds support for touchscreen gestures to the RecyclerView.

Required Reading

Before we begin, check out the tutorial Drag and Swipe with RecyclerView. The included animation is a great visual example of the drag-and-drop and swipe-to-delete functionality we will implement.

Setup

In the next few lessons we'll add the functionality described in the article above by adding the necessary interfaces, and creating an ItemTouchHelper.Callback to listen for gestures. Next, we will construct a new list item layout to indicate to users that list items are drag-enabled. Finally, we will create a custom adapter that both inherits functionality from the FirebaseRecyclerAdapter and acts as the adapter for our new ItemTouchHelper.Callback.

ItemTouchHelper Interfaces

We will need to create 2 interfaces to implement the ItemTouchHelper. These interfaces will provide a list of methods that will eventually tie into callback methods in the ItemTouchHelper class we will create next.

Create a new sub-package named util. In the util sub-package, create new Java class files with the following interface:

ItemTouchHelperAdapter

ItemTouchHelperAdapter.java
public interface ItemTouchHelperAdapter {
    boolean onItemMove(int fromPosition, int toPosition);
    void onItemDismiss(int position);
}

As addressed in previous lessons, interfaces are groups of methods other classes may implement. When we define an interface, we're listing out the methods. Each class that implements the interface will be required to override each of these methods.

In the code above, we've defined an interface called ItemTouchHelperAdapter with the following two methods to override:

  • onItemMove() will be called each time the user moves an item by dragging it across the touch screen. The argument fromPosition represents the location the item originally resided at. toPosition represents the location the user moved the item to.

  • onItemDismiss() is called when an item has been dismissed with a swipe motion. The parameter position represents the location of the dismissed item.

We will eventually implement this interface and override its methods in our custom FirebaseRecyclerAdapter to tell our adapter what to do when an item is moved or dismissed via the touchscreen. It will eventually pass event callbacks from our custom ItemTouchHelper class back up the chain.

OnStartDragListener

Next, we'll create another interface in the util subpackage, and populate it with the following code:

OnStartDragListener.java
public interface OnStartDragListener {
    void onStartDrag(RecyclerView.ViewHolder viewHolder);
}
  • onStartDrag() will be called when the user begins a 'drag-and-drop' interaction with the touchscreen. viewHolder represents the RecyclerView holder corresponding to the object being dragged.

We will implement this interface in our SavedRestaurantsListActivity to attach the event listener to our custom ItemTouchHelper which will eventually be attached to our RecyclerView. This interface will pass events back to our adapter allowing us to attach the touch listener to an item in our ViewHolder.

Enabling Gestures in the RecyclerView

Now, we need to ensure our RecyclerView has the capability to receive and process gestures from the touchscreen. To listen for move and swipe events we need to create an ItemTouchHelper.Callback. This class will define which gestures to enable or disable. It will also receive callbacks when the user performs enabled touchscreen actions.

Create another new class called SimpleItemTouchHelperCallback in the util sub-package. The nested comments in the code below describe what each method does:

SimpleItemTouchHelperCallback.java
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
    private final ItemTouchHelperAdapter mAdapter;

    //  This constructor takes an ItemTouchHelperAdapter parameter. When implemented in 
    //  FirebaseRestaurantListAdapter, the ItemTouchHelperAdapter instance will pass the gesture event back to the
    //  Firebase adapter where we will define what occurs when an item is moved or dismissed.

    public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
        mAdapter = adapter;
    }

    //  The method below informs the ItemTouchHelperAdapter that drag gestures are enabled. 
    //  We could also disable drag gestures by returning 'false'. 

    @Override
    public boolean isLongPressDragEnabled() {
        return true;
    }

     //  The method below informs the ItemTouchHelperAdapter that swipe gestures are enabled. 
     //  We could also disable them by returning 'false'. 

    @Override
    public boolean isItemViewSwipeEnabled() {
        return true;
    }

    //  getMovementFlags informs the ItemTouchHelper which movement directions are supported. 
    // For example, when a user drags a list item, they press 'Down' to begin the drag and lift their finger, 'Up',  to end the drag. 

    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
            final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
            final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
            return makeMovementFlags(dragFlags, swipeFlags);
    }

    //  The method below notifies the adapter that an item has moved. 
    //  This triggers the onItemMove override in our Firebase adapter, 
    //  which will eventually handle updating the restaurants ArrayList to reflect the item's new position. 

    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, 
            RecyclerView.ViewHolder target) {
        if (source.getItemViewType() != target.getItemViewType()) {
            return false;
        }
        mAdapter.onItemMove(source.getAdapterPosition(), target.getAdapterPosition());
        return true;
    }

   //  The method below notifies the adapter that an item was dismissed. 
   //  This triggers the onItemDismiss override in our Firebase adapter 
   //  which will eventually handle deleting this item from the user's "Saved Restaurants" in Firebase.

    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) {
        mAdapter.onItemDismiss(viewHolder.getAdapterPosition());
    }
}

User Interface

Downloading Icons

As stated in the tutorial at the beginning of this lesson, Google’s Material Design Guidelines recommend using the "Reorder" icon to indicate that list items have drag-and-drop capabilities.

Let’s download the Material Icons | Reorder PNG icon and add it to our drawable folder. As we've done previously, place each varying size PNG file in the corresponding sub-directory within drawable . Once complete, you should have a file structure of icons that looks something like this:

icon-file-structure-in-finder

Creating Layouts

We're currently using the same layout for individual restaurant items in both the "Saved Restaurants" and "Find Restaurants" areas of our application. Because "Saved Restaurants" will now allow users to reorder their list, let's create a dedicated layout for list items in this area. This layout will look the same as restaurant_list_item, except it will include the 'reorder' icon in the bottom-left corner of the restaurant ImageView.

Create a new list item layout file called restaurant_list_item_drag . We will use a RelativeLayout and align the icon ImageView to the bottom left of the restaurant ImageView:

restaurant_list_item_drag.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <RelativeLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

    <ImageView
        android:layout_width="130dp"
        android:layout_height="100dp"
        android:id="@+id/restaurantImageView"
        android:src="@drawable/waffles"
        android:scaleType="centerCrop"/>

        <ImageView
            android:id="@+id/dragIcon"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:layout_alignLeft="@id/restaurantImageView"
            android:layout_alignBottom="@id/restaurantImageView"
            android:gravity="bottom|left"
            android:src="@drawable/ic_reorder_white_24dp"/>

    </RelativeLayout>

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:background="#ffffff"
        android:layout_height="match_parent"
        android:padding="10dp">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/restaurantNameTextView"
            android:textSize="20dp"
            android:textStyle="bold"
            android:text="Restaurant Name"
            android:textColor="@color/colorPrimary"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="One Cuisine Type"
            android:id="@+id/categoryTextView"
            android:layout_below="@+id/restaurantNameTextView"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:textStyle="italic"/>

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Rating"
                android:id="@+id/ratingTextView"
                android:layout_alignParentBottom="true"
                android:layout_alignParentRight="true"
                android:textColor="@color/colorAccent"/>
        </RelativeLayout>
    </LinearLayout>
</LinearLayout>

We'll inflate this layout in the FirebaseRecyclerAdapter constructor within SavedRestaurantListActivity. We'll simply replace the R.layout.restaurant_list_item we're currently inflating with R.layout.restaurant_list_item_drag:

SavedRestaurantListActivity.java
public class SavedRestaurantListActivity extends AppCompatActivity {
    ...

    private void setUpFirebaseAdapter() {
        mFirebaseAdapter = new FirebaseRecyclerAdapter<Restaurant, FirebaseRestaurantViewHolder>
                (Restaurant.class, R.layout.restaurant_list_item_drag, FirebaseRestaurantViewHolder.class, mRestaurantReference) {
            ...
    }
    ...
}

If we run our app and navigate to SavedRestaurantsListActivity, we should see that our icons appear over the ImageViews, but we aren't yet able to drag and drop the list items:

reorder-icon-present-in-saved-restaurants-list

Now that we have all necessary components in place, we will enable this functionality in the next lesson.


Example GitHub Repo for MyRestaurants

Terminology


  • Interfaces: Groups of methods other classes may implement, as described in previous lessons.

  • Gestures: Touchscreen-specific interactions, such as drag-and-drop, long-press or swipe.

  • ItemTouchHelper: An Android utility class that adds support for touchscreen gestures to the RecyclerView.

Examples


Additional Resources