ViewPager as a circular queue / wrapping

AndroidAndroid FragmentsAndroid Viewpager

Android Problem Overview


I am using a ViewPager with the FragmentStatePagerAdapter to allow navigation between some fragments.

Let's say I have three fragments: A, B and C. The ViewPager shows Fragment A initially, and allows you to navigate to Fragment B by swiping from right-to-left, and then to Fragment C by swiping again. This allows the following navigation paths: A <--> B <--> C.

What I would like is to be able to swipe from left-to-right on Fragment A and have the ViewPager show Fragment C, i.e. for it to behave as a circular queue and allow ... --> C <--> A <--> B <--> C <--> A <-- ...

I do not want the Fragments duplicated in other positions (i.e. ending up with more than three instances).

Is this wrapping functionality possible with a ViewPager?

Android Solutions


Solution 1 - Android

I've implemented a ViewPager/PagerAdapter that can allow for pseudo-infinite paging behaviour. It works by specifying a very large number as the actual count, but maps them to the actual range of the dataset/pageset. It offsets the beginning by a large number also so that you can immediately scroll to the left from the 'first' page.

It doesn't work so well once you get to 1,000,000th page (you will see graphical glitches when scrolling), but this is typically not a real-world use-case. I could fix this by resetting the count to a lower number once in a while, but I will leave it how it is for now.

The InfinitePagerAdapter wraps an existing ViewPager, and so the usage is quite transparent. The InfiniteViewPager does does a bit of work to make sure you can potentially scroll to the left and right many times.

https://github.com/antonyt/InfiniteViewPager

Solution 2 - Android

This is a solution without fake pages and works like a charm:

public class CircularViewPagerHandler implements ViewPager.OnPageChangeListener {
    private ViewPager   mViewPager;
    private int         mCurrentPosition;
    private int         mScrollState;

    public CircularViewPagerHandler(final ViewPager viewPager) {
        mViewPager = viewPager;
    }

    @Override
    public void onPageSelected(final int position) {
        mCurrentPosition = position;
    }

    @Override
    public void onPageScrollStateChanged(final int state) {
        handleScrollState(state);
        mScrollState = state;
    }

    private void handleScrollState(final int state) {
        if (state == ViewPager.SCROLL_STATE_IDLE) {
            setNextItemIfNeeded();
        }
    }

    private void setNextItemIfNeeded() {
        if (!isScrollStateSettling()) {
            handleSetNextItem();
        }
    }

    private boolean isScrollStateSettling() {
        return mScrollState == ViewPager.SCROLL_STATE_SETTLING;
    }

    private void handleSetNextItem() {
        final int lastPosition = mViewPager.getAdapter().getCount() - 1;
        if(mCurrentPosition == 0) {
            mViewPager.setCurrentItem(lastPosition, false);
        } else if(mCurrentPosition == lastPosition) {
            mViewPager.setCurrentItem(0, false);
        }
    }

    @Override
    public void onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels) {
    }
}

You just have to set it to your ViewPager as onPageChangeListener and that's it: ( ** deprecated now ** check the edit notes)

viewPager.setOnPageChangeListener(new CircularViewPagerHandler(viewPager));

To avoid having this blue shine at the "end" of your ViewPager you should apply this line to your xml where the ViewPager is placed:

android:overScrollMode="never"

We have improved the solution above and created a little library on github. Feel free to check it out :)

Edit:: As per the @Bhavana comment , Just use addOnPageChangeListener instead of setOnPageChangeListener as later is deprecated.

 viewPager.addOnPageChangeListener(new CircularViewPagerHandler(viewPager));

Solution 3 - Android

I've been trying all the suggestions, solutions, libraries, etc but they're not pure circular and most of the time don't have support for only 3 pages.

So I implemented a circular ViewPager example using the new ViewPager2, the new ViewPager uses a RecyclerView and ViewHolders to handle the views recycling and works as expected!

TLDR: GITHUB

In this example, will be building a single activity app with ViewPager2 and a FragmentPagerAdapter supporting circular navigation between 3 pages or more.

I'm using an alpha version of the library androidx.viewpager2:viewpager2, but the version 1.0.0-alpha06 is the last one planned before google freezing the API and moving to beta.

enter image description here

1. Add the ViewPager2 library to the dependencies in your build.gradle

dependencies {
    implementation 'androidx.viewpager2:viewpager2:1.0.0-alpha06'
}

