web-dev-qa-db-fra.com

FragmentPagerAdapter ne supprime pas correctement les éléments (fragments)

J'ai implémenté le FragmentPagerAdapter et et en utilisant un List<Fragment> Pour contenir tous les fragments pour mon ViewPager à afficher. Sur addItem() j'ajoute simplement un Fragment instancié puis j'appelle notifyDataSetChanged(). Je ne sais pas si cela est nécessaire ou non.

Mon problème simplement ... commencez par le fragment 1

[Fragment 1] 

ajouter un nouveau fragment 2

[Fragment 1] [Fragment 2]

supprimer le fragment 2

[Fragment 1]

ajouter un nouveau fragment 3

[Fragment 1] [Fragment 2]

Lors de l'ajout de nouveaux fragments, tout semble parfait. Une fois que j'ai supprimé un fragment puis ajouté un nouveau fragment instancié, l'ancien fragment est toujours affiché. Quand je vais dans une .getClass.getName() cela me donne le nom du Fragment 3 mais je vois toujours le Fragment 2.

Je crois que cela pourrait être un problème avec instantiateItem() ou tel mais je pensais que l'adaptateur devait gérer cela pour nous. Toute suggestion sera appréciée.

code adaptateur ...

public class MyPagerAdapter extends FragmentPagerAdapter {
public final ArrayList<Fragment> screens2 = new ArrayList<Fragment>();


private Context context;

public MyPagerAdapter(FragmentManager fm, Context context) {
    super(fm);
    this.context = context;
}

public void removeType(String name){
    for(Fragment f: screens2){
        if(f.getClass().getName().contains(name)){ screens2.remove(f); return; }
    }
    this.notifyDataSetChanged();
}

public boolean addSt(String tag, Class<?> clss, Bundle args){
    if(clss==null) return false;
    if(!clss.getName().contains("St")) return false; 
    if(!args.containsKey("cId")) return false;
    boolean has = false;
    boolean hasAlready = false;
    for(Fragment tab: screens2){
        if(tab.getClass().getName().contains("St")){
            has = true;
            if(tab.getArguments().containsKey("cId"))
                if(tab.getArguments().getLong("cId") == args.getLong("cId")){
                    hasAlready = true;
                }
            if(!hasAlready){
                // exists but is different so replace
                screens2.remove(tab);
                this.addScreen(tag, clss, args, C.PAGE_ST);
                // if returned true then it notifies dataset
                return true;
            }
        }
        hasAlready = false;
    }

    if(!has){ 
        // no st yet exist in adapter
        this.addScreen(tag, clss, args, C.PAGE_ST);
        return true;
    }

    return false;
}

public boolean removeCCreate(){
    this.removeType("Create");  
    return false;
}

@Override
public int getItemPosition(Object object) {

   return POSITION_NONE; //To make notifyDataSetChanged() do something
  }

public void addCCreate(){
    this.removeCCreate();
    Log.w("addding c", " ");
    this.addScreen("Create C",  CreateCFragment.class, null, C.PAGE_CREATE_C);
}

public void addScreen(String tag, Class<?> clss, Bundle args, int type){
    if(clss!=null){
        screens2.add(Fragment.instantiate(context, clss.getName(), args));
    }
}

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


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

}

Je me rends compte que le code utilise des moyens "ghetto" pour déterminer le type de fragment mais j'ai écrit ce code strictement pour tester la fonctionnalité. Toute aide ou idée serait formidable car il semble que peu de gens se sont aventurés dans le monde des FragmentPagerAdapters.

43
Maurycy

J'ai eu le même problème et ma solution contournait la méthode "destroyItem" comme suit.

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
    FragmentManager manager = ((Fragment)object).getFragmentManager();
    FragmentTransaction trans = manager.beginTransaction();
    trans.remove((Fragment)object);
    trans.commit();
}

Ça marche pour moi, est-ce que quelqu'un a une autre solution?

Mise à jour:

J'ai trouvé inutile de supprimer le fragment de code, j'ai donc ajouté une condition pour l'éviter.

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
    if (position >= getCount()) {
        FragmentManager manager = ((Fragment) object).getFragmentManager();
        FragmentTransaction trans = manager.beginTransaction();
        trans.remove((Fragment) object);
        trans.commit();
    }
}
34
Tericky Shih

Mise à jour de cet article et inclus ma solution (si quelqu'un peut s'améliorer, faites le moi savoir)

