如何在RecyclerView上的排序(notifyDataSetChanged)期间提供自定义动画

Che*_*eng 21 android android-animation notifydatasetchanged android-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();
}
Run Code Online (Sandbox Code Playgroud)

但是,我更喜欢提供自定义动画,而不是排序时的默认动画(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);
Run Code Online (Sandbox Code Playgroud)

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

// 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);
}
Run Code Online (Sandbox Code Playgroud)

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

And*_*ger 9

首先:

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

它通常如何工作

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

1)简单的版本: 我们需要设置adapter.setHasStableIds(true);,并通过提供您的项目的ID public long getItemId(int position)在你AdapterRecyclerView.在RecyclerView利用这些编号,以找出哪些项目中除去/添加/调用期间移动adapter.notifyDataSetChanged();

2)高级版本:adapter.notifyDataSetChanged();您可以明确说明数据集的更改方式,而不是调用您.在Adapter提供了几种方法,比如adapter.notifyItemChanged(int position),adapter.notifyItemInserted(int position)......来描述数据集的变化

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

实现滑出的策略(右),滑入(底部)

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

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

现在我们需要提供一个自定义ItemAnimator来管理添加/删除项目的动画.所提出的结构SlidingAnimator非常相似,android.support.v7.widget.DefaultItemAnimator即具备RecyclerView.另请注意,这是一个概念证明,应该在用于任何应用程序之前进行调整:

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; }
}
Run Code Online (Sandbox Code Playgroud)

这是最终结果:

在此输入图像描述

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

此更新的解决方案不需要使用随机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();
Run Code Online (Sandbox Code Playgroud)

之前提出的SlidingAnimator内容仍然是动画更改的必要条件.


var*_*ren 7

如果您不想在每种排序上重置滚动(GITHUB演示项目),可以看一下这个方向:

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

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

adapter.notifyItemRangeChanged(0, mItems.size());
Run Code Online (Sandbox Code Playgroud)

对于您可以使用的动画代码android.support.v7.widget.DefaultItemAnimator,但此类具有私有,animateChangeImpl()因此您必须复制粘贴的代码并更改此方法或使用反射.或者你可以ItemAnimator@Andreas Wenger他的例子中那样创建自己的类SlidingAnimator.这里的要点是为animateChangeImpl你的代码实现Simmilar有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();
}
Run Code Online (Sandbox Code Playgroud)

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();
}
Run Code Online (Sandbox Code Playgroud)

这是带有工作滚动和类似动画的演示图像 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);
Run Code Online (Sandbox Code Playgroud)