Remember Drag and Drop Position with RecyclerView

In this post I will provide a tutorial on how to remember the drag and drop position of your RecyclerView list items in Android. There are few third party libraries that exist to help implement drag and drop in RecyclerView and Paul Burke provided an excellent three part tutorial on how to use native components to implement drag and drop. I will build upon this tutorial to show how your app can remember the new re-ordered position after the list has been re-ordered by drag and dropping. See this in action below.

https://gyazo.com/eb35ae18d5fbe0f4a3bd3d73a85a24eb

Whichever way you implement RecyclcerView drag and drop, the challenge is when you exit the app and come back, how does your app remember the last positions of the list items after they were re-organized. You may think that there is a new Intent(REMEMBER_LIST_POSITION) you can call but there is none, at-least I have not seen any so you will have to implement this manually if you desire such functionality. Here is how I have chosen to implement this functionality. I

  1. Create a standard RecyclerView
  2. Implement drag and drop on the RecyclerView
  3. Attach a listener to the RecyclcerView Adapter to listen for when the list item changes
  4. When the list item changes, grab all the Ids on the items in the collections
  5. Collapse this list into a JSON string and save to the SharedPreference
  6. When the RecyclerView is shown again, fetch the JSON string from SharedPreference and inflate back into a List<Long>
  7. Use this list to sort the items and display, reflecting their last positions the last time they were changed.

f you find the tutorial helpful at anytime please pause and use the social media buttons to share it incase someone else will benefit from it.Let us go ahead and see this in action. As with most things in programming your approach may be different from mine and I will be glad to hear how you solved this problem if it is something that you have had to deal with. The source code for this tutorial can be found here.

Create New Android Studio Project

In this section of the post we want to create a new Android Studio project for our drag and drop demo app. I am using Android Studio 1.4 and if you have created a new Android project lately you will realize that a few things have changed especially with the new project template. Follow the steps below to create new project for this demo app.

Step 1: Create New Project — follow the standard new project wizard to create a new Android Studio project. Give the project any name, select Phone and Tablet form factor. Select the default API level 16 and Blank Activity template.

Step 2: Cleanup — The new template comes with some boiler plate code that we do not need for this demo app. In the activity_main.xml remove the Floating Action Button component. In MainActivity.java file also remove the Floating Action Button code block. In content_main.xml remove the Hello World TextView.

Step 3: Add RecyclerView — in the place where you remove the Hello World TextView in content_main.xml add RecyclerView like this:

<android.support.v7.widget.RecyclerView
android:id="@+id/note_recycler_view"
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

Step 4: Add Model Class — at the root of your project add a model class Customer.java. This demo app is showing a list of fictitious customers. The names and images of the people used in this demo app are not real. The images are purchased from Dollar Photo Club. Here is the content of the Customer.java, use the generate functional to add the boiler plate getter and setter code.

public class Customer {
private Long id;
private String name;
private String emailAddress;
private int imageId;
private String imagePath;
}

Step 5: Add Drag Image — add this image to your res/drawable folder, we will use this as the handle for our drag and drop.

Step 6: Add Dependencies — Add the following dependencies for the external libraries we will use for this app; many thanks for the authors of those libraries.

compile 'de.hdodenhof:circleimageview:2.0.0'
compile 'com.squareup.picasso:picasso:2.5.2'
compile 'com.yqritc:recyclerview-flexibledivider:1.2.6'
compile 'com.google.code.gson:gson:2.3.1'

Also add this line to your Manifest <uses-permission android:name=”android.permission.INTERNET” />

Step 7: Add Sample Data — we are using sample in memory data to generate the demo data displayed in this app. Optionally create a package called utlities and in this package add a class called SampleData.java. In this class add a method called public static List<Customer> addSampleCustomers() and here is the content of that method.

Implement RecyclerView with Drag and Drop

In this section we will go ahead and implement the RecyclerView with drag and drop functionality. Follow the steps below to implement RecyclerView.

Step 1: Add Adapter — at the root of the project add a class file named CustomerListAdapter.java , leave the file alone and lets come back to it in a minute.