Ok, j'ai maintenant résolu mon problème d'une manière hackish, mais oui ça marche;). Si quelqu'un peut améliorer ma solution, faites-le moi savoir. Pour ma nouvelle solution, j'utilise maintenant un CustomFragmentStatePagerAdapter mais il n'enregistre pas l'état comme il se doit et stocke tous les fragments dans une liste. Cela peut provoquer un problème de mémoire si l'utilisateur possède plus de 50 fragments, comme le fait FragmentPagerAdapter normal. Ce serait génial si quelqu'un pouvait ajouter la chose State à ma solution sans supprimer mes correctifs. Merci.

Voici donc mon CustomFragmentStatePagerAdapter.Java

package com.tundem.webLab.Adapter;

import Java.util.ArrayList;

import Android.os.Bundle;
import Android.os.Parcelable;
import Android.support.v4.app.Fragment;
import Android.support.v4.app.FragmentManager;
import Android.support.v4.app.FragmentTransaction;
import Android.support.v4.view.PagerAdapter;
import Android.util.Log;
import Android.view.View;
import Android.view.ViewGroup;

public abstract class CustomFragmentStatePagerAdapter extends PagerAdapter {
    private static final String TAG = "FragmentStatePagerAdapter";
    private static final boolean DEBUG = false;

    private final FragmentManager mFragmentManager;
    private FragmentTransaction mCurTransaction = null;

    public ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
    public ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
    private Fragment mCurrentPrimaryItem = null;

    public CustomFragmentStatePagerAdapter(FragmentManager fm) {
        mFragmentManager = fm;
    }

    /**
     * Return the Fragment associated with a specified position.
     */
    public abstract Fragment getItem(int position);

    @Override
    public void startUpdate(ViewGroup container) {}

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        // If we already have this item instantiated, there is nothing
        // to do. This can happen when we are restoring the entire pager
        // from its saved state, where the fragment manager has already
        // taken care of restoring the fragments we previously had instantiated.

        // DONE Remove of the add process of the old stuff
        /* if (mFragments.size() > position) { Fragment f = mFragments.get(position); if (f != null) { return f; } } */

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }

        Fragment fragment = getItem(position);
        if (DEBUG)
            Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
        if (mSavedState.size() > position) {
            Fragment.SavedState fss = mSavedState.get(position);
            if (fss != null) {
                try // DONE: Try Catch
                {
                    fragment.setInitialSavedState(fss);
                } catch (Exception ex) {
                    // Schon aktiv (kA was das heißt xD)
                }
            }
        }
        while (mFragments.size() <= position) {
            mFragments.add(null);
        }
        fragment.setMenuVisibility(false);
        mFragments.set(position, fragment);
        mCurTransaction.add(container.getId(), fragment);

        return fragment;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        Fragment fragment = (Fragment) object;

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        mCurTransaction.remove(fragment);

        /*if (mCurTransaction == null) { mCurTransaction = mFragmentManager.beginTransaction(); } if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object + " v=" + ((Fragment)
         * object).getView()); while (mSavedState.size() <= position) { mSavedState.add(null); } mSavedState.set(position, mFragmentManager.saveFragmentInstanceState(fragment));
         * mFragments.set(position, null); mCurTransaction.remove(fragment); */
    }

    @Override
    public void setPrimaryItem(ViewGroup container, int position, Object object) {
        Fragment fragment = (Fragment) object;
        if (fragment != mCurrentPrimaryItem) {
            if (mCurrentPrimaryItem != null) {
                mCurrentPrimaryItem.setMenuVisibility(false);
            }
            if (fragment != null) {
                fragment.setMenuVisibility(true);
            }
            mCurrentPrimaryItem = fragment;
        }
    }

    @Override
    public void finishUpdate(ViewGroup container) {
        if (mCurTransaction != null) {
            mCurTransaction.commitAllowingStateLoss();
            mCurTransaction = null;
            mFragmentManager.executePendingTransactions();
        }
    }

    @Override
    public boolean isViewFromObject(View view, Object object) {
        return ((Fragment) object).getView() == view;
    }

    @Override
    public Parcelable saveState() {
        Bundle state = null;
        if (mSavedState.size() > 0) {
            state = new Bundle();
            Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
            mSavedState.toArray(fss);
            state.putParcelableArray("states", fss);
        }
        for (int i = 0; i < mFragments.size(); i++) {
            Fragment f = mFragments.get(i);
            if (f != null) {
                if (state == null) {
                    state = new Bundle();
                }
                String key = "f" + i;
                mFragmentManager.putFragment(state, key, f);
            }
        }
        return state;
    }

    @Override
    public void restoreState(Parcelable state, ClassLoader loader) {
        if (state != null) {
            Bundle bundle = (Bundle) state;
            bundle.setClassLoader(loader);
            Parcelable[] fss = bundle.getParcelableArray("states");
            mSavedState.clear();
            mFragments.clear();
            if (fss != null) {
                for (int i = 0; i < fss.length; i++) {
                    mSavedState.add((Fragment.SavedState) fss[i]);
                }
            }
            Iterable<String> keys = bundle.keySet();
            for (String key : keys) {
                if (key.startsWith("f")) {
                    int index = Integer.parseInt(key.substring(1));
                    Fragment f = mFragmentManager.getFragment(bundle, key);
                    if (f != null) {
                        while (mFragments.size() <= index) {
                            mFragments.add(null);
                        }
                        f.setMenuVisibility(false);
                        mFragments.set(index, f);
                    } else {
                        Log.w(TAG, "Bad fragment at key " + key);
                    }
                }
            }
        }
    }
}

