web-dev-qa-db-de.com

onBindViewHolder () wird für die Ansicht an Position nie aufgerufen, obwohl RecyclerView.findViewHolderForAdapterPosition () an dieser Position Null zurückgibt

Ich habe eine Liste mit 13 Elementen (obwohl Elemente hinzugefügt oder entfernt werden können), Positionen 0-12. Wenn das Fragment, das die RecyclerView enthält, zuerst angezeigt wird, sind für den Benutzer nur die Positionen 0 bis 7 sichtbar (Position 7 ist nur zur Hälfte sichtbar). In meinem Adapter Log jedes Mal, wenn ein Ansichtshalter gebunden/gebunden ist (idk, wenn hier Grammatik zutrifft) und notiere seine Position.

Adapter 

@Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
    Log.d(TAG, "onBindViewHolder() position: " + position);
    ...
}

Aus meiner Log sehe ich, dass die Positionen 0-7 gebunden sind:

 Log from Adapter

Ich habe eine selectAll() Methode, die jede ViewHolder durch die Adapterposition erhält. Wenn die zurückgegebene holder NICHT null ist, verwende ich die zurückgegebene holder, um die Ansicht zu aktualisieren und die ausgewählte Ansicht anzuzeigen. Wenn der zurückgegebene Inhaber IS null aufrufe selectOnBind(), eine Methode, die die Ansicht an dieser Position markiert, wird sie so angezeigt, dass sie ausgewählt ist, wenn sie gebunden ist, und nicht in Echtzeit, da sie derzeit nicht angezeigt wird:

public void selectAll() {
    for (int i = 0; i < numberOfItemsInList; i++) {
        MyAdapter.ViewHolder holder = (MyAdapter.ViewHolder)
                mRecyclerView.findViewHolderForAdapterPosition(i);

        Log.d(TAG, "holder at position " + i + " is " + holder);

        if (holder != null) {
            select(holder);
        } else {
            selectOnBind(i);
        }
    }
}

In dieser Methode Log die holder zusammen mit ihrer Position:

 Log from selectAll()

Bis zu diesem Punkt scheint alles normal zu sein. Wir haben Positionen 0-7, die zeigen, und gemäß der Log sind dies die Positionen, die gebunden sind. Wenn ich auf selectAll() tippe, ohne die sichtbaren Ansichten zu ändern (Scrollen), sehe ich, dass die Positionen 0-7 definiert sind und 8-12 die Variable null sind. So weit, ist es gut.

Hier wird es interessant. Wenn nach dem Aufruf von selectAll() die Listenpositionen 8 und 9 weiter nach unten gescrollt werden, sind sie nicht ausgewählt. 

Bei der Überprüfung von Log stelle ich fest, dass sie niemals gebunden sind, obwohl berichtet wurde, dass sie null sind:

 Adapter Log after scroll

Noch verwirrender ist, dass dies nicht jedes Mal geschieht. Wenn ich die App zum ersten Mal starte und diese teste, funktioniert sie möglicherweise. Aber es scheint danach unbedingt zu passieren. Ich vermute, es hat etwas mit den Ansichten zu tun, die recycelt werden, aber müssten sie auch nicht gebunden sein?

EDIT (6-29-16)
Nach einem AndroidStudio-Update kann ich den Fehler nicht reproduzieren. Es funktioniert wie erwartet und bindet die Null-Ansichten. Wenn dieses Problem erneut auftritt, werde ich zu diesem Beitrag zurückkehren.

21

Dies geschieht, weil:

  • Die Ansichten werden nicht zur Recyclingübersicht hinzugefügt (getChildAt funktioniert nicht und gibt für diese Position null zurück) 
  • Sie werden auch zwischengespeichert (onBind wird nicht aufgerufen) 

Wenn Sie recyclerView.setItemViewCacheSize(0) aufrufen, wird dieses "Problem" behoben.

Da der Standardwert 2 ist (private static final int DEFAULT_CACHE_SIZE = 2; in RecyclerView.Recycler), erhalten Sie immer 2 Ansichten, in denen nicht onBind aufgerufen wird, die jedoch dem Recycler nicht hinzugefügt werden

15
Pedro Oliveira

In Ihrem Fall werden die Ansichten für die Positionen 8 und 9 nicht wiederverwendet, sie werden vom Fenster getrennt und wieder angebracht. Und für diese freistehende Ansicht wird onBindViewHolder nicht aufgerufen, sondern nur onViewAttachedToWindow. Wenn Sie diese Funktion in Ihrem Adapter überschreiben, können Sie sehen, worüber ich spreche. 

@Override
    public void onViewRecycled(ViewHolder vh){
        Log.wtf(TAG,"onViewRecycled "+vh);
    }

    @Override
    public void onViewDetachedFromWindow(ViewHolder viewHolder){
        Log.wtf(TAG,"onViewDetachedFromWindow "+viewHolder);
    }

Nun, um Ihr Problem zu lösen, müssen Sie die Ansichten verfolgen, die wiederverwendet werden sollten, aber getrennt werden und dann den Abschnittsprozess durchführen 

@Override
    public void onViewAttachedToWindow(ViewHolder viewHolder){
        Log.wtf(TAG,"onViewAttachedToWindow "+viewHolder);
    }
