Como posso atualizar uma única linha em um ListView?

131

Eu tenho um ListViewque exibe itens de notícias. Eles contêm uma imagem, um título e algum texto. A imagem é carregada em um thread separado (com uma fila e tudo) e, quando a imagem é baixada, chamo agora notifyDataSetChanged()o adaptador de lista para atualizar a imagem. Isso funciona, mas getView()está sendo chamado com muita frequência, pois notifyDataSetChanged()solicita getView()todos os itens visíveis. Quero atualizar apenas o item único na lista. Como eu faria isso?

Os problemas que tenho com minha abordagem atual são:

  1. A rolagem é lenta
  2. Tenho uma animação gradual na imagem, que acontece toda vez que uma única nova imagem da lista é carregada.
Erik
fonte

Respostas:

199

Encontrei a resposta, graças às suas informações, Michelle. Você pode realmente obter a visão correta usando View#getChildAt(int index). O problema é que ele começa a contar a partir do primeiro item visível. Na verdade, você só pode obter os itens visíveis. Você resolve isso com ListView#getFirstVisiblePosition().

Exemplo:

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
fonte
2
note: self: mImgView.setImageURI () atualizará o item da lista, mas apenas uma vez; então é melhor usar mLstVwAdapter.notifyDataSetInvalidated ().
Kellogs
Esta solução funciona. Mas isso pode ser feito apenas com yourListView.getChildAt (index); e mude a vista. Ambas as soluções funcionam !!
precisa saber é o seguinte
o que aconteceria se eu chamasse getView () no adaptador apenas se a posição estiver entre getFirstVisiblePosition () e getLastVisiblePosition ()? funcionaria da mesma maneira? acho que atualizará a visualização como de costume, certo?
desenvolvedor android
No meu caso, algumas vezes ver é nulo, porque sendo atualização de cada item de forma assíncrona, melhor para verificar se vista! = Null
Khawar
2
Funciona bem. Isso me ajudou a concluir a tarefa de implementar o recurso Curtir em um aplicativo semelhante ao instagram. Eu estava usando um adaptador personalizado, uma transmissão no botão like dentro de um elemento da lista, iniciando uma tarefa assíncrona para informar o backend sobre o assunto com um receptor de transmissão no fragmento pai e, finalmente, a resposta de Erik para atualizar a lista.
23414 Josh
71

Esta pergunta foi feita no Google I / O 2010, você pode assisti-la aqui:

O mundo do ListView, hora 52:30

Basicamente o que Romain Guy explica é chamar getChildAt(int)a ListViewobter a vista e chamada (eu acho) getFirstVisiblePosition()para descobrir a correlação entre a posição e índice.

Romain também aponta para o projeto chamado Shelves como um exemplo, acho que ele pode ser o método ShelvesActivity.updateBookCovers(), mas não consigo encontrar o chamado getFirstVisiblePosition().

ATUALIZAÇÕES IMPRESSIONANTES QUE VÊM:

O RecyclerView corrigirá isso no futuro próximo. Conforme indicado em http://www.grokkingandroid.com/first-glance-androids-recyclerview/ , você poderá chamar métodos para especificar exatamente a alteração, como:

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

Além disso, todos desejarão usar as novas visualizações baseadas no RecyclerView, pois serão recompensadas com animações bonitas! O futuro parece incrível! :-)

mreichelt
fonte
3
Coisa boa! :) Talvez você possa adicionar uma verificação nula aqui também - v pode ser nulo se a visualização não estiver disponível no momento. E é claro que esses dados desaparecerão se o usuário rolar o ListView, portanto, também se deve atualizar os dados no adaptador (talvez sem chamar o notifyDataSetChanged ()). Em geral, acho que seria uma boa ideia manter toda essa lógica dentro do adaptador, ou seja, passar a referência do ListView para ele.
Mreichelt 17/09/10
Isso funcionou para mim, exceto - quando a visualização saiu da tela, quando ela entra novamente, a alteração é redefinida. Eu acho que porque no getview ele ainda está recebendo as informações originais. Como contornar isso?
gloscherrybomb
6

Foi assim que eu fiz:

