如何在 RecyclerView 排序时提供自定义动画(notifyDataSetChanged)

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

目前,通过使用默认动画制作器 android.support.v7.widget.DefaultItemAnimator,这是我在排序过程中得到的结果

DefaultItemAnimator 动画视频: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();
}

但是,我更喜欢提供自定义动画,而不是排序期间的默认动画 (notifyDataSetChanged)。旧项目从右侧滑出,新项目向上滑动。

期待动画视频:https://youtu.be/9aQTyM7K4B0

我如何在没有 RecylerView 的情况下实现这样的动画

几年前,我用LinearLayout + View实现了这个效果,当时我们还没有RecyclerView

这就是动画的设置方式

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

而且,这就是动画的触发方式。

// 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);
}

我想知道如何在RecylerView中实现同样的效果?

首先:

  • 此解决方案假定在数据集更改后仍然可见的项目也会向右滑出,然后再次从底部滑入(至少我理解您的要求)
  • 由于这个要求,我无法为这个问题找到一个简单而好的解决方案(至少在第一次迭代期间)。我发现的唯一方法是欺骗适配器 - 并与框架作斗争,让它做一些它不打算做的事情。这就是为什么第一部分(它通常是如何工作的)描述了如何使用 RecyclerView 默认方式 来实现漂亮的动画。第二部分描述了如何在数据集更改后为所有项目强制动画中的幻灯片out/slide。
  • 后来我找到了一个更好的解决方案,不需要用随机 ID 欺骗适配器(更新版本跳到底部)。

它通常是如何工作的

要启用动画,您需要告诉 RecyclerView 数据集如何更改(以便它知道应该使用哪种动画 运行)。这可以通过两种方式完成:

1)简单版: 我们需要设置 adapter.setHasStableIds(true); 并通过 public long getItemId(int position) 在您的 Adapter 中向 RecyclerView 提供您的项目 ID。 RecyclerView 利用这些 ID 来确定在调用 adapter.notifyDataSetChanged();

期间哪些项目是 removed/added/moved

2) 高级版本: 除了调用 adapter.notifyDataSetChanged(); 你还可以明确说明数据集是如何变化的。 Adapter 提供了几种方法,如 adapter.notifyItemChanged(int position)adapter.notifyItemInserted(int position)、... 来描述数据集的变化

为反映数据集中的变化而触发的动画由 ItemAnimator 管理。 RecyclerView 已经配备了一个不错的默认值 DefaultItemAnimator。此外,可以使用自定义 ItemAnimator.

定义自定义动画行为

实现滑出(右)、滑入(下)的策略

右边的幻灯片是从数据集中删除项目时应播放的动画。应该为添加到数据集中的项目播放底部动画的幻灯片。如开头所述,我假设希望所有元素都向右滑出并从底部滑入。即使它们在数据集更改前后可见。通常 RecyclerView 会为这些保持可见的项目播放 change/move 动画。然而,因为我们想对所有项目使用 remove/add 动画,我们需要欺骗适配器认为在更改后只有新元素并且所有以前可用的项目都被删除。这可以通过为适配器中的每个项目提供一个随机 ID 来实现:

@Override
public long getItemId(int position) {
    return Math.round(Math.random() * Long.MAX_VALUE);
}

现在我们需要提供自定义 ItemAnimator 来管理 added/removed 项的动画。 SlidingAnimator 的结构与 RecyclerView 提供的 android.support.v7.widget.DefaultItemAnimator 非常相似。另请注意,这是概念证明,在任何应用程序中使用之前应进行调整:

public class SlidingAnimator extends SimpleItemAnimator {
    List<RecyclerView.ViewHolder> pendingAdditions = new ArrayList<>();
    List<RecyclerView.ViewHolder> pendingRemovals = new ArrayList<>();

    @Override
    public void runPendingAnimations() {
        final List<RecyclerView.ViewHolder> additionsTmp = pendingAdditions;
        List<RecyclerView.ViewHolder> removalsTmp = pendingRemovals;
        pendingAdditions = new ArrayList<>();
        pendingRemovals = new ArrayList<>();

        for (RecyclerView.ViewHolder removal : removalsTmp) {
            // run the pending remove animation
            animateRemoveImpl(removal);
        }
        removalsTmp.clear();

        if (!additionsTmp.isEmpty()) {
            Runnable adder = new Runnable() {
                public void run() {
                    for (RecyclerView.ViewHolder addition : additionsTmp) {
                        // run the pending add animation
                        animateAddImpl(addition);
                    }
                    additionsTmp.clear();
                }
            };
            // play the add animation after the remove animation finished
            ViewCompat.postOnAnimationDelayed(additionsTmp.get(0).itemView, adder, getRemoveDuration());
        }
    }