Step 2: Implement ItemTouchHelper — to implement ItemTouchHelper we actually need to add four files. To understand the purpose of each of these files. Read these post series. Add the following files to your project .

  1. In your utilities package add ItemTouchHelperAdapter.java and below is the content
  • public interface ItemTouchHelperAdapter { /** * Called when an item has been dragged far enough to trigger a move. This is called every time * an item is shifted, and not at the end of a “drop” event. * * @param fromPosition The start position of the moved item. * @param toPosition Then end position of the moved item. */ void onItemMove(int fromPosition, int toPosition); /** * Called when an item has been dismissed by a swipe. * * @param position The position of the item dismissed. */ void onItemDismiss(int position); }
  1. In your utilities package add ItemTouchHelperViewHolder.java and below is the content
  • public interface ItemTouchHelperViewHolder { /** * Implementations should update the item view to indicate it’s active state. */ void onItemSelected(); /** * state should be cleared. */ void onItemClear(); }
  1. In your utilities package add SimpleItemTouchHelperCallback.java and here is the content
  • public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback { private final ItemTouchHelperAdapter mAdapter; public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) { mAdapter = adapter; } @Override public boolean isLongPressDragEnabled() { return true; } @Override public boolean isItemViewSwipeEnabled() { return false; } @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); } @Override public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) { mAdapter.onItemMove(source.getAdapterPosition(), target.getAdapterPosition()); return true; } @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) { mAdapter.onItemDismiss(viewHolder.getAdapterPosition()); } @Override public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) { if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) { ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder; itemViewHolder.onItemSelected(); } super.onSelectedChanged(viewHolder, actionState); } @Override public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { super.clearView(recyclerView, viewHolder); ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder; itemViewHolder.onItemClear(); } }
  1. Add a package called listener and add an interface called OnStartDragListener.java and here is the content
  • public interface OnStartDragListener { /** * Called when a view is requesting a start of a drag. * * @param viewHolder The holder of the view to drag. */ void onStartDrag(RecyclerView.ViewHolder viewHolder); }
  1. While you are at the listener package add another interface called OnCustomerListChangedListener.java and here is the content
  • public interface OnCustomerListChangedListener { void onNoteListChanged(List<Customer> customers); }

Step 3: Implement Custom Row — now we need to add the custom row that lays out the views item in each row of our list. In your res/layout folder add the layout file row_customer_list.xml and here is the content of that file.

<?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="wrap_content">

<de.hdodenhof.circleimageview.CircleImageView
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_margin="10dp"
android:layout_centerVertical="true"
android:id="@+id/image_view_customer_head_shot" />

<LinearLayout
android:id="@+id/linear_layout_customer_name"
android:layout_width="0dp"
android:layout_weight="3"
android:layout_margin="10dp"
android:paddingTop="@dimen/margin_padding_normal"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:orientation="vertical">

<TextView
android:id="@+id/text_view_customer_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<TextView
android:id="@+id/text_view_customer_email"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>


<ImageView
android:id="@+id/handle"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|right"
android:scaleType="center"
android:src="@drawable/ic_reorder_grey" />

</LinearLayout>

Step 4: Implement the Adapter — we now have all the components needed to implement the adapter. Now update the adapter with the following code. I am including the import statements so you can see which library is being referenced from this file. A few things is happening in the code below, for the most part it is a standard implementation of RecyclerView Adapter.

The drag and drop is simply implemented with the Java Collections framework. And after the swap, we are attaching our own home made listener which lets us know that a swap has been made and returns the new collections of items back to us. This is the list we will now work on.

package com.okason.draganddrop;

import android.content.Context;
import android.graphics.Color;
import android.support.v4.view.MotionEventCompat;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import com.okason.draganddrop.listeners.OnCustomerListChangedListener;
import com.okason.draganddrop.listeners.OnStartDragListener;
import com.okason.draganddrop.utilities.ItemTouchHelperAdapter;
import com.okason.draganddrop.utilities.ItemTouchHelperViewHolder;
import com.squareup.picasso.Picasso;

