Как я могу обновить одну строку в ListView?
У меня есть ListView
который отображает новости. Они содержат изображение, заголовок и текст. Изображение загружается в отдельном потоке (с очередью и всем), и когда изображение загружается, я сейчас вызываю notifyDataSetChanged()
в списке адаптер для обновления изображения. Это работает, но getView()
вызывается слишком часто, так как notifyDataSetChanged()
звонки getView()
для всех видимых предметов. Я хочу обновить только один элемент в списке. Как бы я это сделал?
Проблемы с моим текущим подходом:
- Прокрутка медленная
- У меня есть постепенная анимация на изображении, которая происходит каждый раз, когда загружается одно новое изображение в списке.
10 ответов
Я нашел ответ, благодаря вашей информации Мишель. Вы действительно можете получить правильный вид, используя 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!");
}
Этот вопрос был задан на Google I/O 2010, вы можете посмотреть его здесь:
В основном, что объясняет Ромен Гай, так это называть 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, потому что они будут вознаграждены красивыми анимациями! Будущее выглядит потрясающе!:-)
Вот как я это сделал:
Ваши элементы (строки) должны иметь уникальные идентификаторы, чтобы вы могли обновить их позже. Установите тег для каждого представления, когда список получает представление от адаптера. (Вы также можете использовать ключевой тег, если тег по умолчанию используется в другом месте)
@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
}
}
}
Это на самом деле проще, чем другие методы, и лучше, когда вы имеете дело с идентификаторами, а не позициями! Также вы должны вызвать обновление для элементов, которые становятся видимыми.
Сначала получить класс модели как глобальный, как этот объект класса модели
SampleModel golbalmodel=new SchedulerModel();
и инициализировать его в глобальный
получить текущую строку представления по модели, инициализировав ее в глобальную модель
SampleModel data = (SchedulerModel) sampleList.get(position);
golbalmodel=data;
установите измененное значение на метод объекта глобальной модели и добавьте notifyDataSetChanged, его работы для меня
golbalmodel.setStartandenddate(changedate);
notifyDataSetChanged();
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, пожалуйста, используйте этот код
Ответы ясны и правильны, я добавлю идею для CursorAdapter
дело здесь.
Если вы подкласс CursorAdapter
(или же ResourceCursorAdapter
, или же SimpleCursorAdapter
), то вы получите либо реализовать ViewBinder
или переопределить bindView()
а также newView()
методы, они не получают текущий индекс элемента списка в аргументах. Следовательно, когда поступают некоторые данные и вы хотите обновить соответствующие видимые элементы списка, как вы узнаете их индексы?
Мой обходной путь должен был:
- вести список всех созданных представлений элементов списка, добавлять элементы в этот список из
newView()
- когда поступают данные, выполните их итерацию и посмотрите, какой из них нужно обновить - лучше, чем делать
notifyDatasetChanged()
и освежает их всех
Из-за повторного использования представления количество ссылок на представления, которые мне нужно будет сохранить и повторить, будет примерно равно количеству элементов списка, видимых на экране.
Я составил другое решение, например, метод 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)
и вы можете вызывать этот метод, когда вы хотите обновить строку, где вы хотите (от нажатия кнопки или где вы хотите вызвать этот метод). И вуаля!, Ваша единственная строка будет обновляться мгновенно..
Именно я использовал это
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);
}
Я использовал код, который предоставил Эрик, прекрасно работает, но у меня есть сложный настраиваемый адаптер для моего списка, и я столкнулся с двойной реализацией кода, который обновляет пользовательский интерфейс. Я попытался получить новое представление из метода getView моих адаптеров (массив данных, содержащий данные списка, уже обновлен / изменен):
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());
}
Это работает хорошо, но я не знаю, хорошая ли это практика. Поэтому мне не нужно реализовывать код, который обновляет элемент списка два раза.
Мое решение: если это правильно *, обновите данные и просматриваемые элементы, не перерисовывая весь список. Иначе 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).
Как упомянуто выше, общее количество "видимых" представлений примерно равно количеству, которое умещается на экране (игнорируя изменения ориентации и т. Д.), Поэтому не имеет значения для памяти.
В дополнение к этому решению ( /questions/15932707/kak-ya-mogu-obnovit-odnu-stroku-v-listview/15932724#15932724) просто хочу добавить, что оно должно работать, только еслиlistView.getChildCount() == yourDataList.size();
Внутри ListView может быть дополнительный вид.