web-dev-qa-db-fra.com

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

J'ai un ListView qui affiche les nouvelles. Ils contiennent une image, un titre et du texte. L'image est chargée dans un thread séparé (avec une file d'attente et tout) et lorsque l'image est téléchargée, j'appelle maintenant notifyDataSetChanged() sur l'adaptateur de liste pour mettre à jour l'image. Cela fonctionne, mais getView() est appelé trop souvent, car notifyDataSetChanged() appelle getView() pour tous les éléments visibles. Je souhaite mettre à jour uniquement le seul élément de la liste. Comment je ferais ça?

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

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

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 problème, 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 ceci 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!");
}
194
Erik

Cette question a été posée sur le Google I/O 2010, vous pouvez la voir ici:

Le monde de ListView, heure 52:

En gros, Romain Guy explique qu'il appelle getChildAt(int) sur le ListView pour obtenir la vue et (je pense) appeler getFirstVisiblePosition() pour connaître la corrélation entre position et index.

Romain pointe également le projet intitulé Shelves à titre d'exemple, je pense qu'il pourrait vouloir dire la méthode ShelvesActivity.updateBookCovers() , mais je ne trouve pas l'appel de getFirstVisiblePosition().

AWESOME UPDATES À VENIR:

Le RecyclerView va résoudre 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 est génial! :-)

70
mreichelt

Voici comment je l'ai fait:

Vos éléments (lignes) doivent avoir des identifiants uniques pour 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 une étiquette si l'étiquette 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 présente, elle est visible et 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, pas des positions! De plus, vous devez appeler update pour les éléments visibles.

6
Ali

obtenir la classe de modèle d'abord en tant que global, comme cet objet de classe de modèle

SampleModel golbalmodel=new SchedulerModel();

et l'initialiser à global

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

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

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

golbalmodel.setStartandenddate(changedate);

notifyDataSetChanged();

Voici une question connexe avec de bonnes réponses.

3
koteswara D K
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.

2
Libin Thomas

Les réponses sont claires et correctes, je vais ajouter une idée pour le cas CursorAdapter ici.

Si vous sous-classez CursorAdapter (ou ResourceCursorAdapter ou SimpleCursorAdapter), vous pouvez alors implémenter ViewBinder ou remplacer bindView() et newView() méthodes, celles-ci ne reçoivent pas l'index des éléments de la liste actuelle sous forme d'arguments. Par conséquent, lorsque vous connaissez certaines données et que vous souhaitez mettre à jour les éléments de liste visibles pertinents, comment connaissez-vous leurs index?

Ma solution de contournement était de:

  • conserve une liste de toutes les vues d’éléments de liste créées, ajoute des éléments à cette liste à partir de newView()
  • quand les données arrivent, itérez-les et voyez lequel vous avez besoin de mettre à jour - mieux que de faire notifyDatasetChanged() et de les rafraîchir toutes

En raison du recyclage de la vue, le nombre de références de vue que j’aurai besoin de stocker et d’itérer sera à peu près égal au nombre d’éléments de la liste visibles à l’écran.

2
Pēteris Caune

J'ai utilisé le code qui a fourni Erik, fonctionne très bien, mais j'ai un adaptateur personnalisé complexe pour mon listview et j'ai été confronté à deux implémentations du code qui met à jour l'interface utilisateur. J'ai essayé d'obtenir la nouvelle vue à l'aide de la méthode getView de mon adaptateur (la liste de contrôle contenant les données de la liste 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.

1
Primoz990

exactement j'ai utilisé cela

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

J'ai créé une autre solution, telle que RecyclyerView méthode void notifyItemChanged(int position), create CustomBaseAdapter classe 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 une variable classe CustomDataSetObservable aussi pour mDataSetObservable 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();
        }
    }
}

on class 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 de souris ou de l'endroit où vous souhaitez appeler cette méthode). Et voila !, votre rangée unique se mettra à jour instantanément ..

1
Aprido Sandyasa

Ma solution: si elle est correcte *, mettez à jour les données et les éléments visibles sans re-dessiner toute la liste. Sinon notifyDataSetChanged.

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 sur:

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

Ignore le dernier paramètre de bindView (je l'utilise pour déterminer si je dois ou non recycler des images bitmap pour ImageDrawable).

Comme mentionné ci-dessus, le nombre total de vues "visibles" correspond à peu près à la quantité qui convient à l'écran (en ignorant les changements d'orientation, etc.), donc pas de mémoire.

0
JRun