RecyclerView parpadeando después de notifyDatasetChanged ()

113

Tengo un RecyclerView que carga algunos datos de la API, incluye una URL de imagen y algunos datos, y uso networkImageView para cargar la imagen de forma diferida.

@Override
public void onResponse(List<Item> response) {
   mItems.clear();
   for (Item item : response) {
      mItems.add(item);
   }
   mAdapter.notifyDataSetChanged();
   mSwipeRefreshLayout.setRefreshing(false);
}

Aquí está la implementación para el adaptador:

public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, final int position) {
        if (isHeader(position)) {
            return;
        }
        // - get element from your dataset at this position
        // - replace the contents of the view with that element
        MyViewHolder holder = (MyViewHolder) viewHolder;
        final Item item = mItems.get(position - 1); // Subtract 1 for header
        holder.title.setText(item.getTitle());
        holder.image.setImageUrl(item.getImg_url(), VolleyClient.getInstance(mCtx).getImageLoader());
        holder.image.setErrorImageResId(android.R.drawable.ic_dialog_alert);
        holder.origin.setText(item.getOrigin());
    }

El problema es que cuando tenemos una actualización en la vista de reciclaje, parpadea durante un tiempo muy corto al principio, lo que parece extraño.

En su lugar, solo usé GridView / ListView y funcionó como esperaba. No hubo parpadeo.

configuración para RecycleView en onViewCreated of my Fragment:

mRecyclerView = (RecyclerView) view.findViewById(R.id.recyclerView);
        // use this setting to improve performance if you know that changes
        // in content do not change the layout size of the RecyclerView
        mRecyclerView.setHasFixedSize(true);

        mGridLayoutManager = (GridLayoutManager) mRecyclerView.getLayoutManager();
        mGridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
            @Override
            public int getSpanSize(int position) {
                return mAdapter.isHeader(position) ? mGridLayoutManager.getSpanCount() : 1;
            }
        });

        mRecyclerView.setAdapter(mAdapter);

¿Alguien se enfrenta a tal problema? ¿Cuál podría ser la razón?

Ali
fuente

Respuestas:

126

Intente usar ID estables en su RecyclerView.Adapter

setHasStableIds(true)y anular getItemId(int position).

Sin ID estables, después notifyDataSetChanged(), los ViewHolders generalmente asignados no a las mismas posiciones. Esa fue la razón de parpadear en mi caso.

Puedes encontrar una buena explicación aquí.

Anatoly Vdovichev
fuente
1
Agregué setHasStableIds (verdadero), que resolvió el parpadeo, pero en mi GridLayout, los elementos aún cambian de lugar al desplazarse. ¿Qué debo anular en getItemId ()? Gracias
Balázs Orbán
3
¿Alguna idea de cómo deberíamos generar el ID?
Mauker
¡Usted es maravilloso! Google NO lo es. ¡Google no sabe nada de esto O lo sabe y no quiere que lo sepamos! GRACIAS HOMBRE
MBH
1
arruinar las posiciones de los elementos, los elementos provienen de la base de datos de firebase en el orden correcto.
MuhammadAliJr
1
@Mauker Si su objeto tiene un número único, puede usarlo o si tiene una cadena única, puede usar object.hashCode (). Eso funciona perfectamente bien para mí
Simon Schubert
106

De acuerdo con esta página de problemas ... es la animación de cambio de elemento de la vista de reciclaje predeterminada ... Puede apagarla ... intente esto

recyclerView.getItemAnimator().setSupportsChangeAnimations(false);

Cambiar en la última versión

Citado del blog de desarrolladores de Android :

Tenga en cuenta que esta nueva API no es compatible con versiones anteriores. Si implementó anteriormente un ItemAnimator, puede extender SimpleItemAnimator, que proporciona la API anterior al empaquetar la API nueva. También notará que algunos métodos se han eliminado por completo de ItemAnimator. Por ejemplo, si llamaba a recyclerView.getItemAnimator (). SetSupportsChangeAnimations (false), este código ya no se compilará. Puede reemplazarlo con:

ItemAnimator animator = recyclerView.getItemAnimator();
if (animator instanceof SimpleItemAnimator) {
  ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
}
Sabeer Mohammed
fuente
5
@sabeer sigue siendo el mismo problema. No está resolviendo el problema.
Shreyash Mahajan
3
@delive, avíseme si encontró alguna solución a esto
Shreyash Mahajan
Yo uso picasso, es algo, no parece parpadear.
entrega el
8
Kotlin: (recyclerView.itemAnimator as? SimpleItemAnimator) ?. supportsChangeAnimations = false
Pedro Paulo Amorim
Está funcionando, pero se avecina otro problema (NPE) java.lang.NullPointerException: intento de leer desde el campo 'int android.support.v7.widget.RecyclerView $ ItemAnimator $ ItemHolderInfo.left' en una referencia de objeto nulo ¿Hay alguna forma para solucionar esto?
Navas pk
46

