Adapter / Material Design / RecyclerView

Material – Part 7

In the previous article we began looking at how RecyclerView makes life an awful lot easier when dragging list items to alter their position. We looked at how we can generate a bitmap of the view that we want to drag, and promote it to an overlay layer so that we can move it around easily. In this article we’ll look at dynamically moving the other items in the list automatically as we drag.

Any developers who have ever tried to drag and re-order items using ListView will be quaking at the very thought of this as it’s rather a tricky thing to do, However RecyclerView has been designed to overcome many of the pain points associated with ListView, and the task becomes a whole lot easier.

The first thing that we need to do is make a change to our Adapter to use stable IDs. Stable IDs mean that if we request an ID for a location, any specific item will always return the same ID regardless of its position within the list. This enables us to identify a specific item even if its position changes, which will be useful later on.

public class FeedAdapter extends RecyclerView.Adapter<FeedAdapter.ViewHolder> {
    .
    .
    .
    public FeedAdapter(List<Item> objects, @NonNull ItemClickListener itemClickListener) {
        this.items = objects;
        this.itemClickListener = itemClickListener;
        setHasStableIds(true);
    }
    .
    .
    .
    @Override
    public long getItemId(int position) {
        return items.get(position).getPubDate();
    }
    .
    .
    .
}

We do this by first calling setHasStableIds(true) in the constructor, and then overriding getItemId() to ensure that we provide a stable ID for each list item. In this case the publication date (which is a long representing the offset in milliseconds from the epoch will suffice).

Next we need to update our drag method – which gets called for each movement event once we’re in dragging state:

private void drag(int y) {
    overlay.setTranslationY(y - startY);
    if (!isInPreviousBounds()) {
        View view = recyclerView.findChildViewUnder(0, y);
        if (recyclerView.getChildPosition(view) != 0 && view != null) {
            swapViews(view);
        }
    }
}

public boolean isInPreviousBounds() {
    float overlayTop = overlay.getTop() + overlay.getTranslationY();
    float overlayBottom = overlay.getBottom() + overlay.getTranslationY();
    return overlayTop < startBounds.bottom && overlayBottom > startBounds.top;
}

This doing a check of whether the view being dragged has moved outside its previous bounds because, if it has, we need to swap it with the item view that it’s now over. There are some additional checks to ensure that we behave correctly for the first item view in the list.

The important thing here is that the basic check is pretty lightweight. This method is going to be called an lot when we’re dragging so we need to be really efficient otherwise we’ll make the dragging laggy.

Once we’ve detected that the view being dragged is outside needs to be swapped with another item, then the swapViews() method gets called passing the view that it needs to swap with as an argument.

private void swapViews(View current) {
    long replacementId = recyclerView.getChildItemId(current);
    FeedAdapter adapter = (FeedAdapter) recyclerView.getAdapter();
    int start = adapter.getPositionForId(replacementId);
    int end = adapter.getPositionForId(draggingId);
    adapter.moveItem(start, end);
    if (isFirst) {
        recyclerView.scrollToPosition(end);
        isFirst = false;
    }
    startBounds.top = current.getTop();
    startBounds.bottom = current.getBottom();
}

What we are doing here is determining the position in the list of the two items that we need to switch the positions of. We then call moveItem() on the Adapter to perform the switch. We have some handling specific to the first item because if we don’t scroll the list at this point the item that we’re switching the item being dragged with will actually fly off the screen because the RecyclerView thinks that the item being dragged is the first visible item in the list. Finally we change the currentBounds of the item being dragged to represent its new position so that our bounds checking code will now detect when it moves outside it’s new position.

What’s actually missing for this is two of the methods that we call on the Adapter (getPositionForId() and moveItem()) so we need to define them. Let’s start with getPositionForId():

public int getPositionForId(long id) {
    for (int i = 0; i < items.size(); i++) {
        if (items.get(i).getPubDate() == id) {
            return i;
        }
    }
    return -1;
}

This is actually pretty crude and you’d probably want to optimise it for larger lists. However, because my list is limited to 10 items because that’s all that the RSS feed we’re consuming supports, then it’s fine for our purposes.

The final method is moveItem():

public void moveItem(int start, int end) {
    int max = Math.max(start, end);
    int min = Math.min(start, end);
    if (min >= 0 && max < items.size()) {
        Item item = items.remove(min);
        items.add(max, item);
        notifyItemMoved(min, max);
    }
}

This is actually pretty straightforward. We perform some basic checks that the start and end positions are within bounds, and then remove it and replace it in the new position. But then the magic happens. I left this piece of code until last because it is the highlighted line in this method which does wonders. notifyItemMoved() is a method in the base class which we can use to trigger updates in the RecyclerView based upon the data itself changing. There are other methods which handle items being added, changed, removed, etc. But in our case we are informing the RecyclerView that the item has moved within the list.

