【问题标题】:How can I update a single row in a ListView?如何更新 ListView 中的单行?
【发布时间】:2011-04-13 02:21:05
【问题描述】:

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

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

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

【问题讨论】:

    标签: android android-listview android-arrayadapter


    【解决方案1】:

    感谢您提供的信息 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!");
    }
    

    【讨论】:

    • 自我注意:mImgView.setImageURI() 将更新列表项,但只会更新一次;所以我最好改用 mLstVwAdapter.notifyDataSetInvalidated()。
    • 此解决方案有效。但这只能通过 yourListView.getChildAt(index);并改变看法。两种解决方案都有效!
    • 如果仅当位置在 getFirstVisiblePosition() 和 getLastVisiblePosition() 之间时才在适配器上调用 getView() 会发生什么?它会一样吗?我认为它会像往常一样更新视图,对吧?
    • 在我的情况下,有时 view 是 null,因为每个项目都是异步更新的,最好检查 view != null
    • 效果很好。它帮助我完成了在类似 Instagram 的应用程序中实现 Like 功能的任务。我使用了一个自定义适配器,一个列表元素内的like按钮中的广播,启动一个异步任务来告诉后端在父片段上使用广播接收器,最后是Erik更新列表的答案。
    【解决方案2】:

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

    The world of ListView, time 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 的新视图,因为他们将获得漂亮的动画效果!未来看起来很棒! :-)

    【讨论】:

    • 好东西! :) 也许您也可以在此处添加一个空检查 - 如果当前视图不可用, v 可能为空。当然,如果用户滚动 ListView,这些数据将会消失,因此还应该更新适配器中的数据(可能不需要调用 notifyDataSetChanged())。一般来说,我认为将所有这些逻辑保留在适配器中是一个好主意,即将 ListView 引用传递给它。
    • 这对我有用,除了 - 当视图离开屏幕时,当它重新进入时,更改被重置。我猜是因为在 getview 中它仍在接收原始信息。我该如何解决这个问题?
    【解决方案3】:

    我就是这样做的:

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

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

    当您处理 ids 而不是位置时,它实际上比其他方法更容易并且更好!此外,您必须为可见的项目调用更新。

    【讨论】:

      【解决方案4】:

      首先获取模型类,就像这个模型类对象一样全局

      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】:

      答案清晰正确,我将在此处添加CursorAdapter案例的想法。

      如果你继承CursorAdapter(或ResourceCursorAdapter,或SimpleCursorAdapter),那么你要么实现ViewBinder,要么覆盖bindView()newView()方法,这些不接收当前列表项索引在争论中。因此,当一些数据到达并且您想要更新相关的可见列表项时,您如何知道它们的索引?

      我的解决方法是:

      • 保留所有已创建列表项视图的列表,将来自newView() 的项目添加到此列表中
      • 当数据到达时,对它们进行迭代,看看哪一个需要更新——比 notifyDatasetChanged() 并刷新所有数据要好

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

      【讨论】:

        【解决方案6】:
        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 上实现了这个,它可以工作。但是,当您滚动列表时,它会再次发生变化。有什么帮助吗?
        【解决方案7】:

        我使用了提供 Erik 的代码,效果很好,但是我的列表视图有一个复杂的自定义适配器,并且我遇到了两次更新 UI 的代码实现。我试图从我的适配器 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());
        }
        

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

        【讨论】:

          【解决方案8】:

          正是我用的这个

          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?
          【解决方案9】:

          我另辟蹊径,如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),当你想在任何地方更新一行时,你可以调用该方法(从按钮单击或任何你想调用该方法的地方)。瞧!,您的单行将立即更新..

          【讨论】:

            【解决方案10】:

            我的解决方案: 如果正确*,请更新数据和可查看项目,而无需重新绘制整个列表。否则 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 回收位图)。

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

            【讨论】:

              【解决方案11】:

              除了这个解决方案 (https://stackoverflow.com/a/3727813/5218712) 只想补充一点,它应该只有在 listView.getChildCount() == yourDataList.size(); ListView 内可能还有其他视图。

              如何填充子元素的示例:

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2015-09-27
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多