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