6
Zartha

Die Antworten von Pedro Oliveira und Zartha sind großartig, um das Problem zu verstehen, aber ich sehe keine Lösungen, mit denen ich zufrieden bin.

Ich glaube, Sie haben zwei gute Optionen, je nachdem, was Sie tun:

Option 1

Wenn Sie möchten, dass onBindViewHolder() für eine Ansicht außerhalb des Bildschirms angerufen wird, unabhängig davon, ob sie zwischengespeichert oder getrennt ist oder nicht, können Sie Folgendes tun:

RecyclerView.ViewHolder view_holder = recycler_view.findViewHolderForAdapterPosition( some_position );

if ( view_holder != null )
{
    //manipulate the attached view
}
else //view is either non-existant or detached waiting to be reattached
    notifyItemChanged( some_position );

Die Idee ist, dass, wenn die Ansicht zwischengespeichert/getrennt wird, der Adapter von notifyItemChanged() informiert wird, dass die Ansicht ungültig ist, was dazu führt, dass onBindViewHolder() aufgerufen wird.

Option 2

Wenn Sie nur eine Teiländerung durchführen möchten (und nicht alles in onBindViewHolder()), müssen Sie in onBindViewHolder( ViewHolder view_holder, int position ) die position im view_holder speichern und die gewünschte Änderung in onViewAttachedToWindow( ViewHolder view_holder ) ausführen. 

Ich empfehle Option 1 aus Gründen der Vereinfachung, es sei denn, Ihre onBindViewHolder() führt etwas intensiveres durch, z. B. das Durcheinander mit Bitmaps.

1
Kacy

Ich denke, Ihre Frage wird unten von @Pedro Oliveira beantwortet. Das Hauptgefühl von RecycleView besteht darin, dass er jederzeit spezielle Algorithmen zum Zwischenspeichern von ViewHolder verwendet. Als nächstes funktioniert onBindViewHolder (...) möglicherweise nicht. wenn die Ansicht statisch ist oder etwas anderes.

Und zu Ihrer Frage sollten Sie RecycleView für dynamisch geänderte Ansichten verwenden. TUN SIE NICHT! Da RecycleView die Ansichten ungültig macht und über ein Caching-System verfügt, werden Sie viele Probleme haben. 

Use LinkedListView für diese Aufgabe! 

0
GensaGames

Ich denke, mit Sicht zu spielen ist im Recycling-Bereich keine gute Idee. Der Ansatz, den ich immer verfolge, um einfach mit RecyclerView ein Flag in das Modell einzuführen. Nehmen wir an, Ihr Modell ist wie - 

class MyModel{
    String name;
    int age;
}

Wenn Sie die Ansicht verfolgen, ist die Ansicht ausgewählt oder nicht, und fügen Sie dem Modell einen Booleschen Wert hinzu. Jetzt wird es aussehen - 

class MyModel{
    String name;
    int age;
    boolean isSelected;
}

Jetzt wird Ihr Kontrollkästchen auf der Grundlage des neuen Flags isSelected (in onBindViewHolder ()) aktiviert/deaktiviert. Bei jeder Auswahl in der Ansicht wird der Wert des entsprechenden Modellwerts in true und in nicht ausgewähltem Wert in false geändert. In Ihrem Fall führen Sie einfach eine Schleife aus, um den isSelected-Wert aller Modelle in true zu ändern, und rufen Sie dann notifyDataSetChanged() auf.

Nehmen wir beispielsweise an, Ihre Liste ist 

ArrayList<MyModel> recyclerList;
private void selectAll(){
    for(MyModel myModel:recyclerList)
        myModel.isSelected = true;
    notifyDataSetChanged();
}

Mein Vorschlag, bei der Verwendung von RecyclerView oder ListView weniger zu versuchen, mit Ansichten zu spielen. 

Also in deinem Fall - 

@Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
   holder.clickableView.setTag(position);
   holder.selectableView.setTag(position);
   holder.checkedView.setChecked(recyclerList.get(position).isSelected);
    Log.d(TAG, "onBindViewHolder() position: " + position);
    ...
}

@Override
public void onClick(View view){
    int position = (int)view.getTag();
    recyclerList.get(position).isSelected = !recyclerList.get(position).isSelected;
}

@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
      int position = (int)buttonView.getTag();
      recyclerList.get(position).isSelected = isChecked;
}

Hoffe, es wird dir helfen, bitte lass es mich wissen, wenn du weitere Erklärungen brauchst :)

0
Neo

Wenn Sie eine große Anzahl von Elementen in der Liste haben, die Sie an recyclerview adapter übergeben haben, tritt das Problem nicht auf, dass onBindViewHolder() beim Scrollen nicht ausgeführt wird.

Wenn die Liste jedoch weniger Elemente enthält (ich habe die Listengröße 5 geprüft), kann dieses Problem auftreten.

Eine bessere Lösung ist, list size zu überprüfen.

Beispielcode finden Sie unten.

private void setupAdapter(){
    if (list.size() <= 10){
        recycler.setItemViewCacheSize(0);
     }
     recycler.setAdapter(adapter);
     recycler.setLayoutManager(linearLayoutManager);
}
0