Circular ViewPager which uses FragmentPagerAdapter

2020-03-18 04:08发布

I would like to implement a ViewPager which uses Fragments and can be swiped in a curcular motion e.g. Page (A<-->B<-->C<-->A). I have read a couple of posts on how this is done, e.g. returning a fake count of how many elements there are and setting the position at the start in the middle. how to create circular viewpager?

These all seem to be based of a PagerAdapter. When I try to do a similar thing while extending FragmentPagerAdapter, as soon as I return a fakeCount of pages I get an exception when I Swipe through my Fragments, I only have 2 Fragments. Exception: java.lang.IllegalStateException: Can't change tag of fragment.

I think this is caused as the FragmentManager thinks I am in position 2 but position 2 points to the fragment at position 0. Does anyone know how I can avoid this? I am thinking I should experiment with extending Fragmentmanager. Any examples or help with this would be greatly appreciated.

3条回答
叛逆
2楼-- · 2020-03-18 04:24

I know it is a bit late but this is how it worked for me:

I needed a circular swipe between 3 fragments, so I made those 3 and two more virtual to help me implement the page looping:

public static class FirstViewFragment extends Fragment {
    // Empty Constructor
    public FirstViewFragment() {
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_landing_1, container, false);
    }
}

public static class SecondViewFragment extends Fragment {
    // Empty Constructor
    public SecondViewFragment() {
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_landing_2, container, false);
    }
}

public static class ThirdViewFragment extends Fragment {
    // Empty Constructor
    public ThirdViewFragment() {
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_landing_3, container, false);
    }
}

And two more virtual fragments that enabled me to swipe left from the first and right from the last. The first virtual inflates the same layout as the last actual and the last virtual the same layout as the first actual:

public static class StartVirtualFragment extends Fragment {
    public StartVirtualFragment() {}

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_landing_3, container, false);
    }
}

public static class EndVirtualFragment extends Fragment {
    public EndVirtualFragment() {}

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_landing_1, container, false);
    }
}

My Adapter:

private class ViewPagerAdapter extends FragmentPagerAdapter {

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

    @Override
    public Fragment getItem(int i) {
        switch (i) {
            case 0:
                return new StartVirtualFragment();
            case 1:
                if (firstViewFragment == null) {
                    firstViewFragment = new FirstViewFragment();
                }
                return firstViewFragment;
            case 2:
                if (secondViewFragment == null) {
                    secondViewFragment = new SecondViewFragment();
                }
                return secondViewFragment;
            case 3:
                if (thirdViewFragment == null) {
                    thirdViewFragment = new ThirdViewFragment();
                }
                return thirdViewFragment;
            case 4:
                return new EndVirtualFragment();
        }
        return null;
    }

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

And my page listener I used the onPageScrollStateChanged to set the correct page and implement the loop:

viewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        }

        @Override
        public void onPageSelected(int position) {
        }

        @Override
        public void onPageScrollStateChanged(int state) {
            if (state == ViewPager.SCROLL_STATE_DRAGGING) {
                int pageCount = viewPager.getChildCount();
                int currentItem = viewPager.getCurrentItem();
                if (currentItem == 0) {
                    viewPager.setCurrentItem(pageCount - 2, false);
                } else if (currentItem == pageCount - 1) {
                    viewPager.setCurrentItem(1, false);
                }
            }
        }
    });

And in the end:

viewPager.setCurrentItem(1);

Hope I helped

