¿Cómo puedo actualizar una sola fila en un ListView?

131

Tengo una ListViewque muestra noticias. Contienen una imagen, un título y algo de texto. La imagen se carga en un hilo separado (con una cola y todo) y cuando la imagen se descarga, ahora llamo notifyDataSetChanged()al adaptador de la lista para actualizar la imagen. Esto funciona, pero getView()se llama con demasiada frecuencia, ya que notifyDataSetChanged()requiere getView()todos los elementos visibles. Quiero actualizar solo el elemento individual de la lista. ¿Cómo haría esto?

Los problemas que tengo con mi enfoque actual son:

  1. El desplazamiento es lento
  2. Tengo una animación de aparición gradual en la imagen que ocurre cada vez que se carga una nueva imagen en la lista.
Erik
fuente

Respuestas:

199

Encontré la respuesta, gracias a tu información Michelle. De hecho, puede obtener la vista correcta utilizando View#getChildAt(int index). El problema es que comienza a contar desde el primer elemento visible. De hecho, solo puede obtener los elementos visibles. Resuelves esto con ListView#getFirstVisiblePosition().

Ejemplo:

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
fuente
2
Nota personal: mImgView.setImageURI () actualizará el elemento de la lista, pero solo una vez; así que estoy mejor usando mLstVwAdapter.notifyDataSetInvalidated () en su lugar.
Kellogs
Esta solución funciona Pero esto solo se puede hacer con yourListView.getChildAt (index); y cambia la vista. Ambas soluciones funcionan !!
AndroidDev
¿Qué pasaría si solo llamo a getView () en el adaptador solo si la posición es entre getFirstVisiblePosition () y getLastVisiblePosition ()? ¿funcionaría igual? Creo que actualizará la vista como de costumbre, ¿verdad?
desarrollador de Android
En mi caso, algunas veces la vista es nula, ya que cada elemento se actualiza de forma asincrónica, ¡mejor comprobar si ver! = Nulo
Khawar
2
Funciona genial. Me ayudó a terminar la tarea de implementar la función Me gusta en una aplicación similar a instagram. Estaba usando un adaptador personalizado, una transmisión en el botón Me gusta dentro de un elemento de la lista, lanzando un asinctask para decirle al backend sobre lo similar con un receptor de transmisión en el fragmento padre, y finalmente la respuesta de Erik para actualizar la lista.
Josh el
71

Esta pregunta se hizo en Google I / O 2010, puede verla aquí:

El mundo de ListView, hora 52:30

Básicamente lo que explica Romain individuo es llamar getChildAt(int)en el ListViewpara obtener la vista y llamada (creo) getFirstVisiblePosition()para averiguar la correlación entre la posición y el índice.

Romain también señala el proyecto llamado Shelves como un ejemplo, creo que podría referirse al método ShelvesActivity.updateBookCovers(), pero no puedo encontrar la llamada getFirstVisiblePosition().

ACTUALIZACIONES IMPRESIONANTES VENIDAS:

RecyclerView solucionará esto en un futuro próximo. Como se señaló en http://www.grokkingandroid.com/first-glance-androids-recyclerview/ , podrá llamar a métodos para especificar exactamente el cambio, como:

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

Además, todos querrán usar las nuevas vistas basadas en RecyclerView porque serán recompensados ​​con animaciones de aspecto agradable. ¡El futuro se ve increíble! :-)

mreichelt
fuente
3
¡Buena cosa! :) Quizás también pueda agregar un cheque nulo aquí - v puede ser nulo si la vista no está disponible en este momento. Y, por supuesto, estos datos desaparecerán si el usuario desplaza ListView, por lo que también se deben actualizar los datos en el adaptador (tal vez sin llamar a notifyDataSetChanged ()). En general, creo que sería una buena idea mantener toda esta lógica dentro del adaptador, es decir, pasarle la referencia ListView.
mreichelt
Esto funcionó para mí, excepto que cuando la vista salió de la pantalla, cuando vuelve a entrar, el cambio se restablece. Supongo que porque en el getview todavía está recibiendo la información original. ¿Cómo puedo evitar esto?
gloscherrybomb
6

Así es como lo hice:

Sus elementos (filas) deben tener identificadores únicos para que pueda actualizarlos más tarde. Establezca la etiqueta de cada vista cuando la lista obtenga la vista del adaptador. (También puede usar la etiqueta clave si la etiqueta predeterminada se usa en otro lugar)

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

Para la actualización, verifique cada elemento de la lista, si hay una vista con una ID dada, es visible, por lo que realizamos la actualización.

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