Esto simplemente funcionó:

recyclerView.getItemAnimator().setChangeDuration(0);
Hamzeh Soboh
fuente
es una buena alternativa a codeItemAnimator animator = recyclerView.getItemAnimator (); if (animador instancia de SimpleItemAnimator) {((SimpleItemAnimator) animador) .setSupportsChangeAnimations (falso); }code
ziniestro
1
detiene todas las animaciones AGREGAR y ELIMINAR
MBH
11

Tengo el mismo problema al cargar la imagen de algunas URL y luego imageView parpadea. Resuelto usando

notifyItemRangeInserted()    

en vez de

notifyDataSetChanged()

lo que evita volver a cargar esos datos antiguos sin cambios.

Wesely
fuente
9

intente esto para deshabilitar la animación predeterminada

ItemAnimator animator = recyclerView.getItemAnimator();

if (animator instanceof SimpleItemAnimator) {
  ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
}

esta es la nueva forma de deshabilitar la animación desde el soporte de Android 23

esta forma antigua funcionará para la versión anterior de la biblioteca de soporte

recyclerView.getItemAnimator().setSupportsChangeAnimations(false)
Mohamed Farouk
fuente
4

Suponiendo mItemsque la colección es la que lo respalda Adapter, ¿por qué está eliminando todo y volviendo a agregar? Básicamente, le está diciendo que todo ha cambiado, por lo que RecyclerView vuelve a enlazar todas las vistas de lo que supongo que la biblioteca de imágenes no lo maneja correctamente, donde aún restablece la vista a pesar de que es la misma url de imagen. Tal vez tenían una solución incorporada para AdapterView para que funcione bien en GridView.

En lugar de llamar, notifyDataSetChangedlo que provocará que se vuelvan a vincular todas las vistas, llame a eventos de notificación granular (notificar agregado / eliminado / movido / actualizado) para que RecyclerView vuelva a vincular solo las vistas necesarias y nada parpadeará.

yigit
fuente
3
¿O tal vez es solo un "error" en RecyclerView? Obviamente, si funcionó bien durante los últimos 6 años con AbsListView y ahora no con RecyclerView, significa que algo no está bien con RecyclerView, ¿verdad? :) Un vistazo rápido muestra que cuando actualiza los datos en ListView y GridView, hacen un seguimiento de la vista + posición, por lo que cuando actualiza obtendrá exactamente el mismo marcador de vista. Mientras que RecyclerView baraja los titulares de la vista, lo que provoca que parpadee.
vovkab
Trabajar en ListView no significa que sea correcto para RecyclerView. Estos componentes tienen diferentes arquitecturas.
yigit
1
De acuerdo, pero a veces es realmente difícil saber qué elementos se han cambiado, por ejemplo, si usa cursores o simplemente actualiza todos los datos. Por lo tanto, recyclerview también debería manejar este caso correctamente.
vovkab
4

Recyclerview usa DefaultItemAnimator como su animador predeterminado. Como puede ver en el código a continuación, cambian el alfa del titular de la vista al cambiar el elemento:

@Override
public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromX, int fromY, int toX, int toY) {
    ...
    final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView);
    ...
    ViewCompat.setAlpha(oldHolder.itemView, prevAlpha);
    if (newHolder != null) {
        ....
        ViewCompat.setAlpha(newHolder.itemView, 0);
    }
    ...
    return true;
}

Quería conservar el resto de las animaciones pero eliminar el "parpadeo", así que cloné DefaultItemAnimator y eliminé las 3 líneas alfa de arriba.

Para usar el nuevo animador, simplemente llame a setItemAnimator () en su RecyclerView:

mRecyclerView.setItemAnimator(new MyItemAnimator());
Peter Archivo
fuente
Quitó el parpadeo pero causó un efecto de parpadeo por alguna razón.
Mohamed Medhat
4

En Kotlin puede usar 'extensión de clase' para RecyclerView:

fun RecyclerView.disableItemAnimator() {
    (itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
}

// sample of using in Activity:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                          savedInstanceState: Bundle?): View? {
    // ...
    myRecyclerView.disableItemAnimator()
    // ...
}
Gregory
fuente
1

