Comment puis-je mettre à jour une seule ligne dans un ListView?

131

J'ai un ListViewqui affiche des actualités. Ils contiennent une image, un titre et du texte. L'image est chargée dans un fil séparé (avec une file d'attente et tout) et lorsque l'image est téléchargée, j'appelle maintenant notifyDataSetChanged()l'adaptateur de liste pour mettre à jour l'image. Cela fonctionne, mais getView()est appelé trop fréquemment, car il notifyDataSetChanged()appelle getView()tous les éléments visibles. Je souhaite mettre à jour uniquement l'élément unique de la liste. Comment ferais-je cela?

Les problèmes que j'ai avec mon approche actuelle sont:

  1. Le défilement est lent
  2. J'ai une animation de fondu sur l'image qui se produit chaque fois qu'une seule nouvelle image dans la liste est chargée.
Erik
la source

Réponses:

199

J'ai trouvé la réponse, grâce à vos informations Michelle. Vous pouvez en effet obtenir la bonne vue en utilisant View#getChildAt(int index). Le hic, c'est qu'il commence à compter à partir du premier élément visible. En fait, vous ne pouvez obtenir que les éléments visibles. Vous résolvez cela avec ListView#getFirstVisiblePosition().

Exemple:

private void updateView(int index){
    View v = yourListView.getChildAt(index - 
        yourListView.getFirstVisiblePosition());

    if(v == null)
       return;

    TextView someText = (TextView) v.findViewById(R.id.sometextview);
    someText.setText("Hi! I updated you manually!");
}
Erik
la source
2
note à moi-même: mImgView.setImageURI () mettra à jour l'élément de liste mais une seule fois; je suis donc mieux d'utiliser mLstVwAdapter.notifyDataSetInvalidated () à la place.
kellogs
Cette solution fonctionne. Mais cela ne peut être fait qu'avec yourListView.getChildAt (index); et changez la vue. Les deux solutions fonctionnent !!
AndroidDev
que se passerait-il si j'appelais simplement getView () sur l'adaptateur uniquement si la position est entre getFirstVisiblePosition () et getLastVisiblePosition ()? fonctionnerait-il de la même manière? Je pense que cela mettra à jour la vue comme d'habitude, non?
développeur android
Dans mon cas, la vue est parfois nulle, car chaque élément est mis à jour de manière asynchrone, mieux vaut vérifier si view! = Null
Khawar
2
Fonctionne très bien. Cela m'aide à terminer la tâche d'implémentation de la fonction J'aime dans une application de type instagram. J'utilisais un adaptateur personnalisé, une diffusion dans le bouton similaire à l'intérieur d'un élément de liste, lançant une asynctask pour informer le backend de quelque chose de similaire avec un récepteur de diffusion sur le fragment parent, et enfin la réponse d'Erik pour mettre à jour la liste.
Josh
71

Cette question a été posée lors du Google I / O 2010, vous pouvez la regarder ici:

Le monde de ListView, heure 52:30

En gros, ce que Romain Guy explique, c'est de faire appel getChildAt(int)au ListViewpour avoir la vue et (je pense) appeler getFirstVisiblePosition()pour connaître la corrélation entre la position et l'indice.

Romain cite également le projet nommé Shelves comme exemple, je pense qu'il peut vouloir dire la méthode ShelvesActivity.updateBookCovers(), mais je ne trouve pas l'appel getFirstVisiblePosition().

MISES À JOUR IMPRESSIONNANTES À VENIR:

Le RecyclerView résoudra ce problème dans un proche avenir. Comme indiqué sur http://www.grokkingandroid.com/first-glance-androids-recyclerview/ , vous pourrez appeler des méthodes pour spécifier exactement le changement, telles que:

void notifyItemInserted(int position)
void notifyItemRemoved(int position)
void notifyItemChanged(int position)

De plus, tout le monde voudra utiliser les nouvelles vues basées sur RecyclerView car ils seront récompensés par de jolies animations! L'avenir semble génial! :-)

Michelt
la source
3
Bonne chose! :) Vous pouvez peut-être ajouter une vérification nulle ici aussi - v peut être nul si la vue n'est pas disponible pour le moment. Et bien sûr, ces données disparaîtront si l'utilisateur fait défiler la ListView, il faut donc également mettre à jour les données de l'adaptateur (peut-être sans appeler notifyDataSetChanged ()). En général, je pense que ce serait une bonne idée de conserver toute cette logique dans l'adaptateur, c'est-à-dire en lui passant la référence ListView.
mreichelt
Cela a fonctionné pour moi, sauf - lorsque la vue a quitté l'écran, quand elle entre à nouveau, la modification est réinitialisée. Je suppose que dans le getview, il reçoit toujours les informations d'origine. Comment puis-je contourner cela?
gloscherrybomb
6

