RecyclerView piscando após notificarDatasetChanged ()

113

Eu tenho um RecyclerView que carrega alguns dados da API, inclui um url de imagem e alguns dados, e eu uso o networkImageView para carregar a imagem lentamente.

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

Aqui está a implementação para o 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());
    }

O problema é que quando atualizamos no recyclerView, ele fica ofuscado por um tempo muito curto no início, o que parece estranho.

Em vez disso, usei GridView / ListView e funcionou como esperado. Não houve cegueira.

configuração para RecycleView em 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);

Alguém enfrenta esse problema? Qual seria a razão?

Todos
fonte

Respostas:

126

Tente usar IDs estáveis ​​em seu RecyclerView.Adapter

setHasStableIds(true)e substituir getItemId(int position).

Depois notifyDataSetChanged(), sem IDs estáveis , ViewHolders geralmente não são atribuídos às mesmas posições. Esse foi o motivo de piscar no meu caso.

Você pode encontrar uma boa explicação aqui.

Anatoly Vdovichev
fonte
1
Eu adicionei setHasStableIds (true), que resolveu a oscilação, mas em meu GridLayout, os itens ainda mudam de lugar na rolagem. O que devo substituir em getItemId ()? Obrigado
Balázs Orbán
3
Alguma ideia de como devemos gerar o ID?
Mauker
Você é demais! O Google NÃO é. O Google não sabe disso OU sabe e não quer que saibamos! OBRIGADO HOMEM
MBH
1
bagunçar as posições dos itens, os itens vêm do banco de dados do firebase na ordem correta.
MuhammadAliJr
1
@Mauker Se o seu objeto tiver um número exclusivo, você pode usá-lo ou se tiver uma string exclusiva, você pode usar object.hashCode (). Isso funciona perfeitamente bem para mim
Simon Schubert,
106

De acordo com esta página de problema .... é a animação de mudança de item de Recycleview padrão ... Você pode desligá-la .. tente isto

recyclerView.getItemAnimator().setSupportsChangeAnimations(false);

Mudança na última versão

Citado do blog do desenvolvedor Android :

Observe que esta nova API não é compatível com versões anteriores. Se você implementou anteriormente um ItemAnimator, você pode estender SimpleItemAnimator, que fornece a API antiga envolvendo a nova API. Você também notará que alguns métodos foram totalmente removidos do ItemAnimator. Por exemplo, se você estiver chamando recyclerView.getItemAnimator (). SetSupportsChangeAnimations (false), este código não compilará mais. Você pode substituí-lo por:

ItemAnimator animator = recyclerView.getItemAnimator();
if (animator instanceof SimpleItemAnimator) {
  ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
}
Sabeer Mohammed
fonte
5
@sabeer ainda é o mesmo problema. Não está resolvendo o problema.
Shreyash Mahajan
3
@delive, deixe-me saber se você encontrou alguma solução para isso
Shreyash Mahajan
Estou usando o picasso, é alguma coisa, não aparece piscar.
8
Kotlin: (recyclerView.itemAnimator as? SimpleItemAnimator) ?. supportedChangeAnimations = false
Pedro Paulo Amorim
Está funcionando, mas outro problema está chegando (NPE) java.lang.NullPointerException: tentativa de ler do campo 'int android.support.v7.widget.RecyclerView $ ItemAnimator $ ItemHolderInfo.left' em uma referência de objeto nulo Existe alguma maneira resolver isso?
Navas pk
46

Isso simplesmente funcionou:

recyclerView.getItemAnimator().setChangeDuration(0);
Hamzeh Soboh
fonte
é uma boa alternativa para codeItemAnimator animator = recyclerView.getItemAnimator (); if (instância do animador de SimpleItemAnimator) {((SimpleItemAnimator) animador) .setSupportsChangeAnimations (false); }code
ziniestro
1
para todas as animações ADICIONAR e REMOVER
MBH
11

Eu tenho o mesmo problema ao carregar a imagem de alguns urls e, em seguida, pisca imageView. Resolvido usando

notifyItemRangeInserted()    

ao invés de

notifyDataSetChanged()

o que evita recarregar os dados antigos inalterados.