Voici mon normal FragmentAdapter.Java

package com.tundem.webLab.Adapter;

import Java.util.LinkedList;
import Java.util.List;

import Android.support.v4.app.FragmentManager;

import com.tundem.webLab.fragments.BaseFragment;
import com.viewpagerindicator.TitleProvider;

public class FragmentAdapter extends CustomFragmentStatePagerAdapter implements TitleProvider {
    public List<BaseFragment> fragments = new LinkedList<BaseFragment>();

    private int actPage;

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

    public void setActPage(int actPage) {
        this.actPage = actPage;
    }

    public void addItem(BaseFragment fragment) {
        // TODO if exists don't open / change to that tab
        fragments.add(fragment);
    }

    public BaseFragment getActFragment() {
        return getItem(getActPage());
    }

    public int getActPage() {
        return actPage;
    }

    @Override
    public BaseFragment getItem(int position) {
        if (position < getCount()) {
            return fragments.get(position);
        } else
            return null;
    }

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

    @Override
    public String getTitle(int position) {
        return fragments.get(position).getTitle();
    }

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

Et c'est ainsi que je supprime un fragment. (Je sais que c'est un peu plus que seulement .remove ()). Soyez libre d'améliorer ma solution, vous pouvez également ajouter ce code quelque part dans l'adaptateur alors oui. C'est à l'utilisateur qui essaie de mettre en œuvre cela. J'utilise ceci dans mon TabHelper.Java (Une classe qui gère toutes les opérations de tabulation comme supprimer, ajouter, ...)

    int act = Cfg.mPager.getCurrentItem();
    Cfg.mPager.removeAllViews();
    Cfg.mAdapter.mFragments.remove(act);
    try {
        Cfg.mAdapter.mSavedState.remove(act);
    } catch (Exception ex) {/* Already removed */}
    try {
        Cfg.mAdapter.fragments.remove(act);
    } catch (Exception ex) {/* Already removed */}

    Cfg.mAdapter.notifyDataSetChanged();
    Cfg.mIndicator.notifyDataSetChanged();

Description du Cfg. chose. Je sauvegarde la référence à ces objets dans une classe cfg, donc je peux toujours les utiliser sans avoir besoin d'une Factory.Java spéciale ...

Ouais. j'espère avoir pu aider. N'hésitez pas à améliorer cela, mais faites-le moi savoir afin que je puisse également améliorer mon code.

Merci.

Si j'ai raté un code, faites le moi savoir.


Mon ancienne réponse fonctionne également, mais uniquement si vous avez des fragments différents. FileFragment, WebFragment, ... Pas si vous utilisez deux fois l'un de ces types de fragments.

Je l'ai fait pseudo travailler pour l'instant. C'est une solution vraiment sale et j'en cherche toujours une meilleure. Veuillez aider.

J'ai changé le code, où je supprime un onglet en celui-ci:

   public static void deleteActTab()
        {   
            //We set this on the indicator, NOT the pager
            int act = Cfg.mPager.getCurrentItem();
            Cfg.mAdapter.removeItem(act);
            List<BaseFragment> frags = new LinkedList<BaseFragment>();
            frags = Cfg.mAdapter.fragments;

            Cfg.mPager = (ViewPager)Cfg.act.findViewById(R.id.pager);
            Cfg.mPager.setAdapter(Cfg.mAdapter);
            Cfg.mIndicator.setViewPager(Cfg.mPager);

            Cfg.mAdapter.fragments = frags;

            if(act > 0)
            {
                Cfg.mPager.setCurrentItem(act-1);
                Cfg.mIndicator.setCurrentItem(act-1);
            }

            Cfg.mIndicator.notifyDataSetChanged();
        }

