如何更新ListView中的单行?

时间:2010-09-16 08:18:26

标签: android android-listview android-arrayadapter

我有一个显示新闻项目的ListView。它们包含图像,标题和一些文本。图像被加载到一个单独的线程(带有队列和所有)中,当下载图像时,我现在在列表适配器上调用notifyDataSetChanged()来更新图像。这很有效,但getView()过于频繁地调用,因为notifyDataSetChanged()会为所有可见项调用getView()。我想只更新列表中的单个项目。我该怎么做?

我目前采用的方法存在的问题是:

  1. 滚动很慢
  2. 我在图像上有一个淡入动画,每次加载列表中的一个新图像时都会发生这种动画。

10 个答案:

答案 0 :(得分:194)

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

答案 1 :(得分:69)

此问题已在Google I / O 2010上提出,您可以在此处观看:

The world of ListView, time 52:30

Romain Guy解释的基本上就是在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的新视图,因为他们将获得精美的动画奖励!未来看起来很棒! : - )

答案 2 :(得分: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 :(得分:2)

答案清晰明了,我会在这里为CursorAdapter案例添加一个想法。

如果您继承CursorAdapter(或ResourceCursorAdapterSimpleCursorAdapter),则可以实施ViewBinder或覆盖bindView()和{{1}方法,这些方法不接收参数中的当前列表项索引。因此,当某些数据到达并且您想要更新相关的可见列表项时,您如何知道它们的索引?

我的解决方法是:

  • 保留所有已创建的列表项视图的列表,从newView()
  • 向此列表添加项目
  • 当数据到达时,迭代它们并查看哪一个需要更新 - 比执行newView()更好并刷新所有这些

由于视图回收,我需要存储和迭代的视图引用数量将大致等于屏幕上可见的列表项数量。

答案 4 :(得分:2)

首先将模型类作为全局模型类对象

SampleModel golbalmodel=new SchedulerModel();

并将其初始化为全局

通过将模型初始化为全局模型来获取模型的当前行

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

将更改后的值设置为要设置的全局模型对象方法,并添加它为我工作的notifyDataSetChanged

golbalmodel.setStartandenddate(changedate);

notifyDataSetChanged();

Here is a related question on this with good answers.

答案 5 :(得分: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());
}

它运作良好,但我不知道这是不是一个好习惯。 所以我不需要实现两次更新列表项的代码。

答案 6 :(得分: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);
    }

答案 7 :(得分: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;
    } {

    }
}

不要忘记为 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),当你想要在任何地方更新一行时你可以调用该方法(从按钮点击或你想要调用该方法的任何地方) 。瞧!,你的单行会立即更新..

答案 8 :(得分:0)

我的解决方案: 如果正确*,则更新数据和可查看项目,而不重新绘制整个列表。否则notifyDataSetChanged。

正确 - 旧数据大小==新数据大小,旧数据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的位图)。

如上所述,“可见”视图的总数大致是适合屏幕的数量(忽略方向变化等),因此没有大记忆。

答案 9 :(得分:0)

除了此解决方案(https://stackoverflow.com/a/3727813/5218712)之外,还想补充一点,即只有None ListView内可能还有其他视图。

如何填充子元素的示例: listView.mChildren array