2. Add the ViewPager2 view to your project:

<androidx.viewpager2.widget.ViewPager2 xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/vwpHome"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

3. Create the FragmentStateAdapter adapter:

getItemCount() needs to returns a huuuuge number. (2147483647)

getCenterPage() returns the central page based on the huuuuge number. This method is used to get the position of the initial page to set in the ViewPager2, in this case the user needs to swipe ˜1073741823 time to reach the end of the ViewPager2.

class CircularPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : FragmentStateAdapter(fragmentManager, lifecycle) {

    override fun getItemCount() = Integer.MAX_VALUE

    /**
     * Create the fragment based on the position
     */
    override fun createFragment(position: Int) = HomePagerScreens.values()[position % HomePagerScreens.values().size].fragment.java.newInstance()

    /**
     * Returns the same id for the same Fragment.
     */
    override fun getItemId(position: Int): Long = (position % HomePagerScreens.values().size).toLong()

    fun getCenterPage(position: Int = 0) = Integer.MAX_VALUE / 2 + position
}

HomeScreens is a ENUM with the page info.

enum class HomePagerScreens(@StringRes val title: Int,
                            val fragment: KClass<out Fragment>) {

    HOME_1(R.string.home_1, FragmentHome::class),
    HOME_2(R.string.home_2, FragmentHome::class),
    HOME_3(R.string.home_3, FragmentHome::class)
}

4. Set the adapter to the ViewPager

val circularAdapter = CircularPagerAdapter(supportFragmentManager, lifecycle)
vwpHome.apply {
    adapter = circularAdapter
    setCurrentItem(circularAdapter.getCenterPage(), false)
}

Solution 4 - Android

Just now saw this question and found a solution. Solution Link

Make the following changes to your onPageScrollStateChanged function. Consider you have 4 tabs.

private int previousState, currentState;

public void onPageScrollStateChanged(int state) {              
			
			int currentPage = viewPager.getCurrentItem();       //ViewPager Type
			if (currentPage == 3 || currentPage == 0){
				previousState = currentState;
				currentState = state;
				if (previousState == 1 && currentState == 0){
					
					viewPager.setCurrentItem(currentPage == 0 ? 3 : 0);
					
				}
				
			}
			
		}

If the state variable is in the existing tabs, its value is 1.

It becomes 0 once you try to navigate before your first tab or after your last tab. So, compare the previous and current states, as shown in the code and you are done.

See onPageScrollStateChanged function here.

Hope it helps.

Solution 5 - Android

Another way to do this is to add a fake copy of the first element in the view pager to the end of your adapter, and a fake copy of the last element to the beginning. Then when you are viewing the first/last element in the view pager, change pages without animating.

So basically, the first/last elements in your view pager are fakes, just there to produce the correct animation, and then switch over to the end/beginning. Example:

list.add(list.get(0)); //add the first item again as the last, to create the wrap effect
list.add(0, list.get(list.size()-2)); //insert a copy of the last item as the new first item, so we can scroll backwards too

viewPager.setAdapter(list);
viewPager.setCurrentItem(1, false); //default to the second element (because the first is now a fake)