Voici comment je l'ai fait:

Vos éléments (lignes) doivent avoir des identifiants uniques afin que vous puissiez les mettre à jour ultérieurement. Définissez la balise de chaque vue lorsque la liste obtient la vue de l'adaptateur. (Vous pouvez également utiliser la balise clé si la balise par défaut est utilisée ailleurs)

@Override
public View getView(int position, View convertView, ViewGroup parent)
{
    View view = super.getView(position, convertView, parent);
    view.setTag(getItemId(position));
    return view;
}

Pour la mise à jour, vérifiez chaque élément de la liste, si une vue avec un identifiant donné est là, elle est visible, nous effectuons la mise à jour.

private void update(long id)
{

    int c = list.getChildCount();
    for (int i = 0; i < c; i++)
    {
        View view = list.getChildAt(i);
        if ((Long)view.getTag() == id)
        {
            // update view
        }
    }
}

C'est en fait plus facile que d'autres méthodes et mieux lorsque vous traitez avec des identifiants et non des positions! Vous devez également appeler update pour les éléments qui deviennent visibles.

Ali
la source
3

obtenir la classe de modèle d'abord comme globale comme cet objet de classe de modèle

SampleModel golbalmodel=new SchedulerModel();

et initialisez-le à global

obtenir la ligne courante de la vue par le modèle en l'initialisant au modèle global

SampleModel data = (SchedulerModel) sampleList.get(position);
                golbalmodel=data;

définissez la valeur modifiée sur la méthode d'objet de modèle global à définir et ajoutez le notifyDataSetChanged qui fonctionne pour moi

golbalmodel.setStartandenddate(changedate);

notifyDataSetChanged();

Voici une question connexe à ce sujet avec de bonnes réponses.

koteswara DK
la source
Il s'agit de la bonne approche, car vous obtenez l'objet que vous souhaitez mettre à jour, mettez-le à jour, puis notifiez l'adaptateur, ce qui modifiera les informations de ligne avec les nouvelles données.
Gastón Saillén
2

Les réponses sont claires et correctes, je vais ajouter une idée de CursorAdaptercas ici.

Si vous êtes en sous-classement CursorAdapter(ou ResourceCursorAdapter, ou SimpleCursorAdapter), alors vous pouvez implémenter ViewBinderou remplacer bindView()et des newView()méthodes, celles-ci ne reçoivent pas l'index d'élément de liste actuel dans les arguments. Par conséquent, lorsque certaines données arrivent et que vous souhaitez mettre à jour des éléments de liste visibles pertinents, comment connaître leurs indices?

Ma solution était de:

  • conserver une liste de toutes les vues d'élément de liste créées, ajouter des éléments à cette liste à partir de newView()
  • lorsque les données arrivent, itérez-les et voyez laquelle doit être mise à jour - mieux que de les faire notifyDatasetChanged()et de les actualiser toutes

En raison du recyclage des vues, le nombre de références de vues que je devrai stocker et itérer sera à peu près égal au nombre d'éléments de liste visibles à l'écran.

Pēteris Caune
la source
2
int wantedPosition = 25; // Whatever position you're looking for
int firstPosition = linearLayoutManager.findFirstVisibleItemPosition(); // This is the same as child #0
int wantedChild = wantedPosition - firstPosition;

if (wantedChild < 0 || wantedChild >= linearLayoutManager.getChildCount()) {
    Log.w(TAG, "Unable to get view for desired position, because it's not being displayed on screen.");
    return;
}

