Jak mogę zaktualizować pojedynczy wiersz w ListView?

134

Mam, ListViewktóry wyświetla wiadomości. Zawierają obraz, tytuł i tekst. Obraz jest ładowany w osobnym wątku (z kolejką i wszystkimi innymi), a po pobraniu obrazu wywołuję teraz notifyDataSetChanged()adapter listy, aby zaktualizować obraz. To działa, ale getView()jest wywoływane zbyt często, ponieważ notifyDataSetChanged()wymaga getView()wszystkich widocznych elementów. Chcę zaktualizować tylko jedną pozycję na liście. Jak bym to zrobił?

Problemy, które mam z moim obecnym podejściem, to:

  1. Przewijanie jest powolne
  2. Mam animację zanikania obrazu, która ma miejsce za każdym razem, gdy ładowany jest pojedynczy nowy obraz z listy.
Erik
źródło

Odpowiedzi:

200

Znalazłem odpowiedź dzięki twojej informacji Michelle. Rzeczywiście możesz uzyskać właściwy widok za pomocą View#getChildAt(int index). Problem polega na tym, że zaczyna liczyć od pierwszego widocznego elementu. W rzeczywistości możesz uzyskać tylko widoczne elementy. Rozwiązujesz to za pomocą ListView#getFirstVisiblePosition().

Przykład:

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
źródło
2
uwaga dla siebie: mImgView.setImageURI () zaktualizuje element listy, ale tylko raz; więc lepiej jest używać zamiast tego mLstVwAdapter.notifyDataSetInvalidated ().
kellogs
To rozwiązanie działa. Ale można to zrobić tylko za pomocą yourListView.getChildAt (index); i zmień widok. Oba rozwiązania działają !!
AndroidDev,
co by się stało, jeśli po prostu wywołam getView () na karcie tylko wtedy, gdy pozycja znajduje się między getFirstVisiblePosition () i getLastVisiblePosition ()? czy to działałoby tak samo? Myślę, że zaktualizuje widok jak zwykle, prawda?
programista Androida
W moim przypadku czasami widok jest zerowy, ponieważ każdy element jest aktualizowany asynchronicznie, lepiej sprawdzić, czy widok! = Null
Khawar
2
Działa świetnie. Pomogło mi to zakończyć zadanie zaimplementowania funkcji Like w aplikacji podobnej do Instagrama. Używałem niestandardowego adaptera, rozgłaszania w podobnym przycisku wewnątrz elementu listy, uruchamiając asynchroniczne zadanie, aby powiedzieć backendowi o tym podobnym z odbiornikiem rozgłoszeniowym na fragmencie nadrzędnym, i na koniec odpowiedź Erika, aby zaktualizować listę.
Josh
72

To pytanie zostało zadane na Google I / O 2010, możesz je obejrzeć tutaj:

Świat ListView, godzina 52:30

Zasadniczo co Romain Guy wyjaśnia to zadzwonić getChildAt(int)na ListView, aby uzyskać widok i (chyba) wezwanie getFirstVisiblePosition(), aby dowiedzieć się korelację pomiędzy pozycją i indeksu.

Romain jako przykład wskazuje również projekt o nazwie Półki , myślę, że może mieć na myśli metodę ShelvesActivity.updateBookCovers(), ale nie mogę znaleźć wezwania getFirstVisiblePosition().

NADCHODZĄCE AKTUALIZACJE:

RecyclerView naprawi ten problem w najbliższej przyszłości. Jak wskazano na http://www.grokkingandroid.com/first-glance-androids-recyclerview/ , będziesz mógł wywołać metody, aby dokładnie określić zmianę, na przykład:

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

Ponadto każdy będzie chciał skorzystać z nowych widoków opartych na RecyclerView, ponieważ zostaną nagrodzeni ładnie wyglądającymi animacjami! Przyszłość wygląda niesamowicie! :-)

mreichelt
źródło
3
Dobra rzecz! :) Może możesz dodać tutaj również zerową kontrolę - v może być zerowa, jeśli widok nie jest w tej chwili dostępny. I oczywiście te dane znikną, jeśli użytkownik przewinie ListView, więc należy również zaktualizować dane w adapterze (być może bez wywoływania notifyDataSetChanged ()). Ogólnie myślę, że dobrym pomysłem byłoby zachowanie całej tej logiki w adapterze, tj. Przekazanie do niego referencji ListView.
mreichelt
U mnie to zadziałało, z wyjątkiem - gdy widok opuścił ekran, po ponownym wejściu zmiana jest resetowana. Chyba dlatego, że w getview nadal otrzymuje oryginalne informacje. Jak mam to obejść?
gloscherrybomb
6

Oto jak to zrobiłem:

Twoje pozycje (wiersze) muszą mieć unikalne identyfikatory, aby można było je później zaktualizować. Ustaw znacznik każdego widoku, gdy lista pobiera widok z adaptera. (Możesz również użyć tagu klucza, jeśli tag domyślny jest używany w innym miejscu)

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

Do aktualizacji sprawdź każdy element listy, czy widok o podanym id jest widoczny, więc wykonujemy aktualizację.

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

