143 votes

Supprimer la page de fragments du ViewPager dans Android

J'essaie d'ajouter et de retirer dynamiquement des fragments d'un ViewPager. L'ajout fonctionne sans problème, mais le retrait ne fonctionne pas comme prévu.

Chaque fois que je veux supprimer l'élément actuel, le dernier est supprimé.

J'ai également essayé d'utiliser un FragmentStatePagerAdapter ou de renvoyer POSITION_NONE dans la méthode getItemPosition de l'adaptateur.

Qu'est-ce que je fais de mal ?

Voici un exemple de base :

MainActivity.java

public class MainActivity extends FragmentActivity implements TextProvider {

    private Button mAdd;
    private Button mRemove;
    private ViewPager mPager;

    private MyPagerAdapter mAdapter;

    private ArrayList<String> mEntries = new ArrayList<String>();

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        mEntries.add("pos 1");
        mEntries.add("pos 2");
        mEntries.add("pos 3");
        mEntries.add("pos 4");
        mEntries.add("pos 5");

        mAdd = (Button) findViewById(R.id.add);
        mRemove = (Button) findViewById(R.id.remove);
        mPager = (ViewPager) findViewById(R.id.pager);

        mAdd.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                addNewItem();
            }
        });

        mRemove.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                removeCurrentItem();
            }
        });

        mAdapter = new MyPagerAdapter(this.getSupportFragmentManager(), this);

        mPager.setAdapter(mAdapter);

    }

    private void addNewItem() {
        mEntries.add("new item");
        mAdapter.notifyDataSetChanged();
    }

    private void removeCurrentItem() {
        int position = mPager.getCurrentItem();
        mEntries.remove(position);
        mAdapter.notifyDataSetChanged();
    }

    @Override
    public String getTextForPosition(int position) {
        return mEntries.get(position);
    }
    @Override
    public int getCount() {
        return mEntries.size();
    }

    private class MyPagerAdapter extends FragmentPagerAdapter {

        private TextProvider mProvider;

        public MyPagerAdapter(FragmentManager fm, TextProvider provider) {
            super(fm);
            this.mProvider = provider;
        }

        @Override
        public Fragment getItem(int position) {
            return MyFragment.newInstance(mProvider.getTextForPosition(position));
        }

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

    }

}

TextProvider.java

public interface TextProvider {
    public String getTextForPosition(int position);
    public int getCount();
}

MonFragment.java

public class MyFragment extends Fragment {

    private String mText;

    public static MyFragment newInstance(String text) {
        MyFragment f = new MyFragment(text);
        return f;
    }

    public MyFragment() {
    }

    public MyFragment(String text) {
        this.mText = text;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {

        View root = inflater.inflate(R.layout.fragment, container, false);

        ((TextView) root.findViewById(R.id.position)).setText(mText);

        return root;
    }

}

activité_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <Button
        android:id="@+id/add"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="add new item" />

    <Button
        android:id="@+id/remove"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="remove current item" />

    <android.support.v4.view.ViewPager
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="0dip"
        android:layout_weight="1" />

</LinearLayout>

fragment.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/position"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:textSize="35sp" />

</LinearLayout>

2 votes

+1 pour le code source

0 votes

Est-il correct d'utiliser un constructeur autre que celui par défaut dans MyFragment.java ? ?

294voto

Louth Points 2943

Le ViewPager ne supprime pas vos fragments avec le code ci-dessus car il charge plusieurs vues (ou fragments dans votre cas) en mémoire. En plus de la vue visible, il charge également la vue située de chaque côté de la vue visible. Cela permet le défilement fluide d'une vue à l'autre qui rend le ViewPager si cool.

Pour obtenir l'effet que vous souhaitez, vous devez faire deux choses.

  1. Changez le FragmentPagerAdapter en un FragmentStatePagerAdapter. La raison en est que le FragmentPagerAdapter gardera toutes les vues qu'il charge en mémoire pour toujours. Alors que le FragmentStatePagerAdapter se débarrasse des vues qui ne font pas partie des vues actuelles et traversables.