Hola @Ali, podría ser una repetición tardía. También enfrenté este problema y lo resolví con la siguiente solución, puede ayudarlo, por favor verifique.

Se crea la clase LruBitmapCache.java para obtener el tamaño de la caché de la imagen

import android.graphics.Bitmap;
import android.support.v4.util.LruCache;
import com.android.volley.toolbox.ImageLoader.ImageCache;

public class LruBitmapCache extends LruCache<String, Bitmap> implements
        ImageCache {
    public static int getDefaultLruCacheSize() {
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        final int cacheSize = maxMemory / 8;

        return cacheSize;
    }

    public LruBitmapCache() {
        this(getDefaultLruCacheSize());
    }

    public LruBitmapCache(int sizeInKiloBytes) {
        super(sizeInKiloBytes);
    }

    @Override
    protected int sizeOf(String key, Bitmap value) {
        return value.getRowBytes() * value.getHeight() / 1024;
    }

    @Override
    public Bitmap getBitmap(String url) {
        return get(url);
    }

    @Override
    public void putBitmap(String url, Bitmap bitmap) {
        put(url, bitmap);
    }
}

La clase singleton VolleyClient.java [extiende la aplicación] se agregó debajo del código

en el constructor de la clase singleton VolleyClient, agregue el siguiente fragmento para inicializar el ImageLoader

private VolleyClient(Context context)
    {
     mCtx = context;
     mRequestQueue = getRequestQueue();
     mImageLoader = new ImageLoader(mRequestQueue,getLruBitmapCache());
}

Creé el método getLruBitmapCache () para devolver LruBitmapCache

public LruBitmapCache getLruBitmapCache() {
        if (mLruBitmapCache == null)
            mLruBitmapCache = new LruBitmapCache();
        return this.mLruBitmapCache;
}

Espero que te ayude.

Alumno de Android
fuente
Gracias por tu respuesta. Eso es exactamente lo que hice en mi VollyClient.java. Echa un vistazo a: VolleyClient.java
Ali
Solo verifique una vez con el uso de la clase LruCache <String, Bitmap>, creo que va a resolver su problema. Eche un vistazo a LruCache
Aprendiz de Android
¿Revisaste una vez el código que compartí contigo en el comentario? ¿Qué tienes en tu clase que me perdí allí?
Ali
Te falta extender la clase LruCache <String, Bitmap> y anular el método sizeOf () como lo hice, pero me parece que todo está bien.
Aprendiz de Android
Ok, lo intentaré muy pronto, pero ¿podrías explicarme qué has hecho allí que hizo la magia por ti y resolvió el problema? código fuente Para mí, parece que su método sizeOf anulado debería estar en el código fuente.
Ali
1

para mi recyclerView.setHasFixedSize(true);funciono

Pramod
fuente
No creo que los artículos de OP tengan un tamaño fijo
Irfandi D. Vendy
esto me ayudó en mi caso
bst91
1

En mi caso, ni ninguno de los anteriores ni las respuestas de otras preguntas de stackoverflow que tenían los mismos problemas funcionaron.

Bueno, estaba usando una animación personalizada cada vez que se hacía clic en el elemento, por lo que estaba llamando a notifyItemChanged (int position, Object Payload) para pasar la carga útil a mi clase CustomAnimator.

Tenga en cuenta que hay 2 métodos onBindViewHolder (...) disponibles en RecyclerView Adapter. El método onBindViewHolder (...) que tiene 3 parámetros siempre se llamará antes que el método onBindViewHolder (...) que tenga 2 parámetros.

Generalmente, siempre anulamos el método onBindViewHolder (...) que tiene 2 parámetros y la raíz principal del problema era que estaba haciendo lo mismo, ya que cada vez que se llama a notifyItemChanged (...), nuestro método onBindViewHolder (...) ser llamado, en el que estaba cargando mi imagen en ImageView usando Picasso, y esta fue la razón por la que se estaba cargando nuevamente, independientemente de si estaba desde la memoria o desde Internet. Hasta que se cargó, me mostraba la imagen del marcador de posición, que era la razón por la que parpadeaba durante 1 segundo cada vez que hacía clic en la vista del elemento.

Más tarde, también anulo otro método onBindViewHolder (...) que tiene 3 parámetros. Aquí verifico si la lista de cargas útiles está vacía, luego devuelvo la implementación de superclase de este método; de lo contrario, si hay cargas útiles, solo estoy estableciendo el valor alfa del itemView del titular en 1.