W rzeczywistości jest to łatwiejsze niż inne metody i lepsze, gdy masz do czynienia z identyfikatorami, a nie pozycjami! Musisz także zadzwonić do aktualizacji dla przedmiotów, które stają się widoczne.

Ali
źródło
3

pobierz najpierw klasę modelu jako globalną, jak ten obiekt klasy modelu

SampleModel golbalmodel=new SchedulerModel();

i zainicjuj go na global

pobierz bieżący wiersz widoku przez model, inicjując go w modelu globalnym

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

ustaw zmienioną wartość na metodę obiektu modelu globalnego, która ma zostać ustawiona, i dodaj informację notifyDataSetChanged dla mnie

golbalmodel.setStartandenddate(changedate);

notifyDataSetChanged();

Oto powiązane pytanie z dobrymi odpowiedziami.

koteswara DK
źródło
Jest to właściwe podejście, ponieważ otrzymujesz obiekt, który chcesz zaktualizować, aktualizujesz go, a następnie powiadamiasz adapter, a to zmieni informacje w wierszu o nowe dane.
Gastón Saillén
2

Odpowiedzi są jasne i poprawne, dodam CursorAdaptertutaj pomysł na przypadek.

Jeśli korzystasz z podklasy CursorAdapter(lub ResourceCursorAdapter, lub SimpleCursorAdapter), możesz zaimplementować ViewBinderlub przesłonić bindView()i newView()metody, które nie otrzymują bieżącego indeksu elementu listy w argumentach. Dlatego, gdy przychodzą jakieś dane i chcesz zaktualizować odpowiednie widoczne elementy listy, skąd znasz ich indeksy?

Moje obejście polegało na:

  • zachowaj listę wszystkich utworzonych widoków elementów listy, dodaj elementy do tej listy z newView()
  • kiedy nadejdą dane, wykonaj iterację i zobacz, który z nich wymaga aktualizacji - lepsze niż robienie notifyDatasetChanged()i odświeżanie ich wszystkich

Ze względu na ponowne wyświetlanie widoku liczba odniesień widoku, które będę musiał przechowywać i iterować, będzie mniej więcej równa liczbie elementów listy widocznych na ekranie.

Pēteris Caune
źródło
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);

W przypadku RecycleView użyj tego kodu

Libin Thomas
źródło
zaimplementowałem to na recycleView, to działa. Ale kiedy przewijasz listę, znowu się zmienia. jakaś pomoc?
Fahid Nadeem
1

Użyłem kodu, który dostarczył Erikowi, działa świetnie, ale mam złożony niestandardowy adapter do mojego widoku listy i miałem do czynienia z dwukrotną implementacją kodu aktualizującego interfejs użytkownika. Próbowałem uzyskać nowy widok z metody getView moich adapterów (lista arraylista przechowująca dane widoku listy została już zaktualizowana / zmieniona):

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

Działa dobrze, ale nie wiem, czy to dobra praktyka. Więc nie muszę implementować kodu, który aktualizuje element listy dwa razy.

Primoz990
źródło
1

dokładnie tego użyłem

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
źródło
Otrzymuję widok jako układ względny, jak znaleźć widok tekstu w układzie względnym?
Girish
1

Wymyśliłem inne rozwiązanie, jak void metody RecyclyerViewnotifyItemChanged(int position) , stworzyłem klasę CustomBaseAdapter tak:

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

    }
}

Nie zapomnij utworzyć klasy CustomDataSetObservable również dla mDataSetObservablezmiennej w CustomAdapterClass , na przykład:

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

w klasie CustomBaseAdapter znajduje się metoda notifyItemChanged(int position), którą możesz wywołać, gdy chcesz zaktualizować wiersz w dowolnym miejscu (po kliknięciu przycisku lub w dowolnym miejscu, w którym chcesz wywołać tę metodę). I voila!, Twój pojedynczy wiersz zostanie natychmiast zaktualizowany.

Aprido Sandyasa
źródło
0

Moje rozwiązanie: jeśli jest poprawne *, zaktualizuj dane i widoczne elementy bez ponownego rysowania całej listy. Else notifyDataSetChanged.

Prawidłowo - stary rozmiar danych == nowy rozmiar danych oraz stare identyfikatory danych i ich kolejność == nowe identyfikatory i kolejność danych

W jaki sposób:

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

i oczywiście:

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

Zignoruj ​​ostatni parametr bindView (używam go do określenia, czy muszę ponownie przetwarzać mapy bitowe dla ImageDrawable).

Jak wspomniano powyżej, całkowita liczba „widocznych” widoków to mniej więcej liczba, która mieści się na ekranie (pomijając zmiany orientacji itp.), Więc nie ma dużej pamięci.

JRun
źródło
0

Oprócz tego rozwiązania ( https://stackoverflow.com/a/3727813/5218712 ) chcę tylko dodać, że powinno działać tylko wtedy, gdy listView.getChildCount() == yourDataList.size(); w ListView może być dodatkowy widok.

Przykład wypełniania elementów podrzędnych:  tablica listView.mChildren

Svyatoslav Ruzhitsky
źródło