Reputation: 3175
Currently working on implementing an "encyclopedia" of sorts via a RecyclerView that I can sort, filter, search, etc. Functionally, I have it working fine, so I was getting started on animations. I only want animations when the data set is changed, not on scrolling or touch events or etc., so I'm just using an ItemAnimator.
Well, many of the ways my RecyclerView can be sorted, filtered, or searched result in virtually or literally the entire data set being replaced. I figured these would be the easiest cases to animate, since in this case I can just call notifyDataSetRemoved(0, oldItemCount)
followed by notifyDataSetInserted(0, newItemCount)
, which would call animateRemove()
and animateAdd()
respectively on each item modified. And that's exactly what happens! In a very simple sense, it works, I guess. But it just so happens that it only works properly once.
Here's the remove animation (currently):
public boolean animateRemove(RecyclerView.ViewHolder holder) {
holder.itemView.clearAnimation();
holder.itemView.animate()
.alpha(0)
.setInterpolator(new AccelerateInterpolator(2.f))
.setDuration(350)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
dispatchRemoveFinished(holder);
}
})
.start();
return false;
}
and the add animation (currently):
public boolean animateAdd(RecyclerView.ViewHolder holder) {
holder.itemView.clearAnimation();
final int screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels;
holder.itemView.setTranslationY(screenHeight);
holder.itemView.animate()
.translationY(0)
.setInterpolator(new DecelerateInterpolator(3.f))
.setDuration(650)
.setStartDelay(450 + holder.getLayoutPosition() * 75)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
dispatchAddFinished(holder);
}
})
.start();
return false;
}
and an example of what code that calls it would look like:
public void filterDataSetExample() {
int oldItemCount = getItemCount();
// ... logic that changes the data set ...
notifyItemRangeRemoved(0, oldItemCount);
notifyItemRangeInserted(0, getItemCount());
}
The first time I do something that calls this sequence, the remove animation plays smoothly followed by the add animations. The problem comes when I try to do it a second time.
On the second time, and any times following, the two animations become "merged" somehow. This is basically what happens (it's the same every time):
The remove animation picks up the delay (base + staggered) from the add animation. This means that the animations are basically playing at the exact same time, which is bad enough already.
The add animation picks up the alpha shift from the remove animation, but only for the ViewHolders on the top 75% or so of the screen. The bottom 2-3 are still visible just fine. But overall, this means 70-80% of my ViewHolders are invisible, throughout my entire list (since they get recycled). At first I thought they disappeared entirely somehow, but if I change the alpha shift to a non-0 value like 0.25f
I can see the holders there, transparent.
On top of those two things - if I completely re-populate the RecyclerView and simply call notifyDataSetChanged()
, which I have no animations set up for currently, some of the invisible ViewHolders will gradually become visible again (2-3 per time, until all of them are back to normal visibility).
Then, if I call notifyDataSetChanged()
enough times to make the whole stack visible again, I'm right exactly back where I started! The animations will work fine for exactly ONE remove -> insert cycle, then merge properties again.
At first, I thought maybe the problem was reusing ViewHolders and them having the same itemView
somehow and the old animation still being attached. Which is why I have the holder.itemView.clearAnimation()
line at the beginning of each method, but it does nothing to the effect.
I'm stumped. Especially since the bottom 2 or so ViewHolders seem to be immune to the effect, despite presumably going through the exact same process as all the others. And it's always the ones at the bottom of the screen, regardless of where I'm scrolled to, so it isn't position-related.
I'm sure there's something (maybe many things) that I'm missing, however, since this is the first time I've worked extensively with an ItemAnimator.
Any help would be greatly appreciated.
Upvotes: 1
Views: 1580
Reputation: 54204
I believe all of your problems come down to the fact that sometimes your ViewHolder
instances are re-used, and sometimes they are not.
I was able to create my own app using the code you posted and reproduce your problems. Here's what my dummy app looks like after two clicks of the GO button:
What's happening here is that the first five ViewHolder
s were re-used and re-bound, while the next five were created from scratch and bound for the first time. Logs verify this:
02-10 12:35:13.112 5754 5754 I System.out: binding view holder: 0
02-10 12:35:13.112 5754 5754 I System.out: binding view holder: 1
02-10 12:35:13.112 5754 5754 I System.out: binding view holder: 2
02-10 12:35:13.114 5754 5754 I System.out: binding view holder: 3
02-10 12:35:13.115 5754 5754 I System.out: binding view holder: 4
02-10 12:35:13.115 5754 5754 I System.out: creating view holder
02-10 12:35:13.116 5754 5754 I System.out: binding view holder: 5
02-10 12:35:13.116 5754 5754 I System.out: creating view holder
02-10 12:35:13.117 5754 5754 I System.out: binding view holder: 6
02-10 12:35:13.117 5754 5754 I System.out: creating view holder
02-10 12:35:13.117 5754 5754 I System.out: binding view holder: 7
02-10 12:35:13.117 5754 5754 I System.out: creating view holder
02-10 12:35:13.117 5754 5754 I System.out: binding view holder: 8
02-10 12:35:13.118 5754 5754 I System.out: creating view holder
02-10 12:35:13.118 5754 5754 I System.out: binding view holder: 9
Because the first five ViewHolder
s have been re-used, their alpha
is still set to 0
... after all, you animated the alpha to 0 during the "remove" process, but you never set it back to 1 anywhere. So just add this in your onAnimationEnd()
callback for the "remove" animation:
holder.itemView.setAlpha(1);
The other problem (overlapping start delays) I'm not exactly sure about. I know how to fix it, but I'm a little bit unclear on why the fix works. Again, it seems that the system is picking up old values... you set a start delay for the "add" animation, but you never explicitly set a start delay for the "remove" animation... so it picks up the old "add" delay. Just add this to your animate()
chain on the "remove" animation:
.setStartDelay(0)
With both of those in place, my dummy app seems to work perfectly.
Upvotes: 3