VerumCH
VerumCH

Reputation: 3175

RecyclerView ItemAnimator 'remove' animation merges with 'add' animation

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):

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

Answers (1)

Ben P.
Ben P.

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:

enter image description here

What's happening here is that the first five ViewHolders 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 ViewHolders 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

Related Questions