¡En realidad es más fácil que otros métodos y mejor cuando se trata de identificadores, no de posiciones! También debe llamar a la actualización para los elementos que se hacen visibles.

Ali
fuente
3

obtener la clase de modelo primero como global como este objeto de clase de modelo

SampleModel golbalmodel=new SchedulerModel();

e inicializarlo a global

obtener la fila actual de la vista por el modelo inicializándola en el modelo global

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

establezca el valor cambiado en el método de objeto de modelo global que se establecerá y agregue el método notifyDataSetChanged para mí.

golbalmodel.setStartandenddate(changedate);

notifyDataSetChanged();

Aquí hay una pregunta relacionada sobre esto con buenas respuestas.

koteswara DK
fuente
Este es el enfoque correcto, ya que está obteniendo el objeto que desea actualizar, actualícelo y luego notifique al adaptador, y eso cambiará la información de la fila con los nuevos datos.
Gastón Saillén
2

Las respuestas son claras y correctas, agregaré una idea para el CursorAdaptercaso aquí.

Si está subclasificando CursorAdapter(o ResourceCursorAdapter, o SimpleCursorAdapter), puede implementar ViewBindero anular bindView()y newView()métodos, estos no reciben el índice del elemento de la lista actual en los argumentos. Por lo tanto, cuando llegan algunos datos y desea actualizar los elementos de la lista visible relevante, ¿cómo sabe sus índices?

Mi solución era:

  • mantenga una lista de todas las vistas de elementos de lista creados, agregue elementos a esta lista desde newView()
  • cuando lleguen los datos, repítalos y vea cuál necesita actualizarse, mejor que hacerlo notifyDatasetChanged()y actualizarlos todos

Debido al reciclaje de vistas, la cantidad de referencias de vista que necesitaré almacenar e iterar será aproximadamente igual a la cantidad de elementos de la lista visibles en la pantalla.

Pēteris Caune
fuente
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);

Para RecycleView, use este código

Libin Thomas
fuente
Implementé esto en recycleView, funciona. Pero cuando desplaza la lista, vuelve a cambiar. ¿alguna ayuda?
Fahid Nadeem
1

Utilicé el código que proporcionó a Erik, funciona muy bien, pero tengo un adaptador personalizado complejo para mi vista de lista y me enfrenté a dos veces la implementación del código que actualiza la interfaz de usuario. Intenté obtener la nueva vista del método getView de mis adaptadores (la lista de arrays que contiene los datos de la vista de lista ya se ha actualizado / cambiado):

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

Está funcionando bien, pero no sé si es una buena práctica. Por lo tanto, no necesito implementar el código que actualiza el elemento de la lista dos veces.

Primoz990
fuente
1

exactamente usé esto

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);
    }
chefish
fuente
Estoy obteniendo Ver como diseño relativo ¿cómo encontrar Textview dentro del diseño relativo?
Girish
1

Creé otra solución, como el método RecyclyerView void notifyItemChanged(int position), creo la clase CustomBaseAdapter así:

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

    }
}

No olvides crear una clase CustomDataSetObservable también para la mDataSetObservablevariable en CustomAdapterClass , como esta:

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

en la clase CustomBaseAdapter hay un método notifyItemChanged(int position), y puede llamar a ese método cuando desee actualizar una fila donde lo desee (desde el clic del botón o desde cualquier lugar donde desee llamar a ese método). Y listo, tu fila individual se actualizará instantáneamente.

Aprido Sandyasa
fuente
0

Mi solución: si es correcto *, actualice los datos y los elementos visibles sin volver a dibujar la lista completa. De lo contrario, notifiqueDataSetChanged.

Correcto: tamaño de datos antiguos == tamaño de datos nuevo e ID de datos antiguos y su orden == ID de datos nuevos y orden

Cómo:

/**
 * 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;
}

y por supuesto:

@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 el último parámetro para bindView (lo uso para determinar si necesito o no reciclar mapas de bits para ImageDrawable).

Como se mencionó anteriormente, el número total de vistas "visibles" es aproximadamente la cantidad que cabe en la pantalla (ignorando los cambios de orientación, etc.), por lo que no es una gran memoria.

JRun
fuente
0

Además de esta solución ( https://stackoverflow.com/a/3727813/5218712 ) solo quiero agregar que debería funcionar solo si listView.getChildCount() == yourDataList.size(); podría haber una vista adicional dentro de ListView.

Ejemplo de cómo se completan los elementos secundarios:  matriz ListView.mChildren

Svyatoslav Ruzhitsky
fuente