¡Y sí, obtuve la solución a mi problema después de perder un día completo lamentablemente!

Aquí está mi código para los métodos onBindViewHolder (...):

onBindViewHolder (...) con 2 parámetros:

@Override
public void onBindViewHolder(@NonNull RecyclerAdapter.ViewHolder viewHolder, int position) {
            Movie movie = movies.get(position);

            Picasso.with(context)
                    .load(movie.getImageLink())
                    .into(viewHolder.itemView.posterImageView);
    }

onBindViewHolder (...) con 3 parámetros:

@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List<Object> payloads) {
        if (payloads.isEmpty()) {
            super.onBindViewHolder(holder, position, payloads);
        } else {
            holder.itemView.setAlpha(1);
        }
    }

Aquí está el código del método que estaba llamando en onClickListener de viewHolder's itemView en onCreateViewHolder (...):

private void onMovieClick(int position, Movie movie) {
        Bundle data = new Bundle();
        data.putParcelable("movie", movie);

        // This data(bundle) will be passed as payload for ItemHolderInfo in our animator class
        notifyItemChanged(position, data);
    }

Nota: Puede obtener esta posición llamando al método getAdapterPosition () de su viewHolder desde onCreateViewHolder (...).

También he anulado el método getItemId (posición int) de la siguiente manera:

@Override
public long getItemId(int position) {
    Movie movie = movies.get(position);
    return movie.getId();
}

y llamé setHasStableIds(true);a mi objeto adaptador en actividad.

¡Espero que esto ayude si ninguna de las respuestas anteriores funciona!

Parth Bhanushali
fuente
1

En mi caso, hubo un problema mucho más simple, pero puede parecerse mucho al problema anterior. Había convertido un ExpandableListView en un RecylerView con Groupie (usando la función ExpandableGroup de Groupie). Mi diseño inicial tenía una sección como esta:

<androidx.recyclerview.widget.RecyclerView
  android:id="@+id/hint_list"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:background="@android:color/white" />

Con layout_height establecido en "wrap_content", la animación del grupo expandido al grupo colapsado parecía parpadear, pero en realidad solo se estaba animando desde la posición "incorrecta" (incluso después de probar la mayoría de las recomendaciones de este hilo).

De todos modos, simplemente cambiando layout_height a match_parent como este solucionó el problema.

<androidx.recyclerview.widget.RecyclerView
  android:id="@+id/hint_list"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:background="@android:color/white" />
Tom
fuente
0

Tuve un problema similar y esto funcionó para mí.Puede llamar a este método para establecer el tamaño de la caché de imágenes

private int getCacheSize(Context context) {

    final DisplayMetrics displayMetrics = context.getResources().
            getDisplayMetrics();
    final int screenWidth = displayMetrics.widthPixels;
    final int screenHeight = displayMetrics.heightPixels;
    // 4 bytes per pixel
    final int screenBytes = screenWidth * screenHeight * 4;

    return screenBytes * 3;
}
developer_android
fuente
0

para mi aplicación, cambié algunos datos pero no quería que la vista completa parpadeara.

Lo resolví atenuando la vista anterior a 0.5 alfa e iniciando la nueva vista alfa en 0.5. Esto creó una transición de desvanecimiento más suave sin que la vista desapareciera por completo.

Desafortunadamente, debido a implementaciones privadas, no pude subclasificar DefaultItemAnimator para realizar este cambio, así que tuve que clonar el código y realizar los siguientes cambios

en animateChange:

ViewCompat.setAlpha(newHolder.itemView, 0);  //change 0 to 0.5f

en animateChangeImpl:

oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() { //change 0 to 0.5f
Deefer
fuente
0

El uso de métodos de reciclaje apropiados para actualizar las vistas resolverá este problema

Primero, realice cambios en la lista

mList.add(item);
or mList.addAll(itemList);
or mList.remove(index);

Luego notifique usando

notifyItemInserted(addedItemIndex);
or
notifyItemRemoved(removedItemIndex);
or
notifyItemRangeChanged(fromIndex, newUpdatedItemCount);

¡Espero que esto ayude!

Sreedhu Madhu
fuente
Absolutamente no. Si está haciendo varias actualizaciones por segundos (escaneo BLE en mi caso) no funciona en absoluto. Pasé un día para actualizar a esta mierda RecyclerAdapter ... Es mejor mantener ArrayAdapter. Es una pena que no esté usando el patrón MVC, pero al menos es casi utilizable.
Gojir4
0

Solución de Kotlin:

(recyclerViewIdFromXML.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
Robert Pal
fuente