  2. Remplacez la méthode de l'adaptateur getItemPosition (illustrée ci-dessous). Lorsque nous appelons mAdapter.notifyDataSetChanged(); le ViewPager interroge l'adaptateur pour déterminer ce qui a changé en termes de positionnement. Nous utilisons cette méthode pour dire que tout a changé, alors retraitez tous vos positionnements de vues.

Et voici le code...

private class MyPagerAdapter extends FragmentStatePagerAdapter {

    //... your existing code

    @Override
    public int getItemPosition(Object object){
        return PagerAdapter.POSITION_NONE;
    }

}

1 votes

Ok, celui-ci semble être la solution la plus simple pour résoudre ce problème. Bien que quelques autres solutions de contournement soient discutées dans les rapports de bogue ici : code.google.com/p/Android/issues/detail?id=19110 et ici : code.google.fr/p/Android/issues/detail?id=19001

0 votes

@Louth : J'utilise ViewPager avec TabsAdapter, comme étendu à partir de FragmentStatePagerAdapter (selon l'exemple sur la page de référence Google : developer.Android.com/reference/Android/support/v4/view/ ). Ma question suivante est la suivante : comment supprimer un onglet en premier lieu ? Merci.

4 votes

@Louth Lorsque nous appelons notifyDataSetChanged(), le viewpager crée de nouveaux fragments, mais les anciens sont toujours en mémoire. Et mon problème est qu'ils reçoivent un appel pour leur événement onOptionsItemSelected. Comment puis-je me débarrasser des anciens fragments ?

102voto

Tim Rae Points 183

La solution proposée par Louth n'a pas suffi à faire fonctionner les choses pour moi, car les fragments existants n'étaient pas détruits. Motivé par cette réponse j'ai découvert que la solution consiste à remplacer la fonction getItemId(int position) méthode de FragmentPagerAdapter pour donner un nouvel identifiant unique chaque fois qu'il y a eu un changement dans la position attendue d'un fragment.

Code source :

private class MyPagerAdapter extends FragmentPagerAdapter {

    private TextProvider mProvider;
    private long baseId = 0;

    public MyPagerAdapter(FragmentManager fm, TextProvider provider) {
        super(fm);
        this.mProvider = provider;
    }

    @Override
    public Fragment getItem(int position) {
        return MyFragment.newInstance(mProvider.getTextForPosition(position));
    }

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

    //this is called when notifyDataSetChanged() is called
    @Override
    public int getItemPosition(Object object) {
        // refresh all fragments when data set changed
        return PagerAdapter.POSITION_NONE;
    }

    @Override
    public long getItemId(int position) {
        // give an ID different from position when position has been changed
        return baseId + position;
    }

    /**
     * Notify that the position of a fragment has been changed.
     * Create a new ID for each position to force recreation of the fragment
     * @param n number of items which have been changed
     */
    public void notifyChangeInPosition(int n) {
        // shift the ID returned by getItemId outside the range of all previous fragments
        baseId += getCount() + n;
    }
}

Maintenant, par exemple, si vous supprimez un seul onglet ou si vous apportez une modification à la commande, vous devez appeler notifyChangeInPosition(1) avant d'appeler notifyDataSetChanged() ce qui garantit que tous les fragments seront recréés.

Pourquoi cette solution fonctionne

Remplacement de getItemPosition() :

Lorsque notifyDataSetChanged() est appelé, l'adaptateur appelle le notifyChanged() de la méthode ViewPager à laquelle il est rattaché. Le site ViewPager puis vérifie la valeur renvoyée par l'adaptateur getItemPosition() pour chaque élément, en supprimant les éléments qui renvoient POSITION_NONE (voir le code source ) et se repeupler ensuite.

Remplacement de getItemId() :