viewPager.setOnPageChangeListener(new OnPageChangeListener() {
private void handleScrollState(final int state) {
        if (state == ViewPager.SCROLL_STATE_IDLE) { //this is triggered when the switch to a new page is complete
            final int lastPosition = viewPager.getAdapter().getCount() - 1;
            if (currentPosition == lastPosition) {
                viewPager.setCurrentItem(1, false); //false so we don't animate
            } else if (currentPosition == 0) {
                viewPager.setCurrentItem(lastPosition -1, false);
            }
        }
        
}

Solution 6 - Android

This almost accomplishes what antonyt wants, it wont let you go from A -> C though. (Not tested vigorously, but seems to work well enough)

public class MyAdapter extends PagerAdapter {
	private List<Object> mItems;
	private int mFakeCount = 0;

	public MyAdapter(Context context, List<Object> items) {
		mItems = items;
		mFakeCount = mItems.size()+1;
	}

	@Override
	public int getCount() {
		return mFakeCount;
	}

	@Override
	public Object instantiateItem(View collection, int position) {
		// make the size larger, and change the position
		// to trick viewpager into paging forever
		if (position >= mItems.size()-1) {
			int newPosition = position%mItems.size();

			position = newPosition;
			mFakeCount++;
		}

		// do your layout inflating and what not here.
		// position will now refer to a correct index into your mItems list
		// even if the user has paged so many times that the view has wrapped
	}
}

Solution 7 - Android

Simple idea how to achieve this. When you have your Array with pages simply add this array to another array exactly in the middle.

For example you have array with 15 elements. So place it into 400 element array in the middle (200 position)

Next simply fill array to the left and right so you can move around.

Sample image:

enter image description here

How it looks?

https://youtu.be/7up36ylTzXk

Let the code talk :)

https://gist.github.com/gelldur/051394d10f6f98e2c13d

public class CyclicPagesAdapter extends FragmentStatePagerAdapter {

	public CyclicPagesAdapter(FragmentManager fm) {
		super(fm);
	}

	@Override
	public Fragment getItem(int position) {
		return _fragments.get(position).createInstance();
	}

	@Override
	public int getCount() {
		return _fragments.size();
	}

	public void addFragment(FragmentCreator creator) {
		_fragments.add(creator);
	}

	public interface FragmentCreator {
		Fragment createInstance();
	}

	public void clear() {
		_fragments.clear();
	}

	public void add(FragmentCreator fragmentCreator) {
		_fragments.add(fragmentCreator);
	}

	public void finishAdding() {
		ArrayList<FragmentCreator> arrayList = new ArrayList<>(Arrays.asList(new FragmentCreator[MAX_PAGES]));
		arrayList.addAll(MAX_PAGES / 2, _fragments);

		int mrPointer = _fragments.size() - 1;
		for (int i = MAX_PAGES / 2 - 1; i > -1; --i) {
			arrayList.set(i, _fragments.get(mrPointer));
			--mrPointer;
			if (mrPointer < 0) {
				mrPointer = _fragments.size() - 1;
			}
		}

		mrPointer = 0;
		for (int i = MAX_PAGES / 2 + _fragments.size(); i < arrayList.size(); ++i) {
			arrayList.set(i, _fragments.get(mrPointer));
			++mrPointer;
			if (mrPointer >= _fragments.size()) {
				mrPointer = 0;
			}
		}
		_fragmentsRaw = _fragments;
		_fragments = arrayList;
	}

	public int getStartIndex() {
		return MAX_PAGES / 2;
	}

	public int getRealPagesCount() {
		return _fragmentsRaw.size();
	}

	public int getRealPagePosition(int index) {
		final FragmentCreator fragmentCreator = _fragments.get(index);
		for (int i = 0; i < _fragmentsRaw.size(); ++i) {
			if (fragmentCreator == _fragmentsRaw.get(i)) {
				return i;
			}
		}
		//Fail...
		return 0;
	}

	private static final int MAX_PAGES = 500;
	public ArrayList<FragmentCreator> _fragments = new ArrayList<>();
	public ArrayList<FragmentCreator> _fragmentsRaw;
}

Solution 8 - Android

I have a solution I believe will work. It isn't tested, but here it is:

A wrapper around FragmentPagerAdapter as the super class:

public abstract class AFlexibleFragmentPagerAdapter extends
		FragmentPagerAdapter
{
	public AFlexibleFragmentPagerAdapter(FragmentManager fm)
	{
		super(fm);
		// TODO Auto-generated constructor stub
	}

	public abstract int getRealCount();

	public abstract int getStartPosition();

	public abstract int transformPosition(int position);
};

Then the standard FragmentPagerAdapter implementation subclass this new abstract class:

public class SomeFragmentPagerAdapter extends
		AFlexibleFragmentPagerAdapter
{
	
	private Collection<Fragment> someContainer;
	
	public SomeFragmentPagerAdapter(FragmentManager fm, Container<Fragment> fs)
	{
		super(fm);
		someContainer = fs;
	}

	@Override
	public int getRealCount() { return someContainer.size(); }

	@Override
	public int getStartPosition() { return 0; }

	@Override
	public int transformPosition(int position) { return position; }
};

And the Cyclic Pager Adapter implementation.

public class SomeCyclicFragmentPagerAdapter extends
		SomeFragmentPagerAdapter 
{
	private int realCount = 0;
    private int dummyCount = 0;
    private static final int MULTI = 3;

	public SomeFragmentPagerAdapter(FragmentManager fm, ...)
	{
		super(fm);
		realCount = super.getCount();
		dummyCount *= MULTI;
	}

	@Override
	public int getCount() { return dummyCount; }

	@Override
	public int getRealCount() { return realCount; }

	@Override
	public int getStartPosition() { return realCount; }

	@Override
	public int transformPosition(int position)
	{
		if (position < realCount) position += realCount;
		else if (position >= (2 * realCount)) position -= realCount;
		
		return position;
	}
};

ViewPager initialization

	this.mViewPager.setCurrentItem(this.mPagerAdapter.getStartPosition());

And finally, the callback implementation:

	@Override
	public void onPageSelected(int position)
	{
		this.mTabHost.setCurrentTab(position % mPagerAdapter.getRealCount());
		this.mViewPager.setCurrentItem(this.mPagerAdapter
				.transformPosition(position));
	}

	@Override
	public void onTabChanged(String tabId)
	{
		int pos = this.mTabHost.getCurrentTab();

		this.mViewPager.setCurrentItem(this.mPagerAdapter
				.transformPosition(pos));
	}

Solution 9 - Android

  1. Set for the items count some large value, say 500000.

  2. Return from adapter this value * number of actual items.

  3. On getItem(int i) adjust the i to -> i = i % number of actual items.

  4. On onCreate(Bundle savedInstanceState) set current item for page 500000/2

     public class MainActivity extends FragmentActivity {
     
         private final static int ITEMS_MAX_VALUE = 500000;
     	private int actualNumCount = 10;
     	private ViewPager mPager = null;
     
         private class OptionPageAdapter extends FragmentStatePagerAdapter {
             public OptionPageAdapter(FragmentManager fm) {
                 super(fm);
             }
     
             @Override
             public Fragment getItem(int i) {
                 try {
                     i = i % OptionType.values().length;
                     OptionPage optionPage = new OptionPage(i);
     				optionPage.id = i;
                     return optionPage;
                 } catch (Exception e) {
                     VayoLog.log(Level.WARNING, "Unable to create OptionFragment for id: " + i, e);
                 }
                 return null;
             }
     
             @Override
             public int getCount() {
                 return ITEMS_MAX_VALUE * actualNumCount;
             }
         }
     
         public static class OptionPage extends Fragment {
             private int id = 0
             @Nullable
             @Override
             public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
                 View mainView = inflater.inflate(R.layout.main_page, container, false);
                 return mainView;
             }
     
         }
     
         @Override
         public void onCreate(Bundle savedInstanceState) {
             super.onCreate(savedInstanceState);
             setContentView(R.layout.main);
             mPager = (ViewPager) findViewById(R.id.main_pager);
             mPager.setOffscreenPageLimit(3);
             mPager.setAdapter(new OptionPageAdapter(this.getSupportFragmentManager()));
             mPager.setCurrentItem(ITEMS_MAX_VALUE/2,false);
     
         }
     
     }
    

