1

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;
    }
}
Community
  • 1
  • 1
NullPointerException
  • 36,107
  • 79
  • 222
  • 382

1 Answers1

1

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.

kris larson
  • 30,387
  • 5
  • 62
  • 74
  • "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." I'm totally confused. ¿telling the viewpager where to put the page? If you can show me some sample of code i will understand it, but now i'm completly confused with your answer sorry. I just want to update the content of the fragments of my viewpager. Just that. Not to move pages.. i dont understand you – NullPointerException Aug 12 '16 at 11:47
  • Updated my answer. Sorry for the confusion. Don't call `notifyDataSetChanged` on the `PagerAdapter`, do it on the adapter inside the fragment you want to update. – kris larson Aug 12 '16 at 13:08
  • mmmm "Call notifyDataSetChanged() on the adapter inside the fragment you want to update". OK that sounds nice but... how i get inside the fragment i want to update? – NullPointerException Aug 12 '16 at 16:14
  • Also, notifyDataSetChanged() is from the adapter class, cannot be called from a fragment... so i think i missunderstand you in something – NullPointerException Aug 12 '16 at 16:23
  • You said your page has a lot of content and a vertical scroll, so I took that to mean a list view of some sort with an adapter. Please post the code from your fragment. The fragment is probably just checking for data in `onCreateView`. There should be a way to update the data without recreating the entire fragment view. – kris larson Aug 12 '16 at 16:38
  • Posted, check the bottom of my question. Is a very simple fragment, just a textview. You can see the java code and the xml code of that fragment. The textview is inside a scrollview because the text is very long. – NullPointerException Aug 12 '16 at 16:50
  • Okay, great. So now I can see that the data is set in the argument to the fragment. When the data updates, does the size of `children` or `infoTitlesArray` ever change? – kris larson Aug 12 '16 at 17:12
  • no, the size of that array is allways the same, the only thing that changes is the content of the 11 elements of the array – NullPointerException Aug 12 '16 at 17:33
  • I am going to work on a sample application for you and post it on GitHub. – kris larson Aug 12 '16 at 18:07
  • Can I assume that you are only interested in saving the scroll position if the data doesn't change, and if the data does change then it's okay if the scroll goes back to the top? – kris larson Aug 12 '16 at 21:24
  • No, the scroll should mantain his position in both cases, with data changed and with data mantained. Also there is another abnormal behaviour: if you are swipping between pages when the notifyDataSet function is called, then you loss your swip movement. It's similar to the scroll problem. – NullPointerException Aug 12 '16 at 22:30
  • did you have something more? if you want you put here instead of creating a github. – NullPointerException Aug 13 '16 at 18:27
  • Thank you so much! it seems to work in you sample. I pareciate a lot your effort and i'm accepting your answer (i will try to add your strategy to my code today at night). Only one think more. Your solution works perfectly and code seems very clean, but it is a little "hack" strategy which I'm not sure if it is the best way to achieve this. Are there more ways to achieve this great behaviour of updating content of fragments inside a viewpager without breaking scroll and swipe movements? Or this is the only way and the best way to achieve this? – NullPointerException Aug 14 '16 at 11:11
  • One question. As you are storing the scroll in the onsavedinstance method and recovering it in the oncreateview, I think that the fragment is being destroyed and recreated... but.. why? I dont see any code for sending a destroy signal to the fragment. The onDataUpdated method only changes the TextView but not destroys and recreates the fragment. – NullPointerException Aug 14 '16 at 16:51
  • The way `ViewPager` works is that it creates and destroys views as you swipe so that there are only three views: the one you see, the one to the left, and the one to the right. (with `setOffscreenPageLimit(1)`, default.) When you swipe from, say, page 2 to page 3, the view on page 1 is destroyed and the view on page 4 is created. That's why you have to save the scroll position, so when you scroll back to page 1 the `ScrollView` is in the same place. You can see this in the source code for `FragmentSatePagerAdapter`. – kris larson Aug 14 '16 at 17:13
  • And because the views for the fragments are getting created and destroyed all the time as the user is swiping, the best way to handle frequent data updates is a publish/subscribe strategy where the fragments are subscribing to & unsubscribing from the activity as their views are being created & destroyed. Then when the activity gets a data change, it publishes the change to all the fragments that are subscribed at that time. So while the code itself may be a little hacky, it shows the correct design principles. – kris larson Aug 14 '16 at 17:30
  • Wonderful explanation and sample code. Thank you so much! – NullPointerException Aug 14 '16 at 18:46