web-dev-qa-db-fra.com

ViewPager en tant que file d'attente circulaire / habillage

J'utilise un ViewPager avec le FragmentStatePagerAdapter pour permettre la navigation entre certains fragments.

Disons que j'ai trois fragments: A, B et C. Le ViewPager affiche initialement le fragment A et vous permet de naviguer vers le fragment B en faisant glisser de droite à gauche, puis vers le fragment C en faisant glisser à nouveau. Cela permet les chemins de navigation suivants: A <--> B <--> C.

Ce que je voudrais, c'est pouvoir balayer de gauche à droite sur le fragment A et que le ViewPager affiche le fragment C, c'est-à-dire pour qu'il se comporte comme une file d'attente circulaire et autorise ... --> C <--> A <--> B <--> C <--> A <-- ...

Je ne veux pas que les fragments soient dupliqués dans d'autres positions (c'est-à-dire se retrouver avec plus de trois instances).

Cette fonctionnalité d'encapsulation est-elle possible avec un ViewPager?

64
antonyt

J'ai implémenté un ViewPager/PagerAdapter qui peut permettre un comportement de pagination pseudo-infini. Il fonctionne en spécifiant un très grand nombre comme nombre réel, mais les mappe à la plage réelle de l'ensemble de données/ensemble de pages. Il décale également le début d'un grand nombre afin que vous puissiez immédiatement faire défiler vers la gauche à partir de la "première" page.

Cela ne fonctionne pas si bien une fois que vous êtes à la 1 000 000e page (vous verrez des problèmes graphiques lors du défilement), mais ce n'est généralement pas un cas d'utilisation réel. Je pourrais résoudre ce problème en réinitialisant le compte à un nombre inférieur de temps en temps, mais je vais le laisser tel qu'il est pour l'instant.

InfinitePagerAdapter encapsule un ViewPager existant, et donc l'utilisation est assez transparente. InfiniteViewPager fait un peu de travail pour vous assurer que vous pouvez potentiellement faire défiler vers la gauche et la droite plusieurs fois.

https://github.com/antonyt/InfiniteViewPager

48
antonyt

Ceci est une solution sans fausses pages et fonctionne comme un charme:

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

Il vous suffit de le définir sur votre ViewPager en tant que onPageChangeListener et c'est tout: (** obsolète maintenant ** vérifiez les notes d'édition)

viewPager.setOnPageChangeListener(new CircularViewPagerHandler(viewPager));

Pour éviter d'avoir cet éclat bleu à la "fin" de votre ViewPager, vous devez appliquer cette ligne à votre xml où le ViewPager est placé:

Android:overScrollMode="never"

Nous avons amélioré la solution ci-dessus et créé une petite bibliothèque sur github . N'hésitez pas à y jeter un œil :)

Edit :: Selon le commentaire @Bhavana, utilisez simplement addOnPageChangeListener au lieu de setOnPageChangeListener car plus tard est obsolète.

 viewPager.addOnPageChangeListener(new CircularViewPagerHandler(viewPager));
16
King of Masses

Je viens de voir cette question et j'ai trouvé une solution. Lien de solution

Apportez les modifications suivantes à votre fonction onPageScrollStateChanged. Considérez que vous avez 4 onglets .

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

                }

            }

        }

Si la variable d'état se trouve dans les onglets existants, sa valeur est 1.

Il devient 0 une fois que vous essayez de naviguer avant votre premier onglet ou après votre dernier onglet. Donc, comparez les états précédent et actuel, comme indiqué dans le code et vous avez terminé.

Voir la fonction onPageScrollStateChanged ici .

J'espère que ça aide.

7
Bhasker

Une autre façon de procéder consiste à ajouter une fausse copie du premier élément du pager de vue à la fin de votre adaptateur et une fausse copie du dernier élément au début. Ensuite, lorsque vous affichez le premier/dernier élément du pager de vue, changez de page sans animation.

Donc, fondamentalement, les premiers/derniers éléments de votre pager de vue sont des faux, juste là pour produire l'animation correcte, puis basculez vers la fin/début. Exemple:

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

}
7
Graydyn Young

