Android 4.2: comportement de back-stack avec des fragments imbriqués

Avec Android 4.2, la bibliothèque de support bénéficie d'un support pour les fragments imbriqués, voir ici . J'ai joué avec ça et j'ai trouvé un comportement / bug intéressant concernant back stack et getChildFragmentManager () . Lorsque vous utilisez getChildFragmentManager () et addToBackStack (String name), en appuyant sur le bouton Précédent, le système ne décompose pas la pile arrière sur le fragment précédent. D'autre part, lors de l'utilisation de getFragmentManager () et addToBackStack (String name), en appuyant sur le bouton arrière, le système revient au fragment précédent.

Pour moi, ce comportement est inattendu. En appuyant sur le bouton arrière de mon appareil, je m'attends à ce que le dernier fragment ajouté à la pile arrière soit diffusé, même si le fragment a été ajouté à la pile arrière dans le gestionnaire de fragments pour enfants.

Ce comportement est-il correct? Ce comportement est-il un bug? Y a-t-il un problème pour ce problème?

Exemple de code avec getChildFragmentManager ():

public class FragmentceptionActivity extends FragmentActivity { @Override protected void onCreate(Bundle arg0) { super.onCreate(arg0); final FrameLayout wrapper1 = new FrameLayout(this); wrapper1.setLayoutParams(new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); wrapper1.setId(1); final FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT); params.topMargin = 0; final TextView text = new TextView(this); text.setLayoutParams(params); text.setText("fragment 1"); wrapper1.addView(text); setContentView(wrapper1); getSupportFragmentManager().beginTransaction().addToBackStack(null) .add(1, new Fragment1()).commit(); } public class Fragment1 extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final FrameLayout wrapper2 = new FrameLayout(getActivity()); wrapper2.setLayoutParams(new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); wrapper2.setId(2); final FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT); params.topMargin = 100; final TextView text = new TextView(getActivity()); text.setLayoutParams(params); text.setText("fragment 2"); wrapper2.addView(text); return wrapper2; } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); getFragmentManager().beginTransaction().addToBackStack(null) .add(2, new Fragment2()).commit(); } } public class Fragment2 extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final FrameLayout wrapper3 = new FrameLayout(getActivity()); wrapper3.setLayoutParams(new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); wrapper3.setId(3); final FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT); params.topMargin = 200; final TextView text = new TextView(getActivity()); text.setLayoutParams(params); text.setText("fragment 3"); wrapper3.addView(text); return wrapper3; } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); getChildFragmentManager().beginTransaction().addToBackStack(null) .add(3, new Fragment3()).commit(); } } public class Fragment3 extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final FrameLayout wrapper4 = new FrameLayout(getActivity()); wrapper4.setLayoutParams(new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); wrapper4.setId(4); final FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT); params.topMargin = 300; final TextView text = new TextView(getActivity()); text.setLayoutParams(params); text.setText("fragment 4"); wrapper4.addView(text); return wrapper4; } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); getChildFragmentManager().beginTransaction().addToBackStack(null) .add(4, new Fragment4()).commit(); } } public class Fragment4 extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final FrameLayout wrapper5 = new FrameLayout(getActivity()); wrapper5.setLayoutParams(new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); wrapper5.setId(5); final FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT); params.topMargin = 400; final TextView text = new TextView(getActivity()); text.setLayoutParams(params); text.setText("fragment 5"); wrapper5.addView(text); return wrapper5; } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); } } } 