import java.util.Collections;
import java.util.List;

/**
* Created by Valentine on 10/18/2015.
*/
public class CustomerListAdapter extends
RecyclerView.Adapter<CustomerListAdapter.ItemViewHolder>
implements ItemTouchHelperAdapter {

private List<Customer> mCustomers;
private Context mContext;
private OnStartDragListener mDragStartListener;
private OnCustomerListChangedListener mListChangedListener;

public CustomerListAdapter(List<Customer> customers, Context context,
OnStartDragListener dragLlistener,
OnCustomerListChangedListener listChangedListener){
mCustomers = customers;
mContext = context;
mDragStartListener = dragLlistener;
mListChangedListener = listChangedListener;
}


@Override
public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View rowView = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_customer_list, parent, false);
ItemViewHolder viewHolder = new ItemViewHolder(rowView);
return viewHolder;
}

@Override
public void onBindViewHolder(final ItemViewHolder holder, int position) {

final Customer selectedCustomer = mCustomers.get(position);

holder.customerName.setText(selectedCustomer.getName());
holder.customerEmail.setText(selectedCustomer.getEmailAddress());
Picasso.with(mContext)
.load(selectedCustomer.getImagePath())
.placeholder(R.drawable.profile_icon)
.into(holder.profileImage);



holder.handleView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN) {
mDragStartListener.onStartDrag(holder);
}
return false;
}
});
}

@Override
public int getItemCount() {
return mCustomers.size();
}

@Override
public void onItemMove(int fromPosition, int toPosition) {
Collections.swap(mCustomers, fromPosition, toPosition);
mListChangedListener.onNoteListChanged(mCustomers);
notifyItemMoved(fromPosition, toPosition);
}

@Override
public void onItemDismiss(int position) {

}

public static class ItemViewHolder extends RecyclerView.ViewHolder implements
ItemTouchHelperViewHolder {
public final TextView customerName, customerEmail;
public final ImageView handleView, profileImage;


public ItemViewHolder(View itemView) {
super(itemView);
customerName = (TextView)itemView.findViewById(R.id.text_view_customer_name);
customerEmail = (TextView)itemView.findViewById(R.id.text_view_customer_email);
handleView = (ImageView)itemView.findViewById(R.id.handle);
profileImage = (ImageView)itemView.findViewById(R.id.image_view_customer_head_shot);
}

@Override
public void onItemSelected() {
itemView.setBahttp://valokafor.com/wp-admin/post.php?post=1804&action=edit#ckgroundColor(Color.LTGRAY);
}

@Override
public void onItemClear() {
itemView.setBackgroundColor(0);
}
}
}

Step 5: Implement RecyclerView Java Code — open your MainActivity.java and add the following instance variables near the top of the file.

private RecyclerView mRecyclerView;
private CustomerListAdapter mAdapter;
private RecyclerView.LayoutManager mLayoutManager;
private ItemTouchHelper mItemTouchHelper;
private List<Customer> mCustomers;

After the onCreate method add this method. And then call this method from the onCreate() method probably after the call to set Toolbar. Ignore the error warning for a minute.

private void setupRecyclerView(){
mRecyclerView = (RecyclerView) findViewById(R.id.note_recycler_view);
mRecyclerView.setHasFixedSize(true);
mLayoutManager = new LinearLayoutManager(this);
mRecyclerView.setLayoutManager(mLayoutManager);
mCustomers = SampleData.addSampleCustomers();

//setup the adapter with empty list
mAdapter = new CustomerListAdapter(mCustomers, this, this, this);
ItemTouchHelper.Callback callback = new SimpleItemTouchHelperCallback(mAdapter);
mItemTouchHelper = new ItemTouchHelper(callback);
mItemTouchHelper.attachToRecyclerView(mRecyclerView);
mRecyclerView.addItemDecoration(new HorizontalDividerItemDecoration.Builder(this)
.colorResId(R.color.colorPrimaryDark)
.size(2)
.build());
mRecyclerView.setAdapter(mAdapter);
}

