如何在RecyclerView上sorting(notifyDataSetChanged)期间提供自定义animation

目前,通过使用默认的animationandroid.support.v7.widget.DefaultItemAnimator ,这是我在sorting过程中的结果

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

但是,在sorting过程中(notifyDataSetChanged),而不是默认的animation,我更喜欢提供自定义animation如下。 旧物品将通过右侧滑出,新物品将滑落。

预期的animationvideo: https //youtu.be/9aQTyM7K4B0

如何在没有RecylerView的情况下实现这样的animation

几年前,我通过使用LinearLayout + View来达到这个效果,那时我们还没有RecyclerView

这是如何设置animation

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

而这就是animation被触发的方式。

 // 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中实现相同的效果?

如果你不希望你的滚动在每一种sorting( GITHUB演示项目 )上重置,那么你可以看一个更多的方向:

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

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

对于animation代码,你可以使用android.support.v7.widget.DefaultItemAnimator ,但是这个类有私有的animateChangeImpl()所以你将不得不复制粘贴代码,并改变这个方法或使用reflection。 或者你可以创build自己的ItemAnimator类,像@Andreas Wenger在他的SlidingAnimator例子中做的SlidingAnimator 。 这里的重点是实现animateChangeImpl类似于你的代码有2个animation:

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

这里是演示图像工作滚动和有点类似的animationhttps://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); 

首先:

  • 这个解决scheme假设在数据集改变之后,仍然可见的项目也向右滑动,然后再从最下面滑动(这至less是我所了解的你所要求的)
  • 由于这个要求,我找不到这个问题的一个简单和很好的解决scheme(至less在第一次迭代期间)。 我发现的唯一方法是欺骗适配器 – 并且打架构做一些不适合的东西。 这就是为什么第一部分(如何正常工作)描述了如何使用RecyclerView默认方式实现漂亮的animation。 第二部分描述了如何在数据集更改之后为所有项目执行幻灯片放映/幻灯片放映。
  • 后来,我find了一个更好的解决scheme,不需要用随机ID来欺骗适配器(跳到最新版本的底部)。

它通常如何工作

要启用animation,您需要告诉RecyclerView数据集是如何改变的(以便它知道应该运行什么样的animation)。 这可以通过两种方式来完成:

1)简单版本:我们需要设置adapter.setHasStableIds(true); 并通过Adapter中的public long getItemId(int position)提供您的项目的ID到RecyclerViewRecyclerView利用这些ID来确定在调用adapter.notifyDataSetChanged();期间删除/添加/移动了哪些项目adapter.notifyDataSetChanged();

2)高级版本:而不是调用adapter.notifyDataSetChanged(); 你也可以明确说明数据集是如何改变的。 Adapter提供了几个方法,比如adapter.notifyItemChanged(int position)adapter.notifyItemInserted(int position) ,…来描述数据集中的变化

被触发以反映数据集中变化的animation由ItemAnimatorpipe理。 RecyclerView已经配备了一个不错的DefaultItemAnimator 。 此外,可以使用自定义ItemAnimator定义自定义animation行为。

实施幻灯片的策略(右),幻灯片(下)

向右的幻灯片是从数据集中删除项目时应播放的animation。 应该为添加到数据集的项目播放底部animation的幻灯片。 正如我在开始时提到的那样,我们假设所有元素都向右滑动并从底部滑入。 即使它们在数据集更改之前和之后都是可见的。 正常情况下, RecyclerView会播放更改/移动这些可见的项目的animation。 但是,因为我们想要为所有项目使用删除/添加animation,所以我们需要欺骗适配器,使其认为更改后只有新的元素,并且所有以前可用的项目都被删除。 这可以通过为适配器中的每个项目提供随机ID来实现:

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

现在我们需要提供一个自定义的ItemAnimatorpipe理添加/删除项目的animation。 呈现的SlidingAnimator的结构与RecyclerView提供的android.support.v7.widget.DefaultItemAnimator非常相似。 另请注意,这是一个概念certificate,应在任何应用程序中使用之前进行调整:

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

这是最后的结果:

在这里输入图像说明

更新:在阅读文章时,我想出了一个更好的解决scheme

这个更新的解决scheme不需要用随机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仍然需要animation变化。