Si quelqu'un peut améliorer ce code, faites-le moi savoir. Si quelqu'un peut nous dire la vraie réponse à ce problème. veuillez l'ajouter ici. Il y a beaucoup de gens qui connaissent ce problème. J'ai ajouté une réputation de 50 pour celui qui le résout. Je peux aussi faire un don pour celui qui le résout.

Merci

6
mikepenz

Peut-être que cette réponse vous aide.

Utilisez FragmentStatePagerAdapter au lieu de FragmentPagerAdapter .

Parce que FragmentPagerAdapter ne détruit pas les vues. Pour plus d'informations, lisez ceci réponse .

3
Afshin

Prenant "le meilleur des deux mondes" (je veux dire les réponses de @Tericky Shih et @mikepenz), nous avons les choses courtes et simples:

public class MyPagerAdapter extends FragmentPagerAdapter {

    public ArrayList<Fragment> fragments = new ArrayList<Fragment>();    

    ...

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        super.destroyItem(container, position, object);
        if (position >= getCount()) fm.beginTransaction().remove((Fragment) object).commit();
    }

    @Override
    public int getItemPosition(Object object) {
        if (fragments.contains(object)) return fragments.indexOf(object);
        else return POSITION_NONE;
    }
}

La principale différence est que si un fragment n'est pas modifié, vous n'avez pas à détruire sa vue et vous n'avez pas à retourner POSITION_NONE Pour cela. Dans le même temps, j'ai rencontré une situation où ViewPager tenait une référence à un élément qui était déjà détruit, donc la vérification if (fragments.contains(object)) aide à déterminer si cet élément n'est plus nécessaire.

3
Alex Kuzmin

J'ai eu une situation similaire à la vôtre. J'ai récemment dû ajouter et supprimer des fragments du ViewPager. Dans le premier mode, j'ai les fragments 0, 1 et 2 et dans le deuxième mode j'ai les fragments 0 et 3. Je veux que le fragment 0 soit le même pour les deux modes et conserve les informations.

Tout ce que je devais faire était de remplacer FragmentPagerAdapter.getItemId pour m'assurer que je retournais un numéro unique pour chaque fragment différent - la valeur par défaut est de retourner "position". J'ai également dû définir à nouveau l'adaptateur dans le ViewPager - une nouvelle instance fonctionnera, mais je l'ai redéfinie sur la même instance. En définissant l'adaptateur, ViewPager supprime toutes les vues et essaye pour les ajouter à nouveau.

Cependant, l'astuce est que l'adaptateur n'appelle getItem que lorsqu'il veut instancier le fragment - pas à chaque fois qu'il le montre. En effet, ils sont mis en cache et les recherchent par la "position" renvoyée par getItemId.

Imaginez que vous avez trois fragments (0, 1 et 2) et que vous souhaitez supprimer "1". Si vous retournez "position" pour getItemId, la suppression du fragment 1 ne fonctionnera pas car lorsque vous essayez d'afficher le fragment 2 après avoir supprimé le fragment 1, le pager/adaptateur pensera qu'il a déjà le fragment pour cette "position" et continuera d'afficher le fragment 1 .

Pour info: j'ai essayé de notifierDataDataSetChanged au lieu de définir l'adaptateur mais cela n'a pas fonctionné pour moi.

Tout d'abord, l'exemple de substitution getItemId et ce que j'ai fait pour mon getItem:

public class SectionsPagerAdapter extends FragmentPagerAdapter
{
    ...

    @Override
    public long getItemId(int position)
    {
        // Mode 1 uses Fragments 0, 1 and 2. Mode 2 uses Fragments 0 and 3
        if ( mode == 2 && position == 1 )
            return 3;
        return position;
    }

    @Override
    public Fragment getItem(int position)
    {
        if ( mode == 1 )
        {
            switch (position)
            {
                case 0:
                    return <<fragment 0>>;
                case 1:
                    return <<fragment 1>>;
                case 2:
                    return <<fragment 2>>;
            }
        }
        else    // Mode 2
        {
            switch (position)
            {
                case 0:
                    return <<fragment 0>>;
                case 1:
                    return <<fragment 3>>;
            }
        }
        return null;
    }
}

Maintenant, le changement de mode:

private void modeChanged(int newMode)
{
    if ( newMode == mode )
        return;

    mode = newMode;

    // Calling mSectionsPagerAdapter.notifyDataSetChanged() is not enough here
    mViewPager.setAdapter(mSectionsPagerAdapter);
}
1
RowanPD