Seus itens (linhas) devem ter IDs exclusivos para que você possa atualizá-los mais tarde. Defina a tag de todas as visualizações quando a lista estiver obtendo a visualização do adaptador. (Você também pode usar a tag key se a tag padrão for usada em outro 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 a atualização, verifique todos os elementos da lista, se uma visualização com o ID fornecido estiver visível, para que possamos executar a atualização.

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

Na verdade, é mais fácil do que outros métodos e melhor quando você lida com identificações e não posições! Além disso, você deve chamar a atualização para itens que ficam visíveis.

Todos
fonte
3

obtenha a classe de modelo primeiro como global como este objeto de classe de modelo

SampleModel golbalmodel=new SchedulerModel();

e inicializá-lo para global

obtenha a linha atual da visualização pelo modelo, inicializando-o no modelo global

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

defina o valor alterado para o método de objeto de modelo global a ser definido e inclua o notifyDataSetChanged que funciona para mim

golbalmodel.setStartandenddate(changedate);

notifyDataSetChanged();

Aqui está uma pergunta relacionada com boas respostas.

koteswara DK
fonte
Essa é a abordagem correta, já que você está obtendo o objeto que deseja atualizar, atualize-o e notifique o adaptador e isso alterará as informações da linha com os novos dados.
Gastón Saillén 30/06/19
2

As respostas são claras e corretas, vou adicionar uma ideia para o CursorAdaptercaso aqui.

Se você está subclassificando CursorAdapter(ou ResourceCursorAdapter, ou SimpleCursorAdapter), pode implementar ViewBinderou substituir bindView()e newView()métodos, eles não recebem o índice atual do item da lista nos argumentos. Portanto, quando alguns dados chegam e você deseja atualizar itens relevantes da lista visível, como você conhece os índices deles?

Minha solução alternativa foi:

  • mantenha uma lista de todas as visualizações de itens de lista criadas, adicione itens a essa lista em newView()
  • quando os dados chegarem, itere-os e veja qual deles precisa ser atualizado - melhor do que fazer notifyDatasetChanged()e atualizar todos eles

Devido à reciclagem de visualizações, o número de referências de visualização que preciso armazenar e a iteração será aproximadamente igual ao número de itens da lista visíveis na tela.

Pēteris Caune
fonte
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 o RecycleView, use este código

Libin Thomas
fonte
Eu implementei isso no recycleView, ele funciona. Mas quando você rola na lista, ela muda novamente. qualquer ajuda?
Fahid Nadeem
1

Eu usei o código que forneceu o Erik, funciona muito bem, mas tenho um adaptador personalizado complexo para o meu listview e fui confrontado com a implementação duas vezes do código que atualiza a interface do usuário. Tentei obter a nova visualização dos meus métodos getView dos adaptadores (a arraylist que contém os dados da listview já foi atualizada / alterada):

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 bem, mas não sei se é uma boa prática. Portanto, não preciso implementar o código que atualiza o item da lista duas vezes.

Primoz990
fonte
1

exatamente eu usei isso

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
fonte
Estou recebendo o layout Visualizar como relativo, como encontrar o Textview dentro do layout relativo?
Girish 15/07
1

Criei outra solução, como o método RecyclyerView void notifyItemChanged(int position), crie a classe CustomBaseAdapter da seguinte maneira:

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ão se esqueça de criar uma classe CustomDataSetObservable também para a mDataSetObservablevariável CustomAdapterClass , assim:

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

na classe CustomBaseAdapter, existe um método notifyItemChanged(int position), e você pode chamá-lo quando quiser atualizar uma linha onde quiser (clique no botão ou em qualquer lugar que desejar chamar esse método). E pronto !, sua única linha será atualizada instantaneamente ..

Aprido Sandyasa
fonte
0

Minha solução: se estiver correto *, atualize os dados e os itens visíveis sem redesenhar a lista inteira. Caso contrário, notifyDataSetChanged.

Correto - tamanho dos dados antigos == novo tamanho dos dados e IDs de dados antigos e sua ordem == novos IDs de dados e ordem

Quão:

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

e claro:

@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 o último parâmetro para bindView (eu o uso para determinar se preciso ou não reciclar bitmaps para o ImageDrawable).

Como mencionado acima, o número total de visualizações 'visíveis' é aproximadamente a quantidade que cabe na tela (ignorando as alterações de orientação, etc.), portanto, nada demais em termos de memória.

JRun
fonte
0

Além desta solução ( https://stackoverflow.com/a/3727813/5218712 ), basta adicionar que ela deve funcionar apenas se houver listView.getChildCount() == yourDataList.size(); uma visualização adicional dentro do ListView.

Exemplo de como os elementos filho são preenchidos:  matriz listView.mChildren

Svyatoslav Ruzhitsky
fonte