Ceci est nécessaire pour éviter que l'adaptateur ne recharge l'ancien fragment lorsque le fichier ViewPager se repeuple. Vous pouvez facilement comprendre pourquoi cela fonctionne en regardant le code source pour instantiateItem() dans FragmentPagerAdapter .

    final long itemId = getItemId(position);

    // Do we already have this fragment?
    String name = makeFragmentName(container.getId(), itemId);
    Fragment fragment = mFragmentManager.findFragmentByTag(name);
    if (fragment != null) {
        if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
        mCurTransaction.attach(fragment);
    } else {
        fragment = getItem(position);
        if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
        mCurTransaction.add(container.getId(), fragment,
                makeFragmentName(container.getId(), itemId));
    }

Comme vous pouvez le voir, le getItem() n'est appelée que si le gestionnaire de fragments ne trouve aucun fragment existant avec le même Id. Il me semble que le fait que les anciens fragments soient toujours attachés même après l'appel de la méthode notifyDataSetChanged() est appelé, mais la documentation pour ViewPager l'indique clairement :

Notez que cette classe est actuellement en cours de conception et de développement. L'API sera probablement modifiée dans les mises à jour ultérieures de la bibliothèque de compatibilité, ce qui nécessitera des modifications du code source des applications lorsqu'elles seront compilées avec la nouvelle version.

Nous espérons donc que la solution de contournement proposée ici ne sera pas nécessaire dans une future version de la bibliothèque de support.

18voto

Vasil Valchev Points 347

Ma solution de travail pour supprimer la page fragmentée du pager de vue

public class MyFragmentAdapter extends FragmentStatePagerAdapter {

    private ArrayList<ItemFragment> pages;

    public MyFragmentAdapter(FragmentManager fragmentManager, ArrayList<ItemFragment> pages) {
        super(fragmentManager);
        this.pages = pages;
    }

    @Override
    public Fragment getItem(int index) {
        return pages.get(index);
    }

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

    @Override
    public int getItemPosition(Object object) {
        int index = pages.indexOf (object);

        if (index == -1)
            return POSITION_NONE;
        else
            return index;
    }
}

Et quand j'ai besoin de supprimer une page par index, je fais ceci

pages.remove(position); // ArrayList<ItemFragment>
adapter.notifyDataSetChanged(); // MyFragmentAdapter

Voici l'initialisation de mon adaptateur

MyFragmentAdapter adapter = new MyFragmentAdapter(getSupportFragmentManager(), pages);
viewPager.setAdapter(adapter);

11voto

Kathan Shah Points 697

Le fragment doit être déjà supprimé mais le problème était l'état de sauvegarde du viewpager.

Essayez

myViewPager.setSaveFromParentEnabled(false);

Rien ne fonctionnait, mais cette solution a résolu le problème !

A la vôtre !

2voto

Sven Points 27

J'ai eu l'idée de simplement copier le code source de android.support.v4.app.FragmentPagerAdpater dans une classe personnalisée nommée CustumFragmentPagerAdapter . Cela m'a donné l'occasion de modifier le instantiateItem(...) de sorte qu'à chaque fois qu'elle est appelée, elle supprime/détruit le fragment actuellement attaché avant d'ajouter le nouveau fragment reçu de la part de getItem() méthode.

Il suffit de modifier le instantiateItem(...) de la manière suivante :

@Override
public Object instantiateItem(ViewGroup container, int position) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }
    final long itemId = getItemId(position);

    // Do we already have this fragment?
    String name = makeFragmentName(container.getId(), itemId);
    Fragment fragment = mFragmentManager.findFragmentByTag(name);

    // remove / destroy current fragment
    if (fragment != null) {
        mCurTransaction.remove(fragment);
    }

    // get new fragment and add it
    fragment = getItem(position);
    mCurTransaction.add(container.getId(), fragment,    makeFragmentName(container.getId(), itemId));

    if (fragment != mCurrentPrimaryItem) {
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
    }

    return fragment;
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X