Подтвердить что ты не робот

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

4b9b3361

Ответ 1

Вот еще одно направление, на которое вы можете посмотреть, если вы не хотите, чтобы ваш свиток показывал reset для каждого вида (демонстрационный проект GITHUB):

Используйте какой-то 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, но этот класс имеет частный animateChangeImpl(), поэтому вам нужно будет скопировать код и изменить этот метод или использовать отражение. Или вы можете создать свой собственный класс ItemAnimator, например @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

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

Ответ 2

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

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

Как обычно работает

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

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

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

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

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

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

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

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

введите описание изображения здесь

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

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