Как я могу обновить одну строку в ListView?

У меня есть ListView, который отображает новости. Они содержат изображение, заголовок и текст. Изображение загружается в отдельный поток (с очередью и всеми), и когда изображение загружается, я теперь вызываю notifyDataSetChanged() в адаптере списка, чтобы обновить изображение. Это работает, но getView() получает вызов слишком часто, так как notifyDataSetChanged() вызывает getView() для всех видимых элементов. Я хочу обновить только один элемент в списке. Как мне это сделать?

У меня есть мой текущий подход:

  • Прокрутка медленная
  • У меня есть анимация с постепенным увеличением изображения, которое происходит каждый раз, когда загружается одно новое изображение в списке.

Ответ 1

Я нашел ответ, благодаря вашей информации Мишель. Вы можете получить правильный вид, используя View#getChildAt(int index). Уловка состоит в том, что он начинает отсчет с первого видимого элемента. Фактически, вы можете получать только видимые предметы. Вы решаете это с помощью ListView#getFirstVisiblePosition().

Пример:

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!");
}

Ответ 2

Этот вопрос задан в Google I/O 2010, вы можете посмотреть его здесь:

Мир ListView, время 52:30

В принципе, объясняет Ромайн Гай, чтобы называть getChildAt(int) на ListView, чтобы получить представление и (я думаю) вызвать getFirstVisiblePosition(), чтобы узнать соотношение между позицией и индексом.

Romain также указывает на проект под названием Shelves в качестве примера, я думаю, что он может означать метод ShelvesActivity.updateBookCovers(), но я не могу найти вызов getFirstVisiblePosition().

УДИВИТЕЛЬНЫЕ ОБНОВЛЕНИЯ:

RecyclerView исправит это в ближайшем будущем. Как указано в http://www.grokkingandroid.com/first-glance-androids-recyclerview/, вы сможете вызвать методы, чтобы точно указать изменение, например:

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

Кроме того, все захотят использовать новые представления на основе RecyclerView, потому что они будут вознаграждены красивой анимацией! Будущее выглядит потрясающе!: -)

Ответ 3

Вот как я это сделал:

Ваши элементы (строки) должны иметь уникальные идентификаторы, чтобы впоследствии их можно обновлять. Установите метку каждого вида, когда список получает представление от адаптера. (Вы также можете использовать ключевой тег, если тег по умолчанию используется где-то еще)

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

Для обновления проверьте каждый элемент списка, если вид с указанным идентификатором существует, поэтому мы выполняем обновление.

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

Это на самом деле проще, чем другие методы и лучше, когда вы имеете дело с идентификаторами не позиций! Также вы должны вызвать обновление для элементов, которые становятся видимыми.

Ответ 4

получить класс модели сначала как глобальный, как этот объект класса модели

SampleModel golbalmodel=new SchedulerModel();

и инициализировать его глобальным

получить текущую строку представления моделью, инициализируя ее глобальной моделью

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

установить измененное значение в метод глобального объекта модели, который будет установлен, и добавить для него notifyDataSetChanged его работы для меня

golbalmodel.setStartandenddate(changedate);

notifyDataSetChanged();

Вот связанный с этим вопрос с хорошими ответами.

Ответ 5

Ответы ясны и правильны, я добавлю идею для случая CursorAdapter здесь.

Если вы подклассифицируете CursorAdapter (или ResourceCursorAdapter или SimpleCursorAdapter), то вы можете либо реализовать методы ViewBinder, либо переопределить bindView() и newView(), они не получают текущий индекс элемента списка в аргументах. Поэтому, когда некоторые данные поступают, и вы хотите обновлять соответствующие элементы видимого списка, как вы знаете их индексы?

Мое обходное решение заключалось в следующем:

  • сохранить список всех созданных представлений элементов списка, добавить элементы в этот список из newView()
  • когда данные поступают, итерации их и посмотреть, какой из них нужно обновить - лучше, чем делать notifyDatasetChanged() и обновлять их все

Из-за просмотра повторного использования количество ссылок на просмотр, которые мне нужно сохранить и повторить, будет примерно равным количеству элементов списка, отображаемых на экране.

Ответ 6

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

Для RecycleView используйте этот код

Ответ 7

Я использовал код, который предоставил Erik, отлично работает, но у меня есть сложный пользовательский адаптер для моего списка, и я столкнулся с двойной реализацией кода, который обновляет интерфейс. Я попытался получить новый вид из моих адаптеров getView method (arraylist, который содержит данные списка, уже обновлен/изменен):

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

Хорошо работает, но я не знаю, хорошо ли это. Поэтому мне не нужно реализовывать код, который обновляет элемент списка два раза.

Ответ 8

точно я использовал это

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

Ответ 9

Я создал другое решение, например RecyclyerView метод void notifyItemChanged(int position), создайте класс CustomBaseAdapter:

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

    }
}

Не забудьте создать класс CustomDataSetObservable для переменной mDataSetObservable в CustomAdapterClass, например:

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

в классе CustomBaseAdapter существует метод notifyItemChanged(int position), и вы можете вызвать этот метод, когда хотите обновить строку везде, где хотите (от нажатия кнопки или в любом месте, где вы хотите вызвать этот метод). И voila!, Ваша единственная строка будет обновляться мгновенно.

Ответ 10

Мое решение: Если это правильно *, обновите данные и просматриваемые элементы без повторного рисования всего списка. Else notifyDataSetChanged.

Правильный - старый размер DATA == новый размер данных и старые идентификаторы данных и их порядок == новые идентификаторы данных и порядок

Как

/**
 * 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 update it!
            bindView(position, view, false);
            nVisibleViews--;
        }
    }

    return true;
}

и, конечно же:

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

Игнорировать последний параметр bindView (я использую его, чтобы определить, нужно ли мне перерабатывать растровые изображения для ImageDrawable).

Как уже упоминалось выше, общее число "видимых" представлений - это примерно то количество, которое подходит на экране (игнорируя изменения ориентации и т.д.), поэтому нет большого размера памяти.

Ответ 11

В дополнение к этому решению (fooobar.com/info/51896/...) просто хочу добавить, что оно должно работать, только если listView.getChildCount() == yourDataList.size(); Там может быть дополнительное представление внутри ListView.

Пример заполнения дочерних элементов:  listView.mChildren array