Search code examples
androidandroid-viewpagerfragmentpageradapternotifydatasetchangedfragmentstatepageradapter

notifyDataSetChanged() forces scroll up in FragmentStatePagerAdapter


I have a FragmentStatePagerAdapter which is being refreshed once every second with new data for some of his pages.

The problem is that some pages has a lot of content and a vertical scroll, so every second when notifyDataSetChanged() is being called, the scroll is forzed to his upper possition. it is a very abnormal and annoying behaviour.

I find this on stackoverflow: notifyDataSetChanged() makes the list refresh and scroll jumps back to the top

The problem is that these solutions are designed for a normal ViewPager or normal Adapter and can't work with FragmentStatePageAdapter or something, because after trying them i still have the same problem.

This is my adapter:

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

    @Override
    public Fragment getItem(int i) {
        Fragment fragment = new ObjectFragment();
        Bundle args = new Bundle();
        args.putString(ObjectFragment.ARG_TEXT, children[i]);
        fragment.setArguments(args);
        return fragment;
    }

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

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

    @Override
    public int getItemPosition(Object object) {
        return POSITION_NONE;
    }
}

The ViewPager which has the problem:

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

Layout of the fragment:

<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:scrollbarSize="5dip"
    style="@style/CustomScrollBar">
    <TextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="left"
        android:textSize="16sp"
        android:padding="5dp"
        style="@style/CustomTextView"/>
</ScrollView>

The java code for the fragment:

public static class ObjectFragment extends Fragment {
    public static final String ARG_TEXT = "object";

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.fragment_collection_object, container, false);
        Bundle args = getArguments();
        TextView tv = ((TextView) rootView.findViewById(R.id.text));
        tv.setText(Html.fromHtml(args.getString(ARG_TEXT)));
        return rootView;
    }
}

Solution

  • EDIT:

    I've created a demo project. Here are some important pieces.

    1. Use a FragmentStatePagerAdapter subclass.

      We need a FragmentStatePagerAdapter base class in order to save the state of the fragment.

    2. Save the scroll position of the ScrollView in onSaveInstanceState(), and set the scroll position to the saved value when the fragment view is (re)created.

      Now that we are saving/restoring the fragment state, we put the scroll position in that state:

          @Override
          public void onSaveInstanceState(Bundle outState) {
              int scrollY = scrollView.getScrollY();
              outState.putInt("scrollY", scrollY);
              super.onSaveInstanceState(outState);
          }
      

      and restore it in onCreateView():

              if (savedInstanceState != null) {
                  final int scrollY = savedInstanceState.getInt("scrollY");
                  scrollView.post(new Runnable() {
                      @Override
                      public void run() {
                          scrollView.setScrollY(scrollY);
                      }
                  });
              }
      
    3. Set up a listener notification system for updates.

      We have an interface called DataUpdateListener that is implemented by the fragment. The activity provides register/unregister methods:

      public void addDataUpdateListener(DataUpdateListener listener) {
          mListenerMap.put(listener.getPage(), listener);
      }
      
      public void removeDataUpdateListener(DataUpdateListener listener) {
          mListenerMap.remove(listener.getPage());
      }
      

      ... and the fragment registers & unregisters with the activity:

      in onCreateView():

          ((MainActivity) getActivity()).addDataUpdateListener(this);
      

      also

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

      then anytime the data changes, the fragments all get an update notification:

              for (int i = 0; i < children.length; i++) {
                  notifyUpdateListener(i, children[i]);
              }
      

    Note that nowhere in the code is onNotifyDataSetChanged() called on the view pager adapter.

    The demo is on GitHub at https://github.com/klarson2/View-Pager-Data-Update


    This is what is causing the scrolling:

        @Override
        public int getItemPosition(Object object) {
            return POSITION_NONE;
        }
    

    When you call notifyDataSetChanged(), what the ViewPager does is ask you what to do with the pages it already has.

    So it will call getItemPosition to find out: where should this page go? You have three options to respond:

    1. Return an index. So if you return 2 for page 0, then the ViewPager will move the page at 0 to 2.

    2. Return POSITION_UNCHANGED. The page will stay exactly where it is now.

    3. Return POSITION_NONE. This means the page should no longer be displayed.

    Once the ViewPager knows where the pages are moved to, it will call getItem() for any gaps in the pages.

    So if you don't want the scroll to be disturbed, tell the ViewPager where to put the page instead of telling it to get rid of it and create a new one.