android - que - Cómo proporcionar una animación personalizada durante la clasificación(notifyDataSetChanged) en RecyclerView
recyclerview cardview horizontal (2)
Ante todo:
- Esta solución supone que los elementos que aún están visibles, después de que se cambió el conjunto de datos, también se deslizan hacia la derecha y luego vuelven a deslizarse desde la parte inferior nuevamente (al menos eso es lo que entendí que está pidiendo)
- Debido a este requisito, no pude encontrar una solución fácil y agradable para este problema (al menos durante la primera iteración). La única forma que encontré fue engañar al adaptador y luchar contra el marco para hacer algo para lo que no estaba destinado. Esta es la razón por la que la primera parte (Cómo funciona normalmente) describe cómo lograr animaciones agradables con
RecyclerView
manera predeterminada . La segunda parte describe la solución sobre cómo imponer la salida / diapositiva en animación para todos los elementos después de que el conjunto de datos haya cambiado. - Más adelante, encontré una mejor solución que no requiere engañar al adaptador con identificadores aleatorios (saltar a la parte inferior de la versión actualizada).
Como funciona normalmente
Para habilitar animaciones, debe decirle al RecyclerView
cómo cambió el conjunto de datos (para que sepa qué tipo de animaciones deben ejecutarse). Esto se puede hacer de dos formas:
1) Versión simple: Necesitamos configurar adapter.setHasStableIds(true);
y proporcionar los identificadores de sus artículos a través de public long getItemId(int position)
en su Adapter
al RecyclerView
. RecyclerView
utiliza estos identificadores para averiguar qué elementos se eliminaron / agregaron / movieron durante la llamada a adapter.notifyDataSetChanged();
2) Versión avanzada: en lugar de llamar a adapter.notifyDataSetChanged();
También puede indicar explícitamente cómo cambió el conjunto de datos. El Adapter
proporciona varios métodos, como adapter.notifyItemChanged(int position)
, adapter.notifyItemInserted(int position)
, ... para describir los cambios en el conjunto de datos
Las animaciones que se activan para reflejar los cambios en el conjunto de datos son administradas por el ItemAnimator
. RecyclerView
ya está equipado con un buen valor predeterminado DefaultItemAnimator
. Además, es posible definir un comportamiento de animación personalizado con un ItemAnimator
personalizado.
Estrategia para implementar el deslizar hacia fuera (derecha), deslizar hacia adentro (abajo)
La diapositiva a la derecha es la animación que se debe reproducir si los elementos se eliminan del conjunto de datos. La diapositiva desde la animación inferior debe reproducirse para los elementos que se agregaron al conjunto de datos. Como mencioné al principio, asumo que es deseable que todos los elementos se deslicen hacia la derecha y se deslicen desde la parte inferior. Incluso si son visibles antes y después del cambio de conjunto de datos. Normalmente, RecyclerView
jugaría para cambiar / mover animaciones para los elementos que permanecen visibles. Sin embargo, debido a que queremos utilizar la animación de quitar / agregar para todos los elementos, necesitamos engañar al adaptador para que piense que solo hay elementos nuevos después del cambio y que se eliminaron todos los elementos previamente disponibles. Esto se puede lograr proporcionando una identificación aleatoria para cada elemento en el adaptador:
@Override
public long getItemId(int position) {
return Math.round(Math.random() * Long.MAX_VALUE);
}
Ahora debemos proporcionar un ItemAnimator
personalizado que administre las animaciones para los elementos agregados / eliminados. La estructura del SlidingAnimator
presentado es muy similar al android.support.v7.widget.DefaultItemAnimator
que se proporciona con RecyclerView
. También tenga en cuenta que esto es una prueba de concepto y debe ajustarse antes de utilizarlo en cualquier aplicación:
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; }
}
Este es el resultado final:
Actualización: Mientras leía la publicación otra vez, descubrí una mejor solución.
Esta solución actualizada no requiere engañar al adaptador con identificadores aleatorios para que piense que todos los elementos se eliminaron y solo se agregaron elementos nuevos. Si aplicamos la 2) Versión avanzada : cómo notificar al adaptador acerca de los cambios en el conjunto de datos, podemos decirle al adapter
que se eliminaron todos los elementos anteriores y se agregaron todos los nuevos elementos:
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();
El SlidingAnimator
presentado anteriormente todavía es necesario para animar los cambios.
Actualmente, al utilizar el animador predeterminado android.support.v7.widget.DefaultItemAnimator
, este es el resultado que tengo durante la clasificación
Video de animación 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();
}
Sin embargo, en lugar de la animación predeterminada durante la clasificación (notifyDataSetChanged), prefiero proporcionar una animación personalizada de la siguiente manera. El elemento antiguo se deslizará hacia el lado derecho y el nuevo elemento se deslizará hacia arriba.
Video de animación esperado: https://youtu.be/9aQTyM7K4B0
Cómo logro tal animación sin RecylerView
Hace algunos años, logro este efecto usando LinearLayout
+ View
, ya que en ese momento no tenemos RecyclerView
todavía.
Así es como se configura la animación.
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);
y, así es como se activa la animación.
// 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);
}
Me preguntaba, ¿cómo puedo lograr el mismo efecto en RecylerView?
Aquí hay una dirección más que puede mirar, si no desea que su desplazamiento se reinicie en cada ordenamiento ( proyecto de demostración GITHUB ):
Use algún tipo de RecyclerView.ItemAnimator
, pero en lugar de reescribir las animateAdd()
y animateRemove()
, puede implementar animateChange()
y animateChangeImpl()
. Después de ordenar, puede llamar a adapter.notifyItemRangeChanged(0, mItems.size());
a la animación trigerina. Así que el código para activar la animación se verá bastante simple:
for (int i = 0, j = mItems.size() - 1; i < j; i++, j--)
Collections.swap(mItems, i, j);
adapter.notifyItemRangeChanged(0, mItems.size());
Para el código de animación puede usar android.support.v7.widget.DefaultItemAnimator
, pero esta clase tiene animateChangeImpl()
privado animateChangeImpl()
por lo que tendrá que copiar y pegar el código y cambiar este método o usar la reflexión. O puede crear su propia clase de @Andreas Wenger
como @Andreas Wenger
hizo en su ejemplo de SlidingAnimator
. El punto aquí es implementar animateChangeImpl
Simmilar a tu código, hay 2 animaciones:
1) Deslice la vista antigua hacia la derecha
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) deslice hacia arriba la nueva vista
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();
}
Aquí está la imagen de demostración con desplazamiento de trabajo y una animación similar https://i.gyazo.com/04f4b767ea61569c00d3b4a4a86795ce.gif https://i.gyazo.com/57a52b8477a361c383d44664392db0be.gif
Editar:
Para acelerar el adapter.notifyItemRangeChanged(0, mItems.size());
de RecyclerView, en lugar de adapter.notifyItemRangeChanged(0, mItems.size());
probablemente querrás usar algo como:
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);