Ça n'a pas marché pour moi. Ma solution a été mise FragmentStatePagerAdapter.Java dans mon projet, renommé FragmentStatePagerAdapter2.Java. Dans destroyItem (), j'ai changé un peu en fonction des journaux d'erreurs. De

// mFragments.set(position, null);

à

if (position < mFragments.size())mFragments.remove(position);

Peut-être que vous n'avez pas le même problème, vérifiez simplement le journal. J'espère que cela aide quelqu'un!

0
BruceDu

Le vrai problème est que FragmentPagerAdapter utilise la position du fragment dans votre liste comme ID. Donc, si vous ajoutez une nouvelle liste ou supprimez simplement des éléments, l'élément "instantiateItem" trouvera différents fragments pour les nouveaux éléments dans la liste ...

@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);
    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));
    }
    if (fragment != mCurrentPrimaryItem) {
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
    }

    return fragment;
}

et

  private static String makeFragmentName(int viewId, long id) {
    return "Android:switcher:" + viewId + ":" + id;
}

et

     * Return a unique identifier for the item at the given position.
 * <p>
 * <p>The default implementation returns the given position.
 * Subclasses should override this method if the positions of items can change.</p>
 *
 * @param position Position within this adapter
 * @return Unique identifier for the item at position
 */
public long getItemId(int position) {
    return position;
}
0
stefan

J'ai eu ce même problème jusqu'à ce que je me rende compte que je créais mon PagerView à partir d'un autre fragment et non de l'activité principale.

Ma solution a été de passer le ChildFragment Manager dans le constructeur du Fragment (State) PagerAdapter et non le Fragment Manager du parent Parent.

En utilisant ChildFragmentManager, tous les fragments créés par ViewPagerAdapter sont nettoyés automatiquement lorsque le fragment parent est détruit.

0
Eurospoofer

Après beaucoup d'essais, je l'ai fait fonctionner pour qu'il supprime ou attache correctement un troisième fragment en position finale.

Object fragments[] = new Object[3];
int mItems = 2;
MyAdapter mAdapter;
ViewPager mPager;


public void addFragment(boolean bool) {

    mAdapter.startUpdate(mPager);

    if (!bool) {
        mAdapter.destroyItem(mPager, 2, fragments[2]);
        mItems = 2;
        fNach = false;
    }
    else if (bool && !fNach){
        mItems = 3;
        mAdapter.instantiateItem(mPager,2);
        fNach = true;
    }
    mAdapter.finishUpdate(mPager);
    mAdapter.notifyDataSetChanged();

}

public class MyAdapter extends FragmentPagerAdapter {
    MyAdapter(FragmentManager fm) {
        super(fm);
    }

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

    @Override
    public CharSequence getPageTitle(int position) {
    ... (code for the PagerTitleStrip)
    }

    @Override
    public Fragment getItem(int position) {
        Fragment f = null;
        switch (position) {
            case 0:
                f = new Fragment1();
                break;
            case 1:
                f = new Fragment2();
                break;
            case 2:
                f = new Fragment3();
                break;
        }
        return f;
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        Object o = super.instantiateItem(container,position);
        fragments[position] = o;
        return o;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        super.destroyItem(container, position, object);
        System.out.println("Destroy item " + position);
        if (position >= getCount()) {
                FragmentManager manager = ((Fragment) object).getFragmentManager();
                FragmentTransaction ft = manager.beginTransaction();
                ft.remove((Fragment) object);
                ft.commit();
        }

    }
}

Quelques précisions: pour obtenir la référence d'objet à appeler destroyItem, j'ai stocké les objets renvoyés par instantiateItem dans un tableau. Lorsque vous ajoutez ou supprimez des fragments, vous devez l'annoncer avec startUpdate, finishUpdate et notifyDataSetChanged. Le nombre d'éléments doit être modifié manuellement, pour l'ajouter, vous l'augmentez et l'instanciez, getItem le crée ensuite. Pour la suppression, vous appelez destroyItem, et dans ce code, il est essentiel la position> = mItems, car destroyItem est également appelé si un fragment sort du cache. Vous ne voulez pas le supprimer alors. La seule chose qui ne fonctionne pas est l'animation de balayage. Après avoir supprimé la dernière page, l'animation "ne peut pas glisser vers la gauche" n'est pas restaurée correctement sur la nouvelle dernière page. Si vous le faites glisser, une page vierge s'affiche et elle rebondit.

0
Stephan Brunker