FragmentStatePagerAdapter with ListView - 如何在不向上滚动的情况下更新列表视图的内容?

FragmentStatePagerAdapter with ListView - how to update content of the listview without scroll up?

我在每个页面中都有一个带有 Listview 的 FragmentStatePagerAdapter。 FragmentStatePagerAdapter 每秒用一些页面的新数据刷新一次(记住每个页面都是一个列表视图)。

我用 setAdapter 设置列表视图的内容:

StableArrayAdapter adapter = new StableArrayAdapter((MainActivity) getActivity(), R.layout.list_view_item, ((MainActivity) getActivity()).getData(mPage));
listView.setAdapter(adapter);

问题是有些页面的内容很多,垂直滚动,所以在调用listView.setAdapter(adapter);的时候每一秒,listview都会强制滚动到他上面的位置。这是一种非常不正常和令人讨厌的行为。

我在 Stack Overflow 上找到了这个:notifyDataSetChanged() makes the list refresh and scroll jumps back to the top

问题是这些解决方案是为普通 ViewPager 或普通 Adapter 设计的,不能与 FragmentStatePageAdapter 或其他东西一起使用,因为在尝试它们之后我仍然遇到同样的问题。

这是我的 FragmentStatePagerAdapter 适配器:

public class CollectionPagerAdapter extends FragmentStatePagerAdapter {
    public CollectionPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int position) {
        return ObjectFragment.newInstance(position);
    }

    @Override
    public int getCount() {
        return infoTitlesArray.length;
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return infoTitlesArray[position];
    }
}

出现问题的ViewPager:

<android.support.v4.view.ViewPager
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.v4.view.PagerTitleStrip
            android:id="@+id/pager_title_strip"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="top"
            android:paddingTop="4dp"
            android:paddingBottom="4dp"
            style="@style/CustomPagerTitleStrip"/>
    </android.support.v4.view.ViewPager>

片段布局:

<ListView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/listview"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">
</ListView>

片段的Java代码:

public static class ObjectFragment extends Fragment implements DataUpdateListener {
    private int mPage;
    private ListView listView;

    public static ObjectFragment newInstance(int page) {
        ObjectFragment objectFragment = new ObjectFragment();
        Bundle args = new Bundle();
        args.putInt("page", page);
        objectFragment.setArguments(args);
        return objectFragment;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mPage = getArguments().getInt("page");
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.fragment_collection_object, container, false);
        ((MainActivity) getActivity()).addDataUpdateListener(this);
        listView = (ListView) rootView;
        
        StableArrayAdapter adapter = new StableArrayAdapter((MainActivity) getActivity(), R.layout.list_view_item, ((MainActivity) getActivity()).getData(mPage));
        listView.setAdapter(adapter);

        return rootView;
    }

    @Override
    public void onDestroyView() {
        ((MainActivity) getActivity()).removeDataUpdateListener(this);
        super.onDestroyView();
    }

    //DataUpdateListener interface methods
    @Override
    public int getPage() {
        return mPage;
    }

    @Override
    public void onDataUpdated(ArrayList<String> data) {
        StableArrayAdapter adapter = new StableArrayAdapter((MainActivity) getActivity(), R.layout.list_view_item, data);
        listView.setAdapter(adapter);
    }
}

当需要更新片段(列表视图)的内容时,使用参数中接收到的新内容调用方法onDataUpdated。

编辑

使用 Budius 中的代码示例添加生成崩溃的适配器:

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

        public void setData(List<String> objects){
            mIdMap.clear();
            for (int i = 0; i < objects.size(); ++i) {
                mIdMap.put(objects.get(i), i);
            }
        }

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

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

编辑 2

尝试使用 Budius 解决方案的新适配器。但它不起作用。内容未刷新。

    private static class StableArrayAdapter extends ArrayAdapter<String> {
    List<String> objects;

    public StableArrayAdapter(Context context, int textViewResourceId, List<String> objects) {
        super(context, textViewResourceId, objects);
        this.objects = new ArrayList<>();
        this.objects.addAll(objects);
    }

    public void setData(List<String> objects){
        this.objects.clear();
        this.objects.addAll(objects);
    }

    @Override
    public long getItemId(int position) {
        return Math.abs(objects.get(position).hashCode());
    }

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

调用 setAdapter(adapter) 并将列表 return 调到顶部是正常的 "expected" 行为。毕竟是新适配器,框架不应该假设新适配器与旧适配器有任何关系。

这么说,你的解决方案应该还是围绕notifyDataSetChanged()stableIds的适当用法。考虑到这一点,您应该做的只是简单地切换 ArrayList<String> data 而不是创建一个新的。像这样:

@Override
public void onDataUpdated(ArrayList<String> data) {
    adapter.setData(data); // internally replaces the old data with this new one
    adapter.notifyDataSetChanged();
}

然后在您实现 StableArrayAdapter 时,您必须将 hasStableIds 覆盖为 return true 并将 getItemId 覆盖为 return a meaningfull 值。考虑到您的数据只是字符串,这有点棘手,但像 Math.abs(value.hashCode) 应该足够了。

PS.: 我强烈建议使用 RecyclerView 而不是 ListView。我个人不明白为什么 Google 还没有弃用 ListView。

编辑:

根据评论,有些像这样:

public class StableArrayAdapter extends /* whatever you're extending */ {

    ... your code as usual

   @Override public boolean hasStableIds() { return true; }
   @Override public long getItemId(int position) {
       return Math.abs(data.get(position).hashCode());
   }
}

您告诉系统 ID 与您获得的数据直接相关,并且您 returning ID 连接到您的数据(即使连接松散)。也就是说,一个数据一个ID,另一个数据一个ID。

这样通知 ListView 知道根据 ID 保持位置。

编辑 2:

我发现了你的错误。您的适配器扩展自 ArrayAdapter。这在内部已经有一个 List<String>,当您调用 getItem(position); 时,数据来自该内部列表。

修复很简单。

  • 删除 List<String> objects; 并使用内部列表。

要更新您从 ArrayAdapter (https://developer.android.com/reference/android/widget/ArrayAdapter.html) 调用 addaddAllclear 方法的数据:

类似于:

@Override
public void onDataUpdated(ArrayList<String> data) {
    adapter.clear();
    adapter.addAll(data);
    adapter.notifyDataSetChanged();
}