Wesely
fonte
9

tente isso para desativar a animação padrão

ItemAnimator animator = recyclerView.getItemAnimator();

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

esta é a nova maneira de desabilitar a animação desde o suporte para Android 23

esta forma antiga funcionará para a versão mais antiga da biblioteca de suporte

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

Supondo que mItemsseja a coleção que o sustenta Adapter, por que você está removendo tudo e adicionando novamente? Basicamente, você está dizendo a ele que tudo mudou, então o RecyclerView vincula novamente todas as visualizações, então presumo que a biblioteca de imagens não o manipule adequadamente, onde ainda redefine a visualização, embora seja o mesmo URL da imagem. Talvez eles tenham alguma solução pronta para o AdapterView para que funcione bem no GridView.

Em vez de chamar, o notifyDataSetChangedque causará a religação de todas as visualizações, chame eventos de notificação granular (notificação adicionada / removida / movida / atualizada) para que o RecyclerView religue apenas as visualizações necessárias e nada pisque.

yigit
fonte
3
Ou talvez seja apenas um "bug" no RecyclerView? Obviamente, se funcionou bem nos últimos 6 anos com AbsListView e agora não funciona com o RecyclerView, significa que algo não está bem com o RecyclerView, certo? :) Uma olhada rápida mostra que quando você atualiza os dados em ListView e GridView, eles mantêm o controle de visão + posição, então quando você atualizar você obterá exatamente o mesmo visualizador. Enquanto o RecyclerView embaralha os suportes de visualização, o que leva à oscilação.
vovkab
Trabalhar em ListView não significa que seja correto para o RecyclerView. Esses componentes possuem arquiteturas diferentes.
yigit de
1
Concordo, mas às vezes é muito difícil saber quais itens foram alterados, por exemplo, se você usa cursores ou apenas atualiza todos os dados. Portanto, o recyclerview também deve tratar esse caso corretamente.
vovkab de
4

Recyclerview usa DefaultItemAnimator como seu animador padrão. Como você pode ver no código abaixo, eles mudam o alfa do portador de visualização quando o item muda:

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

Eu queria reter o resto das animações, mas remover a "cintilação", então clonuei DefaultItemAnimator e removi as 3 linhas alfa acima.

Para usar o novo animador, basta chamar setItemAnimator () em seu RecyclerView:

mRecyclerView.setItemAnimator(new MyItemAnimator());
Arquivo Peter
fonte
Ele removeu o piscar, mas causou um efeito de oscilação por algum motivo.
Mohamed Medhat
4

No Kotlin, você pode usar 'extensão de classe' 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
fonte
1

Ei @Ali, pode ser o replay atrasado. Eu também enfrentei esse problema e resolvi com a solução abaixo, pode ajudar você, verifique.

A classe LruBitmapCache.java é criada para obter o tamanho do cache de imagem

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

VolleyClient.java singleton [estende o aplicativo] adicionado abaixo do código

no construtor da classe singleton VolleyClient adicione o trecho abaixo para inicializar o ImageLoader

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

Criei o método getLruBitmapCache () para retornar LruBitmapCache

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

Espero que ajude você.

Aluno de Android
fonte
Obrigado pela sua resposta. Isso é exatamente o que eu fiz no meu VollyClient.java. Dê uma olhada em: VolleyClient.java
Ali
Basta verificar uma vez usando a classe LruCache <String, Bitmap>, acho que vai resolver seu problema. Dê uma olhada em LruCache
aluno Android de
Você verificou uma vez o código que compartilhei com você no comentário? o que você tem na sua aula que eu perdi lá?
Ali de
Você está perdendo a extensão da classe LruCache <String, Bitmap> e a substituição do método sizeOf () como eu fiz, permanecendo todos parecem ok para mim.
Aluno de Android de
Ok, vou tentar muito em breve, mas você poderia me explicar o que você fez lá que fez a mágica para você e resolveu o problema? código fonte Para mim, parece que o método sizeOf substituído deve estar no código-fonte.
Ali de
1

para mim recyclerView.setHasFixedSize(true);funcionou

Pramod
fonte
Não acho que os itens do OP tenham tamanho fixo
Irfandi D. Vendy
isso me ajudou no meu caso
bst91
1