    @Override
    public boolean animateAdd(RecyclerView.ViewHolder holder) {
        pendingAdditions.add(holder);
        // translate the new items vertically so that they later slide in from the bottom
        holder.itemView.setTranslationY(300);
        // also make them invisible
        holder.itemView.setAlpha(0);
        // this requests the execution of runPendingAnimations()
        return true;
    }

    @Override
    public boolean animateRemove(final RecyclerView.ViewHolder holder) {
        pendingRemovals.add(holder);
        // this requests the execution of runPendingAnimations()
        return true;
    }

    private void animateAddImpl(final RecyclerView.ViewHolder holder) {
        View view = holder.itemView;
        final ViewPropertyAnimatorCompat anim = ViewCompat.animate(view);
        anim
                // undo the translation we applied in animateAdd
                .translationY(0)
                // undo the alpha we applied in animateAdd
                .alpha(1)
                .setDuration(getAddDuration())
                .setInterpolator(new DecelerateInterpolator())
                .setListener(new ViewPropertyAnimatorListener() {
                    @Override
                    public void onAnimationStart(View view) {
                        dispatchAddStarting(holder);
                    }

                    @Override
                    public void onAnimationEnd(View view) {
                        anim.setListener(null);
                        dispatchAddFinished(holder);
                        // cleanup
                        view.setTranslationY(0);
                        view.setAlpha(1);
                    }

                    @Override
                    public void onAnimationCancel(View view) {
                    }
                }).start();
    }

    private void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
        View view = holder.itemView;
        final ViewPropertyAnimatorCompat anim = ViewCompat.animate(view);
        anim
                // translate horizontally to provide slide out to right
                .translationX(view.getWidth())
                // fade out
                .alpha(0)
                .setDuration(getRemoveDuration())
                .setInterpolator(new AccelerateInterpolator())
                .setListener(new ViewPropertyAnimatorListener() {
                    @Override
                    public void onAnimationStart(View view) {
                        dispatchRemoveStarting(holder);
                    }

                    @Override
                    public void onAnimationEnd(View view) {
                        anim.setListener(null);
                        dispatchRemoveFinished(holder);
                        // cleanup
                        view.setTranslationX(0);
                        view.setAlpha(1);
                    }

                    @Override
                    public void onAnimationCancel(View view) {
                    }
                }).start();
    }


    @Override
    public boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
        // don't handle animateMove because there should only be add/remove animations
        dispatchMoveFinished(holder);
        return false;
    }
    @Override
    public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) {
        // don't handle animateChange because there should only be add/remove animations
        if (newHolder != null) {
            dispatchChangeFinished(newHolder, false);
        }
        dispatchChangeFinished(oldHolder, true);
        return false;
    }
    @Override
    public void endAnimation(RecyclerView.ViewHolder item) { }
    @Override
    public void endAnimations() { }
    @Override
    public boolean isRunning() { return false; }
}

这是最终结果:

更新:在再次阅读 post 时,我想出了一个更好的解决方案

这个更新的解决方案不需要用随机 ID 来欺骗适配器,让其认为所有项目都已删除并且只添加了新项目。如果我们应用 2) 高级版本 - 如何通知适配器有关数据集的更改,我们可以告诉 adapter 所有以前的项目都已删除,所有新项目都已删除添加:

int oldSize = oldItems.size();
oldItems.clear();
// Notify the adapter all previous items were removed
notifyItemRangeRemoved(0, oldSize);

oldItems.addAll(items);
// Notify the adapter all the new items were added
notifyItemRangeInserted(0, items.size());

// don't call notifyDataSetChanged
//notifyDataSetChanged();

前面介绍的 SlidingAnimator 仍然需要动画更改。

如果您不想在每次排序时都重置卷轴,您可以查看另一个方向 (GITHUB demo project):

使用某种RecyclerView.ItemAnimator,但不是重写animateAdd()animateRemove()函数,您可以实现animateChange()animateChangeImpl()。排序后可以调用adapter.notifyItemRangeChanged(0, mItems.size());触发动画。 所以触发动画的代码看起来很简单:

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

adapter.notifyItemRangeChanged(0, mItems.size());

对于动画代码你可以使用android.support.v7.widget.DefaultItemAnimator,但是这个class有私有animateChangeImpl()所以你必须复制粘贴代码并改变这个方法或使用反射。或者您可以创建自己的 ItemAnimator class,就像 @Andreas Wenger 在他的 SlidingAnimator 示例中所做的那样。这里的重点是实现 animateChangeImpl 与您的代码类似,有 2 个动画:

1) 向右滑动旧视图

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) 向上滑动新视图

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();
}

这是带有工作卷轴和有点类似动画的演示图像 https://i.gyazo.com/04f4b767ea61569c00d3b4a4a86795ce.gif https://i.gyazo.com/57a52b8477a361c383d44664392db0be.gif

编辑:

为了加快 RecyclerView 的性能,您可能希望使用类似以下内容的方式代替 adapter.notifyItemRangeChanged(0, mItems.size());

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