View wantedView = linearLayoutManager.getChildAt(wantedChild);
mlayoutOver =(LinearLayout)wantedView.findViewById(R.id.layout_over);
mlayoutPopup = (LinearLayout)wantedView.findViewById(R.id.layout_popup);

mlayoutOver.setVisibility(View.INVISIBLE);
mlayoutPopup.setVisibility(View.VISIBLE);

Pour RecycleView, veuillez utiliser ce code

Libin Thomas
la source
J'ai mis en œuvre cela sur recycleView, cela fonctionne. Mais lorsque vous faites défiler la liste, cela change à nouveau. de l'aide?
Fahid Nadeem
1

J'ai utilisé le code qui a fourni Erik, fonctionne très bien, mais j'ai un adaptateur personnalisé complexe pour ma liste et j'ai été confronté à deux fois la mise en œuvre du code qui met à jour l'interface utilisateur. J'ai essayé d'obtenir la nouvelle vue de la méthode getView de mes adaptateurs (l'arraylist qui contient les données de listview a déjà été mise à jour / modifiée):

View cell = lvOptim.getChildAt(index - lvOptim.getFirstVisiblePosition());
if(cell!=null){
    cell = adapter.getView(index, cell, lvOptim); //public View getView(final int position, View convertView, ViewGroup parent)
    cell.startAnimation(animationLeftIn());
}

Cela fonctionne bien, mais je ne sais pas si c'est une bonne pratique. Je n'ai donc pas besoin d'implémenter le code qui met à jour l'élément de liste deux fois.

Primoz990
la source
1

exactement je l'ai utilisé

private void updateSetTopState(int index) {
        View v = listview.getChildAt(index -
                listview.getFirstVisiblePosition()+listview.getHeaderViewsCount());

        if(v == null)
            return;

        TextView aa = (TextView) v.findViewById(R.id.aa);
        aa.setVisibility(View.VISIBLE);
    }
chef
la source
J'obtiens View as Relative layout comment trouver Textview dans la disposition relative?
Girish
1

J'ai inventé une autre solution, comme la méthode RecyclyerView void notifyItemChanged(int position), créez la classe CustomBaseAdapter comme ceci:

public abstract class CustomBaseAdapter implements ListAdapter, SpinnerAdapter {
    private final CustomDataSetObservable mDataSetObservable = new CustomDataSetObservable();

    public boolean hasStableIds() {
        return false;
    }

    public void registerDataSetObserver(DataSetObserver observer) {
        mDataSetObservable.registerObserver(observer);
    }

    public void unregisterDataSetObserver(DataSetObserver observer) {
        mDataSetObservable.unregisterObserver(observer);
    }

    public void notifyDataSetChanged() {
        mDataSetObservable.notifyChanged();
    }

    public void notifyItemChanged(int position) {
        mDataSetObservable.notifyItemChanged(position);
    }

    public void notifyDataSetInvalidated() {
        mDataSetObservable.notifyInvalidated();
    }

    public boolean areAllItemsEnabled() {
        return true;
    }

    public boolean isEnabled(int position) {
        return true;
    }

    public View getDropDownView(int position, View convertView, ViewGroup parent) {
        return getView(position, convertView, parent);
    }

    public int getItemViewType(int position) {
        return 0;
    }

    public int getViewTypeCount() {
        return 1;
    }

    public boolean isEmpty() {
        return getCount() == 0;
    } {

    }
}

N'oubliez pas de créer également une classe CustomDataSetObservable pour la mDataSetObservablevariable dans CustomAdapterClass , comme ceci:

public class CustomDataSetObservable extends Observable<DataSetObserver> {

    public void notifyChanged() {
        synchronized(mObservers) {
            // since onChanged() is implemented by the app, it could do anything, including
            // removing itself from {@link mObservers} - and that could cause problems if
            // an iterator is used on the ArrayList {@link mObservers}.
            // to avoid such problems, just march thru the list in the reverse order.
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onChanged();
            }
        }
    }

    public void notifyInvalidated() {
        synchronized (mObservers) {
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onInvalidated();
            }
        }
    }

    public void notifyItemChanged(int position) {
        synchronized(mObservers) {
            // since onChanged() is implemented by the app, it could do anything, including
            // removing itself from {@link mObservers} - and that could cause problems if
            // an iterator is used on the ArrayList {@link mObservers}.
            // to avoid such problems, just march thru the list in the reverse order.
            mObservers.get(position).onChanged();
        }
    }
}