Update the signature of your MainActivity to implement the two listeners that we added like below and use Android Studio quick fix to implement the methods.

public class MainActivity extends AppCompatActivity
implements OnCustomerListChangedListener,
OnStartDragListener{

Here is the implementation of the one of the methods, and we will implement the other one in the next section.

@Override
public void onStartDrag(RecyclerView.ViewHolder viewHolder) {
mItemTouchHelper.startDrag(viewHolder);

}

At this point, you should be able to run your app. Everything should be working except that when you close the app and open the position of the drag and drop will not be saved. We will implement that next. But make sure that you get the app working before you proceed.

Implement Save Drag and Drop Position

At this point, your drag and drop list should be working and we now want to remember the position of the list items after they have been re-organized. Like I mentioned at the beginning of the post, this is accomplished by saving the ids of the list items to SharedPreference so go ahead and add the following class members to the top of the file.

private SharedPreferences mSharedPreferences;
private SharedPreferences.Editor mEditor;
public static final String LIST_OF_SORTED_DATA_ID = "json_list_sorted_data_id";
public final static String PREFERENCE_FILE = "preference_file";

And in the onCreate() instantiate the SharedPreference like so

mSharedPreferences = this.getApplicationContext()
.getSharedPreferences(PREFERENCE_FILE, Context.MODE_PRIVATE);
mEditor = mSharedPreferences.edit();

Then go ahead and implement the other method that listens for when the list changes and here is the implementation of that method

@Override
public void onNoteListChanged(List<Customer> customers) {
//after drag and drop operation, the new list of Customers is passed in here

//create a List of Long to hold the Ids of the
//Customers in the List
List<Long> listOfSortedCustomerId = new ArrayList<Long>();

for (Customer customer: customers){
listOfSortedCustomerId.add(customer.getId());
}

//convert the List of Longs to a JSON string
Gson gson = new Gson();
String jsonListOfSortedCustomerIds = gson.toJson(listOfSortedCustomerId);


//save to SharedPreference
mEditor.putString(LIST_OF_SORTED_DATA_ID, jsonListOfSortedCustomerIds).commit();
mEditor.commit();


}

Then add this method to your MainActivity.java

private List<Customer> getSampleData(){

//Get the sample data
List<Customer> customerList = SampleData.addSampleCustomers();

//create an empty array to hold the list of sorted Customers
List<Customer> sortedCustomers = new ArrayList<Customer>();

//get the JSON array of the ordered of sorted customers
String jsonListOfSortedCustomerId = mSharedPreferences.getString(LIST_OF_SORTED_DATA_ID, "");


//check for null
if (!jsonListOfSortedCustomerId.isEmpty()){

//convert JSON array into a List<Long>
Gson gson = new Gson();
List<Long> listOfSortedCustomersId = gson.fromJson(jsonListOfSortedCustomerId, new TypeToken<List<Long>>(){}.getType());

//build sorted list
if (listOfSortedCustomersId != null && listOfSortedCustomersId.size() > 0){
for (Long id: listOfSortedCustomersId){
for (Customer customer: customerList){
if (customer.getId().equals(id)){
sortedCustomers.add(customer);
customerList.remove(customer);
break;
}
}
}
}

//if there are still customers that were not in the sorted list
//maybe they were added after the last drag and drop
//add them to the sorted list
if (customerList.size() > 0){
sortedCustomers.addAll(customerList);
}

return sortedCustomers;
}else {
return customerList;
}
}

Now update the line in setupRecyclerView() that get the data from mCustomers = SampleData.addSampleCustomers(); to mCustomers = getSampleData();

This is the method that does the little magic of re-displaying items using their last sorted position. All on the fly without re-saving the items back to the database. Obviously, if the data is coming from a database, the above method must run in a background thread to avoid blocking the main thread. I have used variations of this approach for a few recent projects. I will like to know if you have had to solve a similar problem and what approach you ended up using.

If you think anyone in your social media list can benefit from the list, please feel free to share the post.

Keep coding!


Originally published at Val Okafor.