如何在ListView中更新单行?

135
我有一个显示新闻项目的 ListView,包含一张图片、标题和一些文本。图片在单独的线程中加载(使用了队列等),当图片下载完成时,我现在调用列表适配器上的 notifyDataSetChanged() 来更新该图片。这样做是有效的,但由于 notifyDataSetChanged() 调用了所有可见项目的 getView(),因此 getView() 被频繁地调用,我只想更新列表中的单个项目。如何才能实现呢?
我对当前方法的问题有:
  1. 滚动速度很慢
  2. 当加载单个新图像时,我有一个淡入动画出现在该图像上。
11个回答

201

感谢您的信息Michelle,我找到了答案。 您确实可以使用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!");
}

3
注意:mImgView.setImageURI() 只会更新列表项一次,所以最好使用 mLstVwAdapter.notifyDataSetInvalidated()。 - kellogs
如果我只在位置介于getFirstVisiblePosition()和getLastVisiblePosition()之间时调用适配器的getView()方法,会发生什么?它是否能正常工作?我认为它会像平常一样更新视图,对吗? - android developer
看起来很有用,我会尝试实现单行更新(更改ImageButton图像)以复制Instagram的“喜欢”按钮。困难的部分是我需要确保webservice-calling AsyncTask的回复与按钮的状态匹配。我将尝试使用Broadcast接收器。 - Josh
2
非常好用。它帮助我完成了在类似Instagram的应用程序中实现“喜欢”功能的任务。我使用了自定义适配器,在列表元素中广播喜欢按钮,启动异步任务告诉后端有关喜欢的广播接收器在父片段上,并最终使用Erik的答案更新列表。 - Josh
我的自定义ListView适配器不起作用。我需要在按钮点击时更新行。 - grantespo
显示剩余4条评论

72

这个问题在Google I/O 2010上被提出,你可以在这里观看:

ListView的世界,时间52:30

基本上,Romain Guy解释了要在ListView上调用getChildAt(int)来获取视图,然后(我认为)调用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
不错! :) 或许你也可以在这里添加一个 null 检查 - 如果视图此时不可用,v 可能为空。而且当用户滚动 ListView 时,这些数据也会消失,因此应该更新适配器中的数据(可能无需调用 notifyDataSetChanged())。总的来说,我认为将所有这些逻辑都放在适配器内部,即将 ListView 引用传递给它,是个好主意。 - mreichelt
这对我有用,除了当视图离开屏幕时,重新进入时更改被重置。我猜是因为在getview中它仍然接收原始信息。我该如何解决这个问题? - gloscherrybomb

6
这是我的操作步骤:

您的项目(行)必须具有唯一的ID,以便稍后可以更新它们。当列表从适配器获取视图时,请设置每个视图的标记。(如果默认标记在其他地方使用,则还可以使用关键标记)

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

对于每个列表元素进行更新检查,如果存在指定ID的视图,则显示它并执行更新操作。

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

相比其他方法,这实际上更容易处理id而不是位置。并且,你必须调用更新函数以便让可见的项目得到更新。


3
首先,您需要将模型类作为全局对象获取,例如:模型类对象。
SampleModel golbalmodel=new SchedulerModel();

并将其初始化为全局变量

通过将其初始化为全局模型,获取视图的当前行

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

将更改后的值设置为全局模型对象方法set,并添加notifyDataSetChanged,这对我很有效。

golbalmodel.setStartandenddate(changedate);

notifyDataSetChanged();

这里有一个相关的问题,其中有很好的答案。

注:该问题与在Android中动态添加元素到ListView有关。

这是正确的方法,因为您获取要更新的对象,更新它,然后通知适配器,这将使用新数据更改行信息。 - Gastón Saillén

2
答案清晰且正确,我在这里为CursorAdapter的情况添加一个想法。
如果你正在子类化CursorAdapter(或ResourceCursorAdapterSimpleCursorAdapter),那么你可以实现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上已经实现了这个,它可以工作。但是当您滚动列表时,它会再次发生更改。需要帮助吗? - Fahid Nadeem

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

我正在使用相对布局作为视图,如何在相对布局中找到TextView? - Girish

1

我想出了另一个解决方案,类似于 RecyclyerView 的方法 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;
    } {

    }
}

不要忘记为CustomAdapterClass中的mDataSetObservable变量创建一个CustomDataSetObservable类,如下所示:
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),当你想要更新一行时,你可以在任何地方调用该方法(例如从按钮点击或任何其他地方)。然后,你的单个行将立即更新。请注意,保留 HTML 标签。

1
我使用了 Erik 提供的代码,效果很好,但是我的 ListView 有一个复杂的自定义适配器,我发现需要两次实现更新 UI 的代码。我尝试从我的适配器的 getView 方法中获取新的视图(ListView 数据的 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());
}

它能够正常工作,但我不确定这是否是一个好的做法。 因此,我不需要实现更新列表项两次的代码。


0
我的解决方案: 如果正确,则更新数据和可视项而无需重新绘制整个列表。否则,使用notifyDataSetChanged。
正确- oldData大小==新数据大小,并且旧数据ID及其顺序==新数据ID及其顺序
如何实现:
/**
 * 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的位图)。

如上所述,“可见”视图的总数大致上是屏幕上适合的数量(忽略方向变化等),因此在内存方面没有什么大问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接