sur la classe CustomBaseAdapter, il existe une méthode notifyItemChanged(int position), et vous pouvez appeler cette méthode lorsque vous souhaitez mettre à jour une ligne où vous le souhaitez (à partir d'un clic sur le bouton ou à n'importe quel endroit où vous voulez appeler cette méthode). Et voila !, votre seule ligne sera mise à jour instantanément.

Aprido Sandyasa
la source
0

Ma solution: si elle est correcte *, mettez à jour les données et les éléments visibles sans redessiner la liste entière. Sinon notifierDataSetChanged.

Correct - oldData size == nouvelle taille de données, et anciens ID de données et leur ordre == nouveaux ID de données et ordre

Comment:

/**
 * A View can only be used (visible) once. This class creates a map from int (position) to view, where the mapping
 * is one-to-one and on.
 * 
 */
    private static class UniqueValueSparseArray extends SparseArray<View> {
    private final HashMap<View,Integer> m_valueToKey = new HashMap<View,Integer>();

    @Override
    public void put(int key, View value) {
        final Integer previousKey = m_valueToKey.put(value,key);
        if(null != previousKey) {
            remove(previousKey);//re-mapping
        }
        super.put(key, value);
    }
}

@Override
public void setData(final List<? extends DBObject> data) {
    // TODO Implement 'smarter' logic, for replacing just part of the data?
    if (data == m_data) return;
    List<? extends DBObject> oldData = m_data;
    m_data = null == data ? Collections.EMPTY_LIST : data;
    if (!updateExistingViews(oldData, data)) notifyDataSetChanged();
    else if (DEBUG) Log.d(TAG, "Updated without notifyDataSetChanged");
}


/**
 * See if we can update the data within existing layout, without re-drawing the list.
 * @param oldData
 * @param newData
 * @return
 */
private boolean updateExistingViews(List<? extends DBObject> oldData, List<? extends DBObject> newData) {
    /**
     * Iterate over new data, compare to old. If IDs out of sync, stop and return false. Else - update visible
     * items.
     */
    final int oldDataSize = oldData.size();
    if (oldDataSize != newData.size()) return false;
    DBObject newObj;

    int nVisibleViews = m_visibleViews.size();
    if(nVisibleViews == 0) return false;

    for (int position = 0; nVisibleViews > 0 && position < oldDataSize; position++) {
        newObj = newData.get(position);
        if (oldData.get(position).getId() != newObj.getId()) return false;
        // iterate over visible objects and see if this ID is there.
        final View view = m_visibleViews.get(position);
        if (null != view) {
            // this position has a visible view, let's update it!
            bindView(position, view, false);
            nVisibleViews--;
        }
    }

    return true;
}

et bien sûr:

@Override
public View getView(final int position, final View convertView, final ViewGroup parent) {
    final View result = createViewFromResource(position, convertView, parent);
    m_visibleViews.put(position, result);

    return result;
}

Ignorez le dernier paramètre de bindView (je l'utilise pour déterminer si je dois recycler les bitmaps pour ImageDrawable).

Comme mentionné ci-dessus, le nombre total de vues `` visibles '' est à peu près la quantité qui tient sur l'écran (en ignorant les changements d'orientation, etc.), donc pas de gros problèmes de mémoire.

JRun
la source
0

En plus de cette solution ( https://stackoverflow.com/a/3727813/5218712 ) je veux juste ajouter qu'elle ne devrait fonctionner que s'il listView.getChildCount() == yourDataList.size(); pourrait y avoir une vue supplémentaire dans ListView.

Exemple de remplissage des éléments enfants:  Tableau listView.mChildren

Svyatoslav Ruzhitsky
la source