不重新填充的情况下更新 GridView/ListView

7
每当您更新GridView/ListView时,都会在适配器上调用notifyDatasetChanged,这会使用新数据重新填充列表,并删除当前列表中的所有内容。
现在以L Preview设计指南中的video为例。
是否可能实现相同的效果,即只看到新项目进入而没有任何“闪烁”,当适配器重新加载时,或者这只是目前仅能在L中完成的事情?
此效果也可以在Google Keep应用程序中找到,添加新笔记时。

也许这对您有用:https://android.googlesource.com/platform/packages/apps/Launcher/+/master/src/com/android/launcher/Launcher.java - Lazy
@Lazy 能否解释一下这如何帮助回答我的问题? - tyczj
当您安装新应用程序时,该启动器会将图标添加到主屏幕(GridView),而无需重新填充。我认为这正是您想要的? - Lazy
3个回答

8
我恐怕这都是视觉技巧,也就是动画。
实际上并不是这样的。 notifyDataSetChanged 仅告诉底层观察器数据已更改。仅此而已。作为响应,对于每个可见视图 - 即通过 ListView#getChildCount() 可访问的数量,调用 getView(...)。由于索引已更改(添加/删除项),或者保存在这些索引中的对象中的数据已更改(一个或多个项已更改),因此后续对 ListView#getView(...) 的调用将使数据在视觉上更新。
有一个由“Android Developers”制作的有趣视频,解释了如何产生您想要的效果: DevBytes: ListView Cell Insertion Animation
该视频/示例仅讨论单个项目插入。但是,使用一些时间和数学技能,可以将其扩展到所有数据操作。
我已经成功将代码示例扩展到插入多个项。我将动画更改为简单的 alpha-淡入。单击“Add row”按钮会向列表视图添加一个随机数(介于1和3之间)。以下是它的外观:

enter image description here

伪代码工作流程:
  • 在将新项传递给适配器之前,循环遍历列表视图中的所有可见项,并保存它们的边界(在Rect中)和快照(在 BitmapDrawables中)

  • 将项目传递给适配器

  • 向 ListView 的 ViewTreeObserver 添加一个 OnPreDrawListener

  • 当调用 onPreDraw(...) 时,新项目准备就绪,我们可以使用相应索引的 ListView#getChildAt(...) 访问它们的views

  • 所有新项目均设置为不可见,并分配 alpha 动画器

  • 所有旧项目都被分配了 translation_y 动画器

  • 不再通过getChildAt(..)访问的项目 - 因为添加了新项目 - 分配了 translation_y 动画器 - 将它们从屏幕上移走(或使其超出列表视图范围)

  • 启动 translation_y 动画器。当这些动画器运行完成时,开始 alpha 动画器。

注意,动画的类型(alpha、平移、缩放或任何这些的组合)相对不重要。我想在 GridViewStaggeredGridView (保持) 的情况下这会更加困难-只是因为数学涉及到 XY 的平移。但流程应该保持不变。
(^)- 语法上有错误,但在我看来很自然。

1
作为回应,对于每个可见视图,都会调用getView(...)。这就是我所说的从列表中删除所有内容的意思。我有一种感觉,这可能涉及创建某种副本并移动该副本。除非有人提供更好的解决方案(我怀疑),否则这可能是能够实现我想要的答案。 - tyczj
1
@tyczj “...这就是我所说的从列表中删除所有内容。” 哦,好的。我也会等待更好(和/或更容易)的解决方案。这是一个有趣的问题...谢谢。 - Vikram

1
这可以通过获取视图并在其中更改数据来实现。我已经包含了一个示例,在长按时,您可以更改数据而不刷新可见项并且不调用notifyDataSetChanged()。
这个问题已经在Google I/O 2010上被问到过,你可以在这里观看: ListView的世界,时间52:30 代码来自Vogella
package com.example.stableids;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import android.os.Build;
import android.os.Bundle;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.database.DataSetObserver;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Adapter;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;

@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
@SuppressLint("NewApi")
public class MainActivity extends Activity {

      @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    @SuppressLint("NewApi")
    @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final ListView listview = (ListView) findViewById(R.id.listview);
        String[] values = new String[] { "Android", "iPhone", "WindowsMobile",
            "Blackberry", "WebOS", "Ubuntu", "Windows7", "Max OS X",
            "Linux", "OS/2", "Ubuntu", "Windows7", "Max OS X", "Linux",
            "OS/2", "Ubuntu", "Windows7", "Max OS X", "Linux", "OS/2",
            "Android", "iPhone", "WindowsMobile" };

        final ArrayList<String> list = new ArrayList<String>();
        for (int i = 0; i < values.length; ++i) {
          list.add(values[i]);
        }
        final StableArrayAdapter adapter = new StableArrayAdapter(this,
            android.R.layout.simple_list_item_1, list);
        listview.setAdapter(adapter);

        listview.setOnItemClickListener(new AdapterView.OnItemClickListener() {

          @SuppressLint("NewApi")
        @Override
          public void onItemClick(AdapterView<?> parent, final View view,
              final int position, long id) {
            final String item = (String) parent.getItemAtPosition(position);
            view.animate().setDuration(2000).alpha(0)
                .withEndAction(new Runnable() {
                  @Override
                  public void run() {
                    int i = list.indexOf(item);

                    list.remove(item);
                    list.add(i, "MODIFIED");
                    adapter.mIdMap.put("MODIFIED", position);
                    TextView tv = (TextView)view.findViewById(android.R.id.text1);
                    tv.setText("MODIFIED");
                    //adapter.notifyDataSetChanged();
                    view.setAlpha(1);
                  }
                });
          }

        });
      }

      private class StableArrayAdapter extends ArrayAdapter<String> {

        HashMap<String, Integer> mIdMap = new HashMap<String, Integer>();

        public StableArrayAdapter(Context context, int textViewResourceId,
            List<String> objects) {
          super(context, textViewResourceId, objects);
          for (int i = 0; i < objects.size(); ++i) {
            mIdMap.put(objects.get(i), i);
          }
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {

            Log.d("STABLE ID", "getView called for " + position + " position");
            return super.getView(position, convertView, parent);

        }

        @Override
        public long getItemId(int position) {
          String item = getItem(position);
          return mIdMap.get(item);
        }

        @Override
        public boolean hasStableIds() {
          return true;
        }

      }

}

0

不需重新填充即可更新 GridView/ListView

如果您在每次更新和删除时都使用相同的 ArrayList 而不创建新的 ArrayList,则您的 adapter.notifyDatasetChanged() 方法将起作用。 请每次都使用相同的 arrayList,不要创建 Adapter 类的新实例。


我不会每次都创建一个新的适配器实例,而是使用同一个列表。正如您在我的问题中所读到的那样,我使用notifyDatasetChanged来更新我的网格。 - tyczj

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