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

131

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

Проблемы, которые у меня возникают с моим текущим подходом:

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

Ответы:

199

Я нашел ответ, благодаря вашей информации Мишель. Вы действительно можете получить правильный вид, используя 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!");
}
Erik
источник
2
примечание для себя: mImgView.setImageURI () обновит элемент списка, но только один раз; поэтому мне лучше использовать mLstVwAdapter.notifyDataSetInvalidated ().
kellogs
Это решение работает. Но это можно сделать только с помощью yourListView.getChildAt (index); и измените представление. Оба решения работают !!
AndroidDev
что произойдет, если я просто вызову getView () на адаптере, только если позиция находится между getFirstVisiblePosition () и getLastVisiblePosition ()? он будет работать так же? я думаю, он обновит вид как обычно, верно?
разработчик Android
В моем случае, иногда view имеет значение null, потому что каждый элемент обновляется асинхронно, лучше проверить, не является ли view! =
Null
2
Прекрасно работает. Это помогло мне завершить задачу по реализации функции Like в приложении, похожем на Instagram. Я использовал настраиваемый адаптер, широковещательную передачу на кнопке Like внутри элемента списка, запускал асинтаксическую задачу, чтобы сообщить бэкэнду о подобном с помощью широковещательного приемника в родительском фрагменте, и, наконец, ответ Эрика для обновления списка.
Джош
71

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

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

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

Ромен также указывает на проект под названием 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, потому что они будут вознаграждены красивой анимацией! Будущее выглядит потрясающе! :-)

mreichelt
источник
3
Хорошая вещь! :) Возможно, вы также можете добавить здесь нулевую проверку - v может быть нулевым, если представление недоступно в данный момент. И, конечно, эти данные исчезнут, если пользователь прокручивает ListView, поэтому следует также обновить данные в адаптере (возможно, без вызова notifyDataSetChanged ()). В общем, я думаю, что было бы неплохо сохранить всю эту логику внутри адаптера, то есть передать ему ссылку ListView.
mreichelt
Это сработало для меня, за исключением того, что когда вид покидал экран, когда он снова входит, изменение сбрасывается. Я думаю, потому что в getview он все еще получает исходную информацию. Как мне обойти это?
gloscherrybomb
6

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

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

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

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

Али
источник
3

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

SampleModel golbalmodel=new SchedulerModel();

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

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

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

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

golbalmodel.setStartandenddate(changedate);

notifyDataSetChanged();

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

koteswara DK
источник
Это правильный подход, поскольку вы получаете объект, который хотите обновить, обновляете его, а затем уведомляете адаптер, и это изменяет информацию строки с новыми данными.
Gastón Saillén
2

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

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

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

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

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

Петерис Кауне
источник
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);

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

Либин Томас
источник
Я реализовал это в recycleView, он работает. Но когда вы прокручиваете список, он снова меняется. любая помощь?
Фахид Надим
1

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

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

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

Primoz990
источник
1

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

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
источник
Я получаю View as Relative layout, как найти Textview внутри относительного макета?
Гириш
1

Я придумал другое решение, например, метод 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), и вы можете вызвать этот метод, когда хотите обновить строку в любом месте (при нажатии кнопки или в любом месте, где вы хотите вызвать этот метод). И вуаля !, ваша единственная строка обновится мгновенно ..

Апридо Сандьяса
источник
0

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

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

Как:

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

и конечно:

@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).

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

JRun
источник
0

В дополнение к этому решению ( https://stackoverflow.com/a/3727813/5218712 ) просто хочу добавить, что оно должно работать, только если listView.getChildCount() == yourDataList.size(); внутри ListView может быть дополнительный вид.

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

Святослав Ружицкий
источник