ListView не обновляет уже видимые элементы

Я показываю список контактов (имя + изображение) с помощью ListView. Чтобы ускорить первоначальную загрузку, я сначала загружаю имена и откладываю загрузку изображения. Теперь, когда мой фоновый поток заканчивает загрузку изображения, он назначает мой адаптер notifyDataSetChanged() для вызова в потоке пользовательского интерфейса. К сожалению, когда это происходит, ListView не перерисовывает (т.е. Вызывает getView() для) элементы, которые уже находятся на экране. Из-за этого пользователь не видит вновь загруженное изображение, если только они не прокручиваются и не возвращаются к одному и тому же набору элементов, так что просмотры становятся вторичными. Некоторые соответствующие биты кода:

private final Map<Long, Bitmap> avatars = new HashMap<Long, Bitmap>();

// this is called *on the UI thread* by the background thread
@Override
public void onAvatarLoaded(long contactId, Bitmap avatar) {
    avatars.put(requestCode, avatar);
    notifyDataSetChanged();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
    // snip...
    final Bitmap avatar = avatars.get(contact.id);
    if (avatar != null) {
        tag.avatar.setImageBitmap(avatar);
        tag.avatar.setVisibility(View.VISIBLE);
        tag.defaultAvatar.setVisibility(View.GONE);
    } else {
        tag.avatar.setVisibility(View.GONE);
        tag.defaultAvatar.setVisibility(View.VISIBLE);
        if (!avatars.containsKey(contact.id)) {
            avatars.put(contact.id, null);
            // schedule the picture to be loaded
            avatarLoader.addContact(contact.id, contact.id);
        }
    }
}

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

Ответы

Ответ 1

Здесь я отвечаю на свой вопрос с обманом, который я поселился. По-видимому, notifyDataSetChanged() можно использовать, только если вы добавляете/удаляете элементы. Если вы обновляете информацию о уже отображаемых элементах, вы можете получить видимые элементы, не обновляющие их внешний вид (getView() не вызывается на вашем адаптере).

Кроме того, вызов invalidateViews() на ListView не работает, как рекламируется. Я все еще получаю такое же глючное поведение, когда getView() не вызывается для обновления экранных элементов.

Сначала я думал, что проблема вызвана частотой, с которой я звонил notifyDataSetChanged()/invalidateViews() (очень быстро, из-за обновлений, поступающих из разных источников). Поэтому я попытался использовать дросселирование этих методов, но все же безрезультатно.

Я все еще не уверен на 100%, это ошибка платформы, но тот факт, что мои hackaround работает, кажется, предлагает это. Таким образом, без лишнего шума мой hackaround состоит в расширении ListView для обновления видимых элементов. Обратите внимание, что это работает, только если вы правильно используете convertView в своем адаптере и никогда не возвращаете новый View, когда был отправлен convertView. По понятным причинам:

public class ProperListView extends ListView {

    private static final String TAG = ProperListView.class.getName();

    @SuppressWarnings("unused")
    public ProperListView(Context context) {
        super(context);
    }

    @SuppressWarnings("unused")
    public ProperListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @SuppressWarnings("unused")
    public ProperListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    class AdapterDataSetObserver extends DataSetObserver {
        @Override
        public void onChanged() {
            super.onChanged();

            refreshVisibleViews();
        }

        @Override
        public void onInvalidated() {
            super.onInvalidated();

            refreshVisibleViews();
        }
    }

    private DataSetObserver mDataSetObserver = new AdapterDataSetObserver();
    private Adapter mAdapter;

    @Override
    public void setAdapter(ListAdapter adapter) {
        super.setAdapter(adapter);

        if (mAdapter != null) {
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        }
        mAdapter = adapter;

        mAdapter.registerDataSetObserver(mDataSetObserver);
    }

    void refreshVisibleViews() {
        if (mAdapter != null) {
            for (int i = getFirstVisiblePosition(); i <= getLastVisiblePosition(); i ++) {
                final int dataPosition = i - getHeaderViewsCount();
                final int childPosition = i - getFirstVisiblePosition();
                if (dataPosition >= 0 && dataPosition < mAdapter.getCount()
                        && getChildAt(childPosition) != null) {
                    Log.v(TAG, "Refreshing view (data=" + dataPosition + ",child=" + childPosition + ")");
                    mAdapter.getView(dataPosition, getChildAt(childPosition), this);
                }
            }
        }
    }

}

Ответ 2

Добавьте следующую строку в onResume() listview.setAdapter(listview.getAdapter());

Ответ 3

Согласно документации:

void notifyDataSetChanged()

Сообщите зарегистрированным наблюдателям, что набор данных изменился. ... LayoutManagers будут вынуждены полностью переподтвердить и передать все видимые виды...

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

Обходной путь в классе адаптера:

public void notifyDataSetChanged_fix() {
    // unfortunately notifyDataSetChange is declared final, so cannot be overridden. 
    super.notifyDataSetChanged();
    for (int i = getItemCount()-1; i>=0; i--) notifyItemChanged(i);
}

Заменяет все вызовы notifyDataSetChanged() на notifyDataSetChanged_fix(), а мой RecyclerView с удовольствием освежает с тех пор...