Solution 10 - Android

  1. add fake copy of the Last Element at the begining of your list.

  2. add fake copy of the First Element at the end.

  3. select real First Element somewhere in init code:

     	mViewPager.setCurrentItem(1);
    
  4. modify setOnPageChangeListener:

     	@Override
     	public void onPageSelected(int state) {
     		if (state == ITEMS_NUMBER + 1) {
     			mViewPager.setCurrentItem(1, false);
     		} else if (state == 0) {
     			mViewPager.setCurrentItem(ITEMS_NUMBER, false);
     		}
     	}
    

Solution 11 - Android

based on this approach

true loop, pager title strip, true no refresh scrolling

private static final int ITEM_NUM = 5 ; // 5 for smooth pager title strip
private void addFragment(String title, String className) {
	if (fragments.size() < ITEM_NUM) {
		Bundle b = new Bundle();
		b.putInt("pos", fragments.size());
		fragments.add(Fragment.instantiate(this, className, b));

	}
	titles.add(title);
	itemList.add(className);
}

view page adapter:

public static class MyFragmentPageAdapter extends FragmentPagerAdapter {

	private HashMap<Long, Fragment> mItems = new HashMap<Long, Fragment>();

	public MyFragmentPageAdapter(FragmentManager fragmentManager) {
		super(fragmentManager);
	}

	@Override
	public long getItemId(int position) {
		int id = java.lang.System.identityHashCode(fragments.get(position));
		return id;
	}

	@Override
	public Fragment getItem(int position) {
		long id = getItemId(position);
		if (mItems.get(id) != null) {
			return mItems.get(id);
		}
		Fragment f = fragments.get(position);
		return f;
	}

