Search code examples
androidandroid-animationandroid-recyclerviewnotifydatasetchanged

How to provide custom animation during sorting (notifyDataSetChanged) on RecyclerView


Currently, by using the default animator android.support.v7.widget.DefaultItemAnimator, here's the outcome I'm having during sorting

DefaultItemAnimator animation video : https://youtu.be/EccI7RUcdbg

public void sortAndNotifyDataSetChanged() {
    int i0 = 0;
    int i1 = models.size() - 1;

    while (i0 < i1) {
        DemoModel o0 = models.get(i0);
        DemoModel o1 = models.get(i1);

        models.set(i0, o1);
        models.set(i1, o0);

        i0++;
        i1--;

        //break;
    }

    // adapter is created via adapter = new RecyclerViewDemoAdapter(models, mRecyclerView, this);
    adapter.notifyDataSetChanged();
}

However, instead of the default animation during sorting (notifyDataSetChanged), I prefer to provide custom animation as follow. Old item will slide out via right side, and new item will slide up.

Expected animation video : https://youtu.be/9aQTyM7K4B0

How I achieve such animation without RecylerView

Few years ago, I achieve this effect by using LinearLayout + View, as that time, we don't have RecyclerView yet.

This is how the animation is being setup

PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1.0f, 0f);
PropertyValuesHolder translationX = PropertyValuesHolder.ofFloat("translationX", 0f, (float) width);
ObjectAnimator animOut = ObjectAnimator.ofPropertyValuesHolder(this, alpha, translationX);

animOut.setDuration(duration);
animOut.setInterpolator(accelerateInterpolator);
animOut.addListener(new AnimatorListenerAdapter() {
    public void onAnimationEnd(Animator anim) {
        final View view = (View) ((ObjectAnimator) anim).getTarget();

        Message message = (Message)view.getTag(R.id.TAG_MESSAGE_ID);
        if (message == null) {
            return;
        }

        view.setAlpha(0f);
        view.setTranslationX(0);
        NewsListFragment.this.refreshUI(view, message);
        final Animation animation = AnimationUtils.loadAnimation(NewsListFragment.this.getActivity(),
            R.anim.slide_up);
        animation.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {

            }

            @Override
            public void onAnimationEnd(Animation animation) {
                view.setVisibility(View.VISIBLE);
                view.setTag(R.id.TAG_MESSAGE_ID, null);
            }

            @Override
            public void onAnimationRepeat(Animation animation) {

            }
        });
        view.startAnimation(animation);
    }
});

layoutTransition.setAnimator(LayoutTransition.DISAPPEARING, animOut);

this.nowLinearLayout.setLayoutTransition(layoutTransition);

and, this is how the animation is being triggered.

// messageView is view being added earlier in nowLinearLayout
for (int i = 0, ei = messageViews.size(); i < ei; i++) {
    View messageView = messageViews.get(i);
    messageView.setTag(R.id.TAG_MESSAGE_ID, messages.get(i));
    messageView.setVisibility(View.INVISIBLE);
}

I was wondering, how I can achieve the same effect in RecylerView?


Solution

  • Here is one more direction you can look at, if you don't want your scroll to reset on each sort (GITHUB demo project):

    Use some kind of RecyclerView.ItemAnimator, but instead of rewriting animateAdd() and animateRemove() functions, you can implement animateChange() and animateChangeImpl(). After sort you can call adapter.notifyItemRangeChanged(0, mItems.size()); to triger animation. So code to trigger animation will look pretty simple:

    for (int i = 0, j = mItems.size() - 1; i < j; i++, j--)
        Collections.swap(mItems, i, j);
    
    adapter.notifyItemRangeChanged(0, mItems.size());
    

    For animation code you can use android.support.v7.widget.DefaultItemAnimator, but this class has private animateChangeImpl() so you will have to copy-pasted code and changed this method or use reflection. Or you can create your own ItemAnimator class like @Andreas Wenger did in his example of SlidingAnimator. The point here is to implement animateChangeImpl Simmilar to your code there are 2 animations:

    1) Slide old view to the right

    private void animateChangeImpl(final ChangeInfo changeInfo) {
        final RecyclerView.ViewHolder oldHolder = changeInfo.oldHolder;
        final View view = oldHolder == null ? null : oldHolder.itemView;
        final RecyclerView.ViewHolder newHolder = changeInfo.newHolder;
        final View newView = newHolder != null ? newHolder.itemView : null;
    
        if (view == null) return;
        mChangeAnimations.add(oldHolder);
    
        final ViewPropertyAnimatorCompat animOut = ViewCompat.animate(view)
                .setDuration(getChangeDuration())
                .setInterpolator(interpolator)
                .translationX(view.getRootView().getWidth())
                .alpha(0);
    
        animOut.setListener(new VpaListenerAdapter() {
            @Override
            public void onAnimationStart(View view) {
                dispatchChangeStarting(oldHolder, true);
            }
    
            @Override
            public void onAnimationEnd(View view) {
                animOut.setListener(null);
                ViewCompat.setAlpha(view, 1);
                ViewCompat.setTranslationX(view, 0);
                dispatchChangeFinished(oldHolder, true);
                mChangeAnimations.remove(oldHolder);
    
                dispatchFinishedWhenDone();
    
                // starting 2-nd (Slide Up) animation
                if (newView != null)
                    animateChangeInImpl(newHolder, newView);
            }
        }).start();
    }
    

    2) Slide up new view

    private void animateChangeInImpl(final RecyclerView.ViewHolder newHolder,
                                     final View newView) {
    
        // setting starting pre-animation params for view
        ViewCompat.setTranslationY(newView, newView.getHeight());
        ViewCompat.setAlpha(newView, 0);
    
        mChangeAnimations.add(newHolder);
    
        final ViewPropertyAnimatorCompat animIn = ViewCompat.animate(newView)
                .setDuration(getChangeDuration())
                .translationY(0)
                .alpha(1);
    
        animIn.setListener(new VpaListenerAdapter() {
            @Override
            public void onAnimationStart(View view) {
                dispatchChangeStarting(newHolder, false);
            }
    
            @Override
            public void onAnimationEnd(View view) {
                animIn.setListener(null);
                ViewCompat.setAlpha(newView, 1);
                ViewCompat.setTranslationY(newView, 0);
                dispatchChangeFinished(newHolder, false);
                mChangeAnimations.remove(newHolder);
                dispatchFinishedWhenDone();
            }
        }).start();
    }
    

    Here is demo image with working scroll and kinda similar animation https://i.gyazo.com/04f4b767ea61569c00d3b4a4a86795ce.gif https://i.gyazo.com/57a52b8477a361c383d44664392db0be.gif

    Edit:

    To speed up RecyclerView preformance, instead of adapter.notifyItemRangeChanged(0, mItems.size()); you probably would want to use something like:

    LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
    int firstVisible = layoutManager.findFirstVisibleItemPosition();
    int lastVisible = layoutManager.findLastVisibleItemPosition();
    int itemsChanged = lastVisible - firstVisible + 1; 
    // + 1 because we start count items from 0
    
    adapter.notifyItemRangeChanged(firstVisible, itemsChanged);