查看更多
forever°为你锁心
3楼-- · 2020-03-18 04:43
**If you want to make 3 views visible at same time and make it circular** 

    public abstract class CircularPagerAdapter extends PagerAdapter{
        private int count;
        int[] pagePositionArray;
        public static final int EXTRA_ITEM_EACH_SIDE = 2;
        private ViewPager.OnPageChangeListener pageChangeListener;
        private ViewPager viewPager;

        public CircularPagerAdapter(final ViewPager pager, int originalCount ) {
            super();
            this.viewPager = pager;
            count = originalCount + 2*EXTRA_ITEM_EACH_SIDE;
            pager.setOffscreenPageLimit(count-2);
            pagePositionArray = new int[count];
            for (int i = 0; i < originalCount; i++) {
                pagePositionArray[i + EXTRA_ITEM_EACH_SIDE] = i;
            }
            pagePositionArray[0] = originalCount - 2;
            pagePositionArray[1] = originalCount -1;
            pagePositionArray[count - 2] = 0;
            pagePositionArray[count - 1] = 1;

            pager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {

                public void onPageSelected(final int position) {
                    if(pageChangeListener != null)
                    {
                        pageChangeListener.onPageSelected(pagePositionArray[position]);
                    }

                    pager.post(new Runnable() {
                        @Override
                        public void run() {
                            if (position == 1){
                                pager.setCurrentItem(count-3,false);
                            } else if (position == count-2){
                                pager.setCurrentItem(2,false);
                            }
                        }
                    });
                }

                public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                    if(pageChangeListener != null)
                    {
                        pageChangeListener.onPageScrolled(pagePositionArray[position],positionOffset,positionOffsetPixels);
                    }
                }

                public void onPageScrollStateChanged(int state) {
                    if(pageChangeListener != null)
                    {
                        pageChangeListener.onPageScrollStateChanged(state);
                    }
                }
            });
        }


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

        @Override
        public boolean isViewFromObject(View view, Object object) {
            return false;
        }

        public abstract Object customInstantiateItem(ViewGroup container, int position);

        public void setPageChangeListener(ViewPager.OnPageChangeListener pageChangeListener)
        {
            this.pageChangeListener = pageChangeListener;
        }

        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            int pageId = pagePositionArray[position];
            return customInstantiateItem(container,pageId);
        }

        @Override
        public void destroyItem(View container, int position, Object object) {
            ((ViewPager) container).removeView((View) object);
        }

        public void setFirstItem()
        {
            viewPager.setCurrentItem(EXTRA_ITEM_EACH_SIDE - 1);
        }
    }
查看更多
够拽才男人
4楼-- · 2020-03-18 04:44

I have a project in the GitHub with some widgets I've created. Here it its:

https://github.com/CyberEagle/AndroidWidgets

In the following package, there are the adapters to be used with the CircularViewPager: https://github.com/CyberEagle/AndroidWidgets/tree/master/src/main/java/br/com/cybereagle/androidwidgets/adapter

First, you will use CircularViewPager instead of ViewPager in your layout. The CircularViewPager is here: https://github.com/CyberEagle/AndroidWidgets/blob/master/src/main/java/br/com/cybereagle/androidwidgets/view/CircularViewPager.java

This ViewPager expects a WrapperCircularPagerAdapter, instead of a PagerAdapter. This wrapper is used to trick the ViewPager, making it to think there are a lot of items in the ViewPager, but it actually repeat your items to make the circular effect. So, instead of implementing either PagerAdapter, FragmentPagerAdapter or FragmentStatePagerAdapter, you will implement either CircularFragmentPagerAdapter, CircularFragmentStatePagerAdapter or CircularPagerAdapter. Then, you will wrap your adapter with the WrapperCircularPagerAdapter and set the wrapper in the CircularViewPager, instead of your adapter. Also, when it's time to notify dataset changed, you will call the notifyDatasetChanged() in the wrapper.

When implementing one of the circular adapter, you will notice that instead of implementing instantiateItem, you will have to implement instantiateVirtualItem. For the fragment's pager adapter, you will implement getVirtualItem instead of getItem. That is because I've created the concept of virtual items.

To make it clear, imagine a view pager with 4 items, giving that each item represents a music. When you go all the way to left, you will see the 4th item in the left of the first. Actually, it's a whole new item, but it's linked to the virtual item that represents the 4th music.

Another example: imagine there's only one music now. You will see the same music on the left and on the right. There're 3 items at a time, but only one virtual item.

So, as explained, the Wrapper is tricking the ViewPager, making it think that there are a lot of items. To make it more difficult for the user to reach one of the ends of the ViewPager (it'd take a long time anyway), everytime a change happens to the dataset, the ViewPager goes to the same virtual item, but to one of the real items near the middle.

One more important thing is that the CircularViewPager has the method setCurrentVirtualItem. This method calculates which real item is the nearest desired virtual item and then it uses the setCurrentItem to set it. You have also the option to use the getCurrentVirtualItem, that will return the index of the current virtual item. Notice that if you use getCurrentItem, you'll get a large index.

Well, this is it. I'm sorry for the lack of documentation of the project. I'm planning document it soon. I'm also planning to remove the need for the wrapper. Feel free to copy the code (respecting the Apache 2.0 license), to fork or even contribute to it.

查看更多
登录 后发表回答