Как обеспечить пользовательскую анимацию во время сортировки (notifyDataSetChanged) на 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?

2 ответов


вот еще одно направление, на которое вы можете посмотреть, если вы не хотите, чтобы ваш свиток сбрасывался при каждом сортировке (демонстрационный проект 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());

для анимационного кода Вы можете использовать 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();
}

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

Edit:

для ускорения предварительной обработки 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);

прежде всего:

  • это решение предполагает, что элементы, которые по-прежнему видны после изменения набора данных, также скользят вправо, а затем снова скользят снизу (это, по крайней мере, то, что я понял, что вы просите)
  • из-за этого требования я не смог найти простое и хорошее решение для этой проблемы (по крайней мере, во время первой итерации). Единственный способ, который я нашел, - обмануть адаптер и бороться с каркасом, чтобы что - то сделать для этого она и не предназначалась. Вот почему первая часть (как она обычно работает) описывает, как достичь приятной анимации с помощью RecyclerView на по умолчанию. Во второй части описывается решение, как применить слайд-выход / слайд в анимации для всех элементов после изменения набора данных.
  • позже я нашел лучше решение, которое не требует, чтобы обмануть адаптер со случайными идентификаторами (перейти к нижней части для обновленной версии).

как это нормально работает

чтобы включить анимацию, вам нужно сказать RecyclerView как изменился набор данных (чтобы он знал, какие анимации следует запускать). Это можно сделать двумя способами:--41-->

1) Простой Вариант: Нам нужно установить adapter.setHasStableIds(true); и предоставление идентификаторов ваших предметов через public long getItemId(int position) в своем Adapter до RecyclerView. The RecyclerView использует эти идентификаторы, чтобы выяснить, какие элементы были удалены/добавлены/перемещается во время вызова adapter.notifyDataSetChanged();

2) Расширенная Версия: вместо adapter.notifyDataSetChanged(); вы также можете явно указать, как изменился набор данных. The Adapter предоставляет несколько методов, как adapter.notifyItemChanged(int position),adapter.notifyItemInserted(int position),... для описания изменений в наборе данных

анимации, которые запускаются для отражения изменений в наборе данных, управляются ItemAnimator. The RecyclerView уже оснащен хорошим по умолчанию DefaultItemAnimator. Кроме того, можно определить пользовательскую анимацию поведение с пользовательским ItemAnimator.

стратегия для реализации слайд (справа), слайд в (внизу)

слайд справа-это анимация, которая должна воспроизводиться, если элементы удалены из набора данных. Слайд из нижней анимации должен воспроизводиться для элементов, добавленных в набор данных. Как упоминалось в начале, я предполагаю, что желательно, чтобы все элементы скользили вправо и скользили снизу. Даже если они видны до и после изменение набора данных. Обычно RecyclerView будет играть, чтобы изменить / переместить анимацию для таких элементов, которые остаются видимыми. Однако, поскольку мы хотим использовать анимацию удаления / добавления для всех элементов, нам нужно обмануть адаптер, думая, что после изменения есть только новые элементы, и все ранее доступные элементы были удалены. Это может быть достигнуто путем предоставления случайного идентификатора для каждого элемента в адаптере:

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

теперь нам нужно обеспечить таможню 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; }
}

это окончательный результат:

enter image description here

Update: при чтении сообщения снова я понял лучшее решение

это обновленное решение не требует, чтобы обмануть адаптер со случайными идентификаторами, думая, что все элементы были удалены и добавлены только новые элементы. Если мы применяем 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 еще надо анимировать изменения.