Exemple de code avec getFragmentManager ():

 public class FragmentceptionActivity extends FragmentActivity { @Override protected void onCreate(Bundle arg0) { super.onCreate(arg0); final FrameLayout wrapper1 = new FrameLayout(this); wrapper1.setLayoutParams(new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); wrapper1.setId(1); final FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT); params.topMargin = 0; final TextView text = new TextView(this); text.setLayoutParams(params); text.setText("fragment 1"); wrapper1.addView(text); setContentView(wrapper1); getSupportFragmentManager().beginTransaction().addToBackStack(null) .add(1, new Fragment1()).commit(); } public class Fragment1 extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final FrameLayout wrapper2 = new FrameLayout(getActivity()); wrapper2.setLayoutParams(new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); wrapper2.setId(2); final FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT); params.topMargin = 100; final TextView text = new TextView(getActivity()); text.setLayoutParams(params); text.setText("fragment 2"); wrapper2.addView(text); return wrapper2; } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); getFragmentManager().beginTransaction().addToBackStack(null) .add(2, new Fragment2()).commit(); } } public class Fragment2 extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final FrameLayout wrapper3 = new FrameLayout(getActivity()); wrapper3.setLayoutParams(new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); wrapper3.setId(3); final FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT); params.topMargin = 200; final TextView text = new TextView(getActivity()); text.setLayoutParams(params); text.setText("fragment 3"); wrapper3.addView(text); return wrapper3; } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); getFragmentManager().beginTransaction().addToBackStack(null) .add(3, new Fragment3()).commit(); } } public class Fragment3 extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final FrameLayout wrapper4 = new FrameLayout(getActivity()); wrapper4.setLayoutParams(new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); wrapper4.setId(4); final FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT); params.topMargin = 300; final TextView text = new TextView(getActivity()); text.setLayoutParams(params); text.setText("fragment 4"); wrapper4.addView(text); return wrapper4; } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); getFragmentManager().beginTransaction().addToBackStack(null) .add(4, new Fragment4()).commit(); } } public class Fragment4 extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final FrameLayout wrapper5 = new FrameLayout(getActivity()); wrapper5.setLayoutParams(new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); wrapper5.setId(5); final FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT); params.topMargin = 400; final TextView text = new TextView(getActivity()); text.setLayoutParams(params); text.setText("fragment 5"); wrapper5.addView(text); return wrapper5; } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); } } } 

  • Comment déterminer le fragment restauré depuis la couche arrière
  • Animation personnalisée pour le viewpager
  • Comment puis-je implémenter un balayage horizontal sans utiliser la bibliothèque de support?
  • V4 getFragmentManager avec Activité - Types incompatibles
  • Restauration de l'état de MapView lors de la rotation et de l'arrière
  • Stuck with porting from activity to fragment
  • Fragment isAdded () renvoie false sur un fragment déjà ajouté
  • Circulaire ViewPager. Les fragments ne fonctionnent pas comme ils l'ont supposé après le premier tour
  • 11 Solutions collect form web for “Android 4.2: comportement de back-stack avec des fragments imbriqués”

    On dirait un bug. Consultez http://code.google.com/p/android/issues/detail?id=40323

    Pour une solution de contournement, j'ai utilisé avec succès (comme suggéré dans les commentaires):

      @Override public void onBackPressed() { // If the fragment exists and has some back-stack entry if (mActivityDirectFragment != null && mActivityDirectFragment.getChildFragmentManager().getBackStackEntryCount() > 0){ // Get the fragment fragment manager - and pop the backstack mActivityDirectFragment.getChildFragmentManager().popBackStack(); } // Else, nothing in the direct fragment back stack else{ // Let super handle the back press super.onBackPressed(); } } 

    Cette solution peut être une meilleure version de @Sean answer:

     @Override public void onBackPressed() { // if there is a fragment and the back stack of this fragment is not empty, // then emulate 'onBackPressed' behaviour, because in default, it is not working FragmentManager fm = getSupportFragmentManager(); for (Fragment frag : fm.getFragments()) { if (frag.isVisible()) { FragmentManager childFm = frag.getChildFragmentManager(); if (childFm.getBackStackEntryCount() > 0) { childFm.popBackStack(); return; } } } super.onBackPressed(); } 

    Encore une fois, j'ai préparé cette solution en fonction de @Sean réponse ci-dessus.

    Comme l'a déclaré @ AZ13, cette solution n'est possible que dans des situations de fragments d'enfants à un niveau. Dans un cas de fragments à plusieurs niveaux, les travaux deviennent un peu complexes, je recommande donc d'essayer cette solution uniquement le cas possible que j'ai dit. =)

    Remarque: Étant getFragments méthode getFragments est maintenant une méthode privée, cette solution ne fonctionnera pas. Vous pouvez vérifier les commentaires pour un lien qui suggère une solution à propos de cette situation.

    Cette solution peut être une meilleure version de @msms répondre:

    Version du fragment imbriqué

     private boolean onBackPressed(FragmentManager fm) { if (fm != null) { if (fm.getBackStackEntryCount() > 0) { fm.popBackStack(); return true; } List<Fragment> fragList = fm.getFragments(); if (fragList != null && fragList.size() > 0) { for (Fragment frag : fragList) { if (frag == null) { continue; } if (frag.isVisible()) { if (onBackPressed(frag.getChildFragmentManager())) { return true; } } } } } return false; } @Override public void onBackPressed() { FragmentManager fm = getSupportFragmentManager(); if (onBackPressed(fm)) { return; } super.onBackPressed(); } 

    Avec cette réponse, il va gérer la vérification récursive du retour et donner à chaque fragment la possibilité de remplacer le comportement par défaut. Cela signifie que vous pouvez avoir un fragment qui héberge un ViewPager, faites quelque chose de spécial, faites défiler jusqu'à la page en tant que pile de fond, ou faites défiler jusqu'à la page d'accueil, puis appuyez sur pour continuer.

    Ajoutez ceci à votre activité qui étend AppCompatActivity.

     @Override public void onBackPressed() { if(!BaseFragment.handleBackPressed(getSupportFragmentManager())){ super.onBackPressed(); } } 

    Ajoutez ceci à votre BaseFragment ou à la classe dont vous avez tous les fragments hérités.

     public static boolean handleBackPressed(FragmentManager fm) { if(fm.getFragments() != null){ for(Fragment frag : fm.getFragments()){ if(frag != null && frag.isVisible() && frag instanceof BaseFragment){ if(((BaseFragment)frag).onBackPressed()){ return true; } } } } return false; } protected boolean onBackPressed() { FragmentManager fm = getChildFragmentManager(); if(handleBackPressed(fm)){ return true; } else if(getUserVisibleHint() && fm.getBackStackEntryCount() > 0){ fm.popBackStack(); return true; } return false; } 

    La raison en est que votre activité découle de FragmentActivity, qui gère la touche BACK (voir la ligne 173 de FragmentActivity .

    Dans notre application, j'utilise un ViewPager (avec des fragments) et chaque fragment peut avoir des fragments imbriqués. La façon dont j'ai traité ceci est la suivante:

    • Définissant une interface OnBackKeyPressedListener avec une méthode unique void onBackKeyPressed()
    • A implémenté cette interface dans des fragments "haut" que ViewPager montre
    • En suralimentant l'onKeyDown et en détectant BACK press, et en appelantBackKeyPressed dans un fragment actif actuellement dans le pager de vue.

    Notez également que j'utilise getChildFragmentManager() dans les fragments pour imbriquer correctement les fragments. Vous pouvez voir une discussion et une explication dans cette publication sur les développeurs Android .

    J'ai pu gérer la pile de fragments en ajoutant au fragment parent cette méthode à la méthode onCreate View () et en passant la vue racine.

     private void catchBackEvent(View v){ v.setFocusableInTouchMode(true); v.requestFocus(); v.setOnKeyListener( new OnKeyListener() { @Override public boolean onKey( View v, int keyCode, KeyEvent event ) { if( keyCode == KeyEvent.KEYCODE_BACK ) { if(isEnableFragmentBackStack()){ getChildFragmentManager().popBackStack(); setEnableFragmentBackStack(false); return true; } else return false; } return false; } } ); } 

    La méthode isEnableFragmentBackStack () est un indicateur booléen pour savoir quand je suis sur le fragment principal ou le suivant.

    Assurez-vous que lorsque vous commettez le fragment qui doit être disposé, vous devez ajouter la méthode addToBackstack.

    Cette solution peut être meilleure, car elle vérifie tous les niveaux de fragments imbriqués:

      /** * This method will go check all the back stacks of the added fragments and their nested fragments * to the the {@code FragmentManager} passed as parameter. * If there is a fragment and the back stack of this fragment is not empty, * then emulate 'onBackPressed' behaviour, because in default, it is not working. * * @param fm the fragment manager to which we will try to dispatch the back pressed event. * @return {@code true} if the onBackPressed event was consumed by a child fragment, otherwise {@code false}. */ public static boolean dispatchOnBackPressedToFragments(FragmentManager fm) { List<Fragment> fragments = fm.getFragments(); boolean result; if (fragments != null && !fragments.isEmpty()) { for (Fragment frag : fragments) { if (frag != null && frag.isAdded() && frag.getChildFragmentManager() != null) { // go to the next level of child fragments. result = dispatchOnBackPressedToFragments(frag.getChildFragmentManager()); if (result) return true; } } } // if the back stack is not empty then we pop the last transaction. if (fm.getBackStackEntryCount() > 0) { fm.popBackStack(); fm.executePendingTransactions(); return true; } return false; } 

    Dans votre activité onBackPressed vous pouvez simplement l'appeler de cette façon:

     FragmentManager fm = getSupportFragmentManager(); // if there is a fragment and the back stack of this fragment is not empty, // then emulate 'onBackPressed' behaviour, because in default, it is not working if (!dispatchOnBackPressedToFragments(fm)) { // if no child fragment consumed the onBackPressed event, // we execute the default behaviour. super.onBackPressed(); } 

    Merci à tous pour leur aide, cette (version tordue) fonctionne pour moi:

     @Override public void onBackPressed() { if (!recursivePopBackStack(getSupportFragmentManager())) { super.onBackPressed(); } } /** * Recursively look through nested fragments for a backstack entry to pop * @return: true if a pop was performed */ public static boolean recursivePopBackStack(FragmentManager fragmentManager) { if (fragmentManager.getFragments() != null) { for (Fragment fragment : fragmentManager.getFragments()) { if (fragment != null && fragment.isVisible()) { boolean popped = recursivePopBackStack(fragment.getChildFragmentManager()); if (popped) { return true; } } } } if (fragmentManager.getBackStackEntryCount() > 0) { fragmentManager.popBackStack(); return true; } return false; } 

    REMARQUE: Vous voudrez probablement définir la couleur d'arrière-plan de ces fragments imbriqués dans la couleur d'arrière-plan de la fenêtre du thème de l'application, car par défaut ils sont transparents. Un peu en dehors de la portée de cette question, mais il est accompli en résolvant l'attribut android.R.attr.windowBackground et en définissant l'arrière-plan de la vue Fragment à cette ID de ressource.

    Ce code va naviguer dans l'arborescence des gestionnaires de fragments et renvoyer le dernier qui a été ajouté qui a des fragments qu'il peut sortir de la pile:

     private FragmentManager getLastFragmentManagerWithBack(FragmentManager fm) { FragmentManager fmLast = fm; List<Fragment> fragments = fm.getFragments(); for (Fragment f : fragments) { if ((f.getChildFragmentManager() != null) && (f.getChildFragmentManager().getBackStackEntryCount() > 0)) { fmLast = f.getFragmentManager(); FragmentManager fmChild = getLastFragmentManagerWithBack(f.getChildFragmentManager()); if (fmChild != fmLast) fmLast = fmChild; } } return fmLast; } 

    Appelez la méthode:

     @Override public void onBackPressed() { FragmentManager fm = getLastFragmentManagerWithBack(getSupportFragmentManager()); if (fm.getBackStackEntryCount() > 0) { fm.popBackStack(); return; } super.onBackPressed(); } 

    Si vous avez un DialogFragment qui à son tour a des fragments imbriqués, la «solution de contournement» est un peu différente. Au lieu de définir un onKeyListener sur la rootView , vous devrez le faire avec la Dialog . De plus, vous configurerez un DialogInterface.OnKeyListener et non le View one. Bien sûr, souvenez-vous de addToBackStack !

    Btw, ayant un fragment sur le backstack pour déléguer l'appel à l'activité, est mon cas d'utilisation personnelle. Les scénarios typiques peuvent être que le nombre soit 0.

    Voici ce que vous devez faire au sein de onCreateDialog

      @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Dialog dialog = super.onCreateDialog(savedInstanceState); dialog.setOnKeyListener(new DialogInterface.OnKeyListener() { @Override public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { if(keyCode == KeyEvent.KEYCODE_BACK){ FragmentManager cfm = getChildFragmentManager(); if(cfm.getBackStackEntryCount()>1){ cfm.popBackStack(); return true; } } return false; } }); return dialog; } 

    Pour ChildFragments, cela fonctionne …

     @Override public void onBackPressed() { if (getSupportFragmentManager().getBackStackEntryCount() > 0) { getSupportFragmentManager().popBackStack(); } else { doExit(); //super.onBackPressed(); } } 
    coAndroid est un fan Android de Google, tout sur les téléphones Android, Android Wear, Android Dev et Android Games Apps.