	@Override
	public int getCount() {
		return ITEM_NUM;
	}

	@Override
	public CharSequence getPageTitle(int position) {
		String t = titles.get(getItem(position).getArguments()
				.getInt("pos"));
		return t;
	}

	@Override
	public int getItemPosition(Object object) {
		for (int i = 0; i < ITEM_NUM; i++) {
			Fragment item = (Fragment) fragments.get(i);
			if (item.equals((Fragment) object)) {
				return i;
			}
		}
		return POSITION_NONE;
	}
}

using:

	itemList = new ArrayList<String>();
	fragments = new ArrayList<Fragment>();
	titles = new ArrayList<String>();

	addFragment("Title1", Fragment1.class.getName());
	...
	addFragment("TitleN", FragmentN.class.getName());

	mAdapter = new MyFragmentPageAdapter(getSupportFragmentManager());
	mViewPager = (ViewPager) findViewById(R.id...);

	mViewPager.setAdapter(mAdapter);
	mViewPager.setCurrentItem(2);
	currPage = 2;
	visibleFragmentPos = 2;

	mViewPager.setOnPageChangeListener(new OnPageChangeListener() {

		@Override
		public void onPageSelected(int curr) {
			dir = curr - currPage;
			currPage = curr;
		}

		private void shiftRight() {
			fragments.remove(0);
			int pos = visibleFragmentPos + 3;
			if (pos > itemList.size() - 1)
				pos -= itemList.size();
			if (++visibleFragmentPos > itemList.size() - 1)
				visibleFragmentPos = 0;
			insertItem(4, pos);

		}

		private void shiftLeft() {
			fragments.remove(4);
			int pos = visibleFragmentPos - 3;
			if (pos < 0)
				pos += itemList.size();
			if (--visibleFragmentPos < 0)
				visibleFragmentPos = itemList.size() - 1;
			insertItem(0, pos);
		}

		private void insertItem(int datasetPos, int listPos) {
			Bundle b = new Bundle();
			b.putInt("pos", listPos);
			fragments.add(datasetPos,
					Fragment.instantiate(ctx, itemList.get(listPos), b));
		}

		@Override
		public void onPageScrollStateChanged(int state) {

			if (state == ViewPager.SCROLL_STATE_IDLE) {
				if (dir == 1) {
					shiftRight();
				} else if (dir == -1) {
					shiftLeft();

				}
				mAdapter.notifyDataSetChanged();
				currPage = 2;
			}
		}
	});

Solution 12 - Android

You may use simple view from https://github.com/pozitiffcat/cyclicview
One of features is:

> Do not duplicate first and last views, used bitmap variants instead

Example:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        CyclicView cyclicView = (CyclicView) findViewById(R.id.cyclic_view);
        cyclicView.setAdapter(new CyclicAdapter() {
            @Override
            public int getItemsCount() {
                return 10;
            }

            @Override
            public View createView(int position) {
                TextView textView = new TextView(MainActivity.this);
                textView.setText(String.format("TextView #%d", position + 1));
                return textView;
            }

            @Override
            public void removeView(int position, View view) {
                // Do nothing
            }
        });
    }
}

Solution 13 - Android

Sorry for the delay, but I've seen the answers and I I was unable to hold, got to answer it too :).

On your adapter, on the method get count, proceed as follows:

 @Override
    public int getCount() {
        return myList.size() % Integer.MAX_VALUE; 
  }

That will work!

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionantonytView Question on Stackoverflow
Solution 1 - AndroidantonytView Answer on Stackoverflow
Solution 2 - AndroidKing of MassesView Answer on Stackoverflow
Solution 3 - AndroidextmkvView Answer on Stackoverflow
Solution 4 - AndroidBhaskerView Answer on Stackoverflow
Solution 5 - AndroidGraydyn YoungView Answer on Stackoverflow
Solution 6 - AndroidNPikeView Answer on Stackoverflow
Solution 7 - AndroidGelldurView Answer on Stackoverflow
Solution 8 - AndroidJohnView Answer on Stackoverflow
Solution 9 - AndroidYanView Answer on Stackoverflow
Solution 10 - AndroidAlykView Answer on Stackoverflow
Solution 11 - AndroidAlykView Answer on Stackoverflow
Solution 12 - AndroidАлексей МальченкоView Answer on Stackoverflow
Solution 13 - AndroidJasJarView Answer on Stackoverflow