Cela accomplit presque ce que veut antonyt, mais cela ne vous laissera pas passer de A -> C. (Non testé vigoureusement, mais semble fonctionner assez bien)

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

J'ai essayé toutes les suggestions, solutions, bibliothèques, etc. mais elles ne sont pas purement circulaires et la plupart du temps ne prennent en charge que 3 pages.

J'ai donc implémenté un exemple circulaire ViewPager en utilisant le nouveau ViewPager2, Le nouveau ViewPager utilise un RecyclerView et ViewHolders pour gérer le recyclage des vues et fonctionne comme prévu!

TLDR: GITHUB

Dans cet exemple, va créer une application d'activité unique avec ViewPager2 Et un FragmentPagerAdapter supportant la navigation circulaire entre 3 pages ou plus.

J'utilise une version alpha de la bibliothèque androidx.viewpager2:viewpager2, Mais la version 1.0.0-alpha06 Est la dernière prévue avant que google ne bloque l'API et passe en version bêta.

enter image description here

1. Ajoutez la bibliothèque ViewPager2 Aux dépendances dans votre build.gradle

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

2. Ajoutez la vue ViewPager2 À votre projet:

<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. Créez l'adaptateur FragmentStateAdapter:

getItemCount() doit retourner un nombre huuuuge. (2147483647)

getCenterPage() renvoie la page centrale en fonction du nombre huuuuge. Cette méthode est utilisée pour obtenir la position de la page initiale à définir dans le ViewPager2, Dans ce cas, l'utilisateur doit glisser ˜1073741823 pour atteindre la fin du 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 est un ENUM avec les informations de la page.

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. Définissez l'adaptateur sur le ViewPager

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

Idée simple pour y parvenir. Lorsque vous avez votre tableau avec des pages, ajoutez simplement ce tableau à un autre tableau exactement au milieu.

Par exemple, vous avez un tableau avec 15 éléments. Placez-le donc dans un tableau de 400 éléments au milieu (position 200)

Ensuite, remplissez simplement le tableau à gauche et à droite pour pouvoir vous déplacer.

Exemple d'image:

enter image description here

À quoi ça ressemble?

https://youtu.be/7up36ylTzXk

Laissez parler le code :)

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;
}
1
Dawid Drozd

J'ai une solution qui, je pense, fonctionnera. Ce n'est pas testé, mais le voici:

Un wrapper autour de FragmentPagerAdapter comme super classe:

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

Ensuite, l'implémentation standard de FragmentPagerAdapter sous-classe cette nouvelle classe abstraite:

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

Et l'implémentation de l'adaptateur Cyclic Pager.

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

Initialisation de ViewPager

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

Et enfin, l'implémentation du rappel:

    @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));
    }
0
user2646772

Vous pouvez utiliser une vue simple de https://github.com/pozitiffcat/cyclicview
L'une des fonctionnalités est:

Ne dupliquez pas les première et dernière vues, utilisez plutôt les variantes bitmap

Exemple:

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
            }
        });
    }
}
  1. La valeur pour les articles compte une valeur élevée, disons 500000.
  2. Renvoyez de l'adaptateur cette valeur * nombre d'articles réels.
  3. Sur getItem (int i), ajustez le i à -> i = i% nombre d'articles réels.
  4. On onCreate (Bundle savedInstanceState) définir l'élément actuel pour la 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);
    
        }
    
    }
    
0
Kvant

Désolé pour le retard, mais j'ai vu les réponses et je n'ai pas pu tenir, j'ai dû y répondre aussi :).

Sur votre adaptateur, sur la méthode get count, procédez comme suit:

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

Ça marchera!

0
JasJar
  1. ajoutez une fausse copie du dernier élément au début de votre liste.
  2. ajoutez une fausse copie du premier élément à la fin.
  3. sélectionnez le véritable premier élément quelque part dans le code init:

        mViewPager.setCurrentItem(1);
    
  4. modifier 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);
            }
        }
    
0
Alyk

basé sur cette approche

boucle vraie, bande de titre du pager, défilement sans rafraîchissement vrai

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

afficher l'adaptateur de page:

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

en utilisant:

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