Incredibly that’s it! RecyclerView takes care of the rest. It not only switches the to view for us, but it will actually animate them changing places as can be seen if this code is now run:

If you want to override the animations then that’s entirely possible by changing the ItemAnimator on RecyclerView, but the default one provides more than adequate behaviour.

Before finishing, it is worth pointing out that the above code is not perfect! As well as the inefficient getPositionForId() method, there is also a glitch if you rapidly drag up and down where items not being dragged swap positions. This is caused by the view locations being animated during the drag handling so incorrect view positions are being detected. While this is not difficult to fix, I elected to keep the code a bit simpler and easier to understand at the expense of leaving this glitch in there.

In the next article we’ll move away from RecyclerView, but keep with the animation theme.

The source code for this article is available here.

© 2014, Mark Allison. All rights reserved.

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

16 Comments

  1. Hi Mark,

    I noticed that you will need to use the X value as well in the drag method for cases where you have padding on the RecyclerView, in my case because of CardViews. Many thanks for the tutorial though, it helped me a bunch!

    1. I don’t think that’s necessary. For my example I’m only interested in when the position of the item in the list changes, so I only need to track it’s vertical position.

  2. Thank you for the interesting articles on Material.

    In part 7 you say:
    “there is also a glitch if you rapidly drag up and down where items not being dragged swap positions. This is caused by the view locations being animated during the drag handling so incorrect view positions are being detected. While this is not difficult to fix…”

    Could you, please, hint at how this can be fixed (or direct me to a fix)?

    Thank you in advance!
    /Bengt

    1. Basically it’s because of how the position detection is working – it uses the positions of items around the ones being dragged in real-time, so if they’re being animated, it can cause problems.

    2. I am also faced with this problem where the “order” gets confused. I end up with multiple
      ID’s having the same ordering.

      I have attempted fix this by disabling the RecyclerView as below, but in my case this did not fix the problem.
      I am using a Cursor to get the position of the id’s. Could this be my problem?
      my getPositionForID method:
      public int getPositionForId(long id) {

      mCursor.moveToFirst();

      if (mCursor.getInt(0) == id) {
      return mCursor.getInt(15);
      } else {
      while (mCursor.moveToNext()) {
      if (mCursor.getInt(0) == id) {
      return mCursor.getInt(15);
      }
      }
      }
      return -1;
      }

      Here is my Animation Fix:
      ObjectAnimator anim = ObjectAnimator.ofFloat(draggingView,”translationY”,0f,translationY – startBounds.top);
      anim.setDuration(ANIMATION_DURATION);
      anim.addListener(new AnimatorListenerAdapter() {
      @Override
      public void onAnimationStart(Animator animation) {
      super.onAnimationStart(animation);
      recyclerView.setEnabled(false);
      }

      @Override
      public void onAnimationEnd(Animator animation) {
      super.onAnimationEnd(animation);
      recyclerView.setEnabled(true);
      }
      });

      anim.start();

      1. I fixed my problem… it was due to the positions that were “unstable”

        so my solution was to use the two id’s and query the database for their positions and then to swap the orders. There was another snag, I had to wrap the two database updates in a transaction so
        that the swap is atomic and only become visible when both alterations are done.

  3. Hi, I’ve spotted that swapping isnt working on pre lollipop devices, in my case S3 LTE with 4.4.4 onboard. Thanks for this tutorial though!

    1. Erm, try changing the way that the movement detection works so that it doesn’t use the actual position of the surrounding items. That way the animation will not interfere with anything.

  4. Any ideas on how to handle scrolling pass visible items ? basically scrolling the recycler view ? By this way of implementation the move is not handled by the scroll .

      1. What he means by “scrolling past the screen” is when you drag an item towards the top edge and vice versa towards the bottom edge of the screen the recycler view starts auto scrolling with a certain velocity. For example if i have 4 items visible at the moment and there are 3 items at the top that are not visible when i drag the 3rd item from the visible items towards the top edge the recycler view starts scrolling with constant velocity so that the 3 not visible items reveal without dropping the dragged item and scrolling manually.

  5. Very nice tutorial. I am trying to use it for a horizontal grid view (with one row) but I don’t know how to track horizontal position changes. Can you help me, please

  6. Thank you for great guide! I have noticed one problem – when you have a long list, which goes beyond the screen and you try to drag some element to the position, which lies out of screen, your recycler won’t scroll down/up with the item your are currently dragging. Do you have any advice how to fix it?

  7. Thanks a lot for the crisp and clear tutorial. Helped a lot while implementing drag feature within recyclerview.

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.