No meu caso, nenhum dos itens acima nem as respostas de outras questões stackoverflow com os mesmos problemas funcionaram.

Bem, eu estava usando animação personalizada cada vez que o item era clicado, para o qual eu estava chamando notificarItemChanged (posição int, carga útil do objeto) para passar carga útil para minha classe CustomAnimator.

Observe, há 2 métodos onBindViewHolder (...) disponíveis no Adaptador RecyclerView. O método onBindViewHolder (...) com 3 parâmetros sempre será chamado antes do método onBindViewHolder (...) com 2 parâmetros.

Geralmente, sempre substituímos o método onBindViewHolder (...) com 2 parâmetros e a principal raiz do problema era que eu estava fazendo o mesmo, pois cada vez que notificarItemChanged (...) for chamado, nosso método onBindViewHolder (...) irá ser chamado, no qual estava carregando minha imagem no ImageView usando o Picasso, e por isso estava carregando novamente independentemente de ser da memória ou da internet. Até o carregamento, ele me mostrava a imagem do placeholder, que era o motivo de piscar por 1 segundo sempre que clicava no itemview.

Posteriormente, também substituo outro método onBindViewHolder (...) com 3 parâmetros. Aqui, eu verifico se a lista de cargas úteis está vazia e, em seguida, retorno a implementação da superclasse desse método; caso contrário, se houver cargas úteis, estou apenas definindo o valor alfa do itemView do titular como 1.

E sim, consegui a solução para o meu problema depois de perder um dia inteiro, infelizmente!

Este é meu código para os métodos onBindViewHolder (...):

onBindViewHolder (...) com 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 (...) com 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);
        }
    }

Aqui está o código do método que eu estava chamando em onClickListener do itemView do viewHolder em 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: Você pode obter esta posição chamando o método getAdapterPosition () de seu viewHolder de onCreateViewHolder (...).

Também substituí o método getItemId (int position) da seguinte maneira:

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

e chamei setHasStableIds(true);meu objeto adaptador em atividade.

Espero que isso ajude se nenhuma das respostas acima funcionar!

Parth Bhanushali
fonte
1

No meu caso, havia um problema muito mais simples, mas pode ser muito parecido com o problema acima. Eu tinha convertido um ExpandableListView em um RecylerView com Groupie (usando o recurso ExpandableGroup do Groupie). Meu layout inicial tinha uma seção 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" />

Com layout_height definido como "wrap_content", a animação do grupo expandido para o grupo recolhido parecia que iria piscar, mas na verdade estava apenas animando da posição "errada" (mesmo depois de tentar a maioria das recomendações neste tópico).

De qualquer forma, simplesmente alterar layout_height para match_parent assim resolveu o 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
fonte
0

Eu tive um problema semelhante e isso funcionou para mim Você pode chamar este método para definir o tamanho do cache de imagem

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
fonte
0

para meu aplicativo, tive alguns dados alterados, mas não queria que toda a visualização piscasse.

Eu resolvi isso apenas diminuindo a visão antiga em 0,5 alfa e iniciando a nova visualização em 0,5. Isso criou uma transição de desbotamento mais suave sem fazer a visualização desaparecer completamente.

Infelizmente, por causa de implementações privadas, não pude criar uma subclasse de DefaultItemAnimator para fazer essa alteração, então tive que clonar o código e fazer as seguintes alterações

em animateChange:

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

em animateChangeImpl:

oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() { //change 0 to 0.5f
Adiar
fonte
0

Usar métodos de reciclagem apropriados para atualizar as visualizações resolverá esse problema

Primeiro, faça alterações na lista

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

Em seguida, notifique usando

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

Espero que isso ajude !!

Sreedhu Madhu
fonte
Absolutamente não. Se você estiver fazendo várias atualizações por segundos (digitalização BLE no meu caso), ele não funcionará de todo. Passei um dia atualizando para essa merda de RecyclerAdapter ... Melhor manter ArrayAdapter. É uma pena que não esteja usando o padrão MVC, mas pelo menos é quase utilizável.
Gojir4
0

Solução Kotlin:

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