ViewPager as a circular queue / wrapping

2019-01-02 15:23发布

问题:

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?

回答1:

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



回答2:

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


回答3:

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


回答4:

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.



回答5:

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

}


回答6:

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:

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


回答7:

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


回答8:

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!



回答9:

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


回答10:

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


回答11:

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


回答12:

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


标签: