33

I am building a component based on RecyclerView, allowing user to reorder items by drag and drop. Once I am on the DragListener side, I need the position it has in the adapter in order to perform correct move, but I only have access to the view. So here is what I am doing in the adapter view binding :

@Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {
    Track track = mArray.get(position);
    viewHolder.itemView.setTag(R.string.TAG_ITEM_POSITION, position);
}

Does it seem correct to you ? Because if I move an item like this :

public void move(int from, int to){
    Track track = mArray.remove(from);
    mArray.add(to, track);
    notifyItemMoved(from, to);
}

then position tag is not correct anymore, and if I notifyDataSetChanged(), I lose the fancy animation. Any suggestion ?

elgui
  • 3,303
  • 4
  • 28
  • 37

7 Answers7

129

There is a way to preserve fancy animations with just notifyDataSetChanged()

  1. You need to make your own GridLayoutManager with overriden supportsPredictiveItemAnimations() method returning true;

  2. You need to mAdapter.setHasStableIds(true)

  3. The part I find tricky is you need to override you adapter's getItemId() method. It should return value that is truly unique and not a direct function of position. Something like mItems.get(position).hashCode()

Worked perfectly fine in my case - beautiful animations for adding, removing and moving items only using notifyDataSetChanged()

tochkov
  • 2,917
  • 2
  • 15
  • 21
  • 20
    You saved me a lot of pain man, tyvm. BTW this should be accepted answer not the one above. – M. Erfan Mowlaei Apr 28 '15 at 23:25
  • and I forgot to say, if you are using DB to feed the adapter something like primary key for part 3 will solve your problem. – M. Erfan Mowlaei Apr 28 '15 at 23:38
  • Hi, I don't understand what you need use "mItems.get(position).hashCode()" returns long I need return a object .., can you tell me ? and how return object ? thx –  Nov 27 '15 at 13:47
  • 1
    It is for your [getItemId (int position)](http://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html#getItemId(int)) method which returns long. – tochkov Nov 30 '15 at 09:26
  • That no 3 was something that turned my app Happy !! :) It can not control its emotions.. – kirtan403 Apr 30 '16 at 07:19
  • When removing, the view which was offscreen and should now slide in-screen just fades in, any way to make it slide instead? – urSus Nov 08 '16 at 23:26
  • Not sure this is such a great idea because unequal objects can have the same hash codes http://eclipsesource.com/blogs/2012/09/04/the-3-things-you-should-know-about-hashcode/ – alexbirkett Apr 27 '17 at 13:07
  • Thank you @tochkov for the answer. It saved my life. Can you please give me some details about how this make it work? Or just refer me to a link or a video. Thx again – HiddenDroid Jul 17 '17 at 11:24
7

No, it is wrong. First of all, you cannot reference to the position passed to the onBindViewHolder after that method returns. RecyclerView will not rebind a view when its position changes (due to items moving etc).

Instead, you can use ViewHolder#getPosition() which will return you the updated position.

If you fix that, your move code should work & provide nice animations.

Calling notifyDataSetChanged will prevent predictive animations so avoid it as long as you can. See documentation for details.

Edit (from comment): to get position from the outside, get child view holder from recyclerview and then get position from the vh. See RecyclerView api for details

elgui
  • 3,303
  • 4
  • 28
  • 37
yigit
  • 37,683
  • 13
  • 72
  • 58
  • ok, thank you for this answer =) but now what is the best way to retrieve the position from outside the adpater (my dragListener for example) ? – elgui Dec 05 '14 at 08:38
  • Get child view holder from recyclerview and then get position from the vh. See RecyclerView api for details – yigit Dec 05 '14 at 09:30
  • getPosition() was deprecated because it's too ambiguous. In this case you'll want to use getLayoutPosition() – jj. Apr 26 '16 at 18:07
  • but `mAdapter.notifyItemMoved(viewHolder.getAdapterPosition(), target.getAdapterPosition());` sucks. I see that items was moved `Collections.swap(....,...)` but when I click on them they are old ones (so only titles of items were changed) – user25 Mar 19 '17 at 18:28
3

1) You'll use notifyItemInserted(position); or notifyItemRemoved(position); instead of notifyDataSetChanged() for animation. 2) You can just manually fix your problem - using

public void move(int from, int to){
    Track track = mArray.remove(from);
    mArray.add(to, track);
    notifyItemMoved(from, to);
    ViewHolder fromHolder = (ViewHolder) mRecyclerView.findViewHolderForPosition(from);
    ViewHolder toHolder = (ViewHolder) mRecyclerView.findViewHolderForPosition(to);
    Tag fromTag = fromHolder.itemView.getTag();
    fromHolder.itemView.setTag(toHolder.itemView.getTag()); 
    toHolder.itemView.setTag(fromTag);

}
Aleksandr M
  • 24,264
  • 12
  • 69
  • 143
Rahim Rahimov
  • 1,347
  • 15
  • 24
0

You should move your method to OnCreateViewHolder, then notifyItemRemoved(index) works properly.

Dominik
  • 49
  • 1
  • 3
0

I'm able to maintain the touch animations by adding this to my list item's outer element

<View
    android:foreground="?android:attr/selectableItemBackground"
...>
fix
  • 1,425
  • 16
  • 27
0

I fixed it with using 'notifyItemChanged(int position);' instead of 'notifyDataSetChanged();'

My adapter shows fancy animations perfectly and without any lags

Edit: I got position from onBindViewHolder's position.

E. Kolver
  • 103
  • 1
  • 7
0

as stated by others above, you can have animation while using notifyDataSetChanged on your adapter, although you need to specifically use stable ids. if your items IDs are strings, you can generate a long id for each string id you have and keep them in a map. for example:

class StringToLongIdMap {
    private var stringToLongMap = HashMap<String, Long>()
    private var longId: Long = 0

    fun getLongId(stringId: String): Long {
        if (!stringToLongMap.containsKey(stringId)) {
            stringToLongMap[stringId] = longId++
        }
        return stringToLongMap[stringId] ?: -1
    }
}

and then in your adapter:

private var stringToLongIdMap = StringToLongIdMap()

override fun getItemId(position: Int): Long {
    val item = items[position]
    return stringToLongIdMap.getLongId(item.id)
}

another useful thing to consider, if you are using kotlin data class as items in your adapter, and you don't have an id, you can use the hashCode of the data class itself as stable id (if you are sure that the item properties combination are unique in your data set):

override fun getItemId(position: Int): Long = items[position].hashCode().toLong()
Raphael C
  • 2,296
  • 1
  • 22
  • 22