Les fragments nesteds disparaissent lors de l’animation de transition

Voici le scénario: L’activité contient le fragment A , qui à son tour utilise getChildFragmentManager() pour append les fragments A1 et A2 dans son onCreate comme ceci:

 getChildFragmentManager() .beginTransaction() .replace(R.id.fragmentOneHolder, new FragmentA1()) .replace(R.id.fragmentTwoHolder, new FragmentA2()) .commit() 

Jusqu’ici, tout va bien, tout fonctionne comme prévu.

Nous exécutons ensuite la transaction suivante dans l’activité:

 getSupportFragmentManager() .beginTransaction() .setCustomAnimations(anim1, anim2, anim1, anim2) .replace(R.id.fragmentHolder, new FragmentB()) .addToBackStack(null) .commit() 

Pendant la transition, les animations d’ enter pour le fragment B s’exécutent correctement mais les fragments A1 et A2 disparaissent complètement . Lorsque nous annulons la transaction avec le bouton Précédent, ils s’initialisent correctement et s’affichent normalement lors de l’animation popEnter .

Dans mes brefs tests, il est devenu plus étrange – si je configure les animations pour les fragments enfants (voir ci-dessous), l’animation de exit s’exécute par intermittence lorsque nous ajoutons le fragment B

 getChildFragmentManager() .beginTransaction() .setCustomAnimations(enter, exit) .replace(R.id.fragmentOneHolder, new FragmentA1()) .replace(R.id.fragmentTwoHolder, new FragmentA2()) .commit() 

L’effet que je veux obtenir est simple – je veux que l’animation exit (ou devrait-elle être popExit ?) Sur le fragment A (anim2) s’exécute, animant tout le conteneur, y compris ses enfants nesteds.

Y a-t-il un moyen d’y parvenir?

Edit : S’il vous plaît trouver un cas de test ici

Edit2 : Merci à @StevenByle de m’avoir poussé à continuer d’essayer les animations statiques. Apparemment, vous pouvez définir des animations sur une base par opération (pas globale pour l’ensemble de la transaction), ce qui signifie que les enfants peuvent avoir une animation statique indéfinie, alors que leurs parents peuvent avoir une animation différente . Voir la discussion ci-dessous et le projet de scénario de test mis à jour .

Afin d’éviter que l’utilisateur ne voie disparaître les fragments nesteds lorsque le fragment parent est supprimé / remplacé dans une transaction, vous pouvez «simuler» les fragments encore présents en leur fournissant une image telle qu’elle apparaît à l’écran. Cette image sera utilisée comme arrière-plan pour le conteneur de fragments nesteds, donc même si les vues du fragment nested disparaissent, l’image simulera leur présence. En outre, je ne pense pas que le fait de perdre l’interactivité avec les vues du fragment nested pose problème car je ne pense pas que vous souhaitiez que l’utilisateur agisse sur ces vues juste au moment de leur suppression (probablement comme action de la part de l’utilisateur). bien).

J’ai fait un petit exemple en configurant l’image d’arrière-plan (quelque chose de basique).

Donc, il semble y avoir beaucoup de solutions de contournement pour cela, mais basé sur la réponse de @ Jayd16, je pense avoir trouvé une solution attrape-tout assez solide qui permet toujours des animations de transition personnalisées sur des fragments enfants, et ne nécessite pas de faire un cache bitmap de la mise en page.

Avoir une classe BaseFragment qui étend Fragment et faire en sorte que tous vos fragments étendent cette classe (pas seulement les fragments enfants).

Dans cette classe BaseFragment , ajoutez ce qui suit:

 // Arbitrary value; set it to some reasonable default private static final int DEFAULT_CHILD_ANIMATION_DURATION = 250; @Override public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) { final Fragment parent = getParentFragment(); // Apply the workaround only if this is a child fragment, and the parent // is being removed. if (!enter && parent != null && parent.isRemoving()) { // This is a workaround for the bug where child fragments disappear when // the parent is removed (as all children are first removed from the parent) // See https://code.google.com/p/android/issues/detail?id=55228 Animation doNothingAnim = new AlphaAnimation(1, 1); doNothingAnim.setDuration(getNextAnimationDuration(parent, DEFAULT_CHILD_ANIMATION_DURATION)); return doNothingAnim; } else { return super.onCreateAnimation(transit, enter, nextAnim); } } private static long getNextAnimationDuration(Fragment fragment, long defValue) { try { // Attempt to get the resource ID of the next animation that // will be applied to the given fragment. Field nextAnimField = Fragment.class.getDeclaredField("mNextAnim"); nextAnimField.setAccessible(true); int nextAnimResource = nextAnimField.getInt(fragment); Animation nextAnim = AnimationUtils.loadAnimation(fragment.getActivity(), nextAnimResource); // ...and if it can be loaded, return that animation's duration return (nextAnim == null) ? defValue : nextAnim.getDuration(); } catch (NoSuchFieldException|IllegalAccessException|Resources.NotFoundException ex) { Log.w(TAG, "Unable to load next animation from parent.", ex); return defValue; } } 

Malheureusement, il faut y réfléchir. Cependant, comme cette solution de contournement concerne la bibliothèque de support, vous ne courez pas le risque que l’implémentation sous-jacente change à moins que vous ne mettiez à jour votre bibliothèque de support. Si vous créez la bibliothèque de support à partir de la source, vous pouvez append un accesseur pour l’ID de ressource d’animation suivant à Fragment.java et supprimer le besoin de reflection.

Cette solution supprime le besoin de “deviner” la durée de l’animation du parent (de sorte que l’animation “ne rien faire” ait la même durée que l’animation de sortie du parent) et vous permet de réaliser des animations personnalisées sur des fragments enfants échanger des fragments d’enfants avec différentes animations).

J’ai pu trouver une solution assez propre. IMO est le moins pirate, et bien que ce soit techniquement la solution “dessine un bitmap” au moins son abstraite par le fragment lib.

Assurez-vous que vos frags enfants remplacent une classe parente avec ceci:

 private static final Animation dummyAnimation = new AlphaAnimation(1,1); static{ dummyAnimation.setDuration(500); } @Override public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) { if(!enter && getParentFragment() != null){ return dummyAnimation; } return super.onCreateAnimation(transit, enter, nextAnim); } 

Si nous avons une animation de sortie sur les frags enfants, elles seront animées au lieu de clignoter. Nous pouvons l’exploiter en ayant une animation qui dessine simplement les fragments enfants en alpha pour une durée donnée. De cette façon, ils restront visibles dans le fragment parent lorsqu’ils s’animent, donnant le comportement souhaité.

Le seul problème auquel je peux penser est de garder une trace de cette durée. Je pourrais peut-être le définir sur un grand nombre, mais j’ai bien peur que cela puisse avoir des problèmes de performance si cela dessine toujours cette animation quelque part.

Je publie ma solution pour plus de clarté. La solution est assez simple. Si vous essayez d’imiter l’animation de transaction de fragment du parent, ajoutez simplement une animation personnalisée à la transaction de fragment enfant avec la même durée. Oh, et assurez-vous de définir l’animation personnalisée avant add ().

 getChildFragmentManager().beginTransaction() .setCustomAnimations(R.anim.none, R.anim.none, R.anim.none, R.anim.none) .add(R.id.container, nestedFragment) .commit(); 

Le xml pour R.anim.none (Mes parents entrent / sortent du temps d’animation est de 250ms)

     

Je comprends que cela puisse ne pas être capable de résoudre complètement votre problème, mais peut-être qu’il conviendra aux besoins de quelqu’un d’autre, vous pouvez append des animations enter / exit et popEnter / popExit à vos enfants qui ne déplacent / animent pas les Fragment . Tant que les animations ont la même durée / décalage que leurs animations de Fragment parent, elles apparaîtront comme animées / animées avec l’animation du parent.

@@@@@@@@@@@@@@@@@@@@

EDIT: J’ai fini par mettre en œuvre cette solution car il y avait d’autres problèmes. Square a récemment sorti 2 bibliothèques qui remplacent des fragments. Je dirais que cela pourrait être une meilleure alternative que d’essayer de pirater des fragments pour faire quelque chose que google ne veut pas qu’ils fassent.

http://corner.squareup.com/2014/01/mortar-and-flow.html

@@@@@@@@@@@@@@@@@@@@

Je pensais que je mettrais en place cette solution pour aider les personnes qui ont ce problème à l’avenir. Si vous parcourez la conversation avec d’autres personnes et regardez le code qu’il a posté, vous verrez que l’affiche originale finit par conclure à utiliser une animation sans opération sur les fragments enfants tout en animant le fragment parent. Cette solution n’est pas idéale car elle vous oblige à garder une trace de tous les fragments enfants, ce qui peut être compliqué lorsque vous utilisez un ViewPager avec FragmentPagerAdapter.

Depuis que j’utilise Child Fragments partout, j’ai trouvé cette solution efficace et modulaire (elle peut être facilement retirée) au cas où elle serait corrigée et que cette animation sans opération n’était plus nécessaire.

Il y a de nombreuses façons de l’implémenter. J’ai choisi d’utiliser un singleton et je l’appelle ChildFragmentAnimationManager. Il va essentiellement garder une trace d’un fragment enfant pour moi basé sur son parent et appliquera une animation de non-opération aux enfants quand demandé.

 public class ChildFragmentAnimationManager { private static ChildFragmentAnimationManager instance = null; private Map> fragmentMap; private ChildFragmentAnimationManager() { fragmentMap = new HashMap>(); } public static ChildFragmentAnimationManager instance() { if (instance == null) { instance = new ChildFragmentAnimationManager(); } return instance; } public FragmentTransaction animate(FragmentTransaction ft, Fragment parent) { List children = getChildren(parent); ft.setCustomAnimations(R.anim.no_anim, R.anim.no_anim, R.anim.no_anim, R.anim.no_anim); for (Fragment child : children) { ft.remove(child); } return ft; } public void putChild(Fragment parent, Fragment child) { List children = getChildren(parent); children.add(child); } public void removeChild(Fragment parent, Fragment child) { List children = getChildren(parent); children.remove(child); } private List getChildren(Fragment parent) { List children; if ( fragmentMap.containsKey(parent) ) { children = fragmentMap.get(parent); } else { children = new ArrayList(3); fragmentMap.put(parent, children); } return children; } } 

Ensuite, vous devez avoir une classe qui étend Fragment que tous vos fragments s’étendent (au moins vos fragments d’enfants). J’ai déjà eu cette classe et je l’appelle BaseFragment. Lorsqu’une vue de fragments est créée, nous l’ajoutons à ChildFragmentAnimationManager et la supprimons lorsqu’elle est détruite. Vous pouvez le faire surAttach / Detach ou d’autres méthodes correspondantes dans la séquence. Ma logique pour choisir Create / Destroy View était que si un fragment n’a pas de vue, je ne me soucie pas de l’animer pour continuer à être vu. Cette approche devrait également fonctionner mieux avec les ViewPagers qui utilisent Fragments car vous ne suivrez pas chaque fragment contenu par FragmentPagerAdapter, mais uniquement 3.

 public abstract class BaseFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Fragment parent = getParentFragment(); if (parent != null) { ChildFragmentAnimationManager.instance().putChild(parent, this); } return super.onCreateView(inflater, container, savedInstanceState); } @Override public void onDestroyView() { Fragment parent = getParentFragment(); if (parent != null) { ChildFragmentAnimationManager.instance().removeChild(parent, this); } super.onDestroyView(); } } 

Maintenant que tous vos fragments sont stockés dans la mémoire par le fragment parent, vous pouvez les appeler comme ceci et vos fragments enfants ne disparaîtront pas.

 FragmentTransaction ft = getActivity().getSupportFragmentManager().beginTransaction(); ChildFragmentAnimationManager.instance().animate(ft, ReaderFragment.this) .setCustomAnimations(R.anim.up_in, R.anim.up_out, R.anim.down_in, R.anim.down_out) .replace(R.id.container, f) .addToBackStack(null) .commit(); 

Aussi, juste pour que vous l’ayez, voici le fichier no_anim.xml qui va dans votre dossier res / anim:

     

Encore une fois, je ne pense pas que cette solution soit parfaite, mais c’est beaucoup mieux que pour chaque instance de Fragment Enfant, implémentant du code personnalisé dans le fragment parent pour suivre chaque enfant. J’y suis allé et ce n’est pas amusant.

J’avais le même problème avec un fragment de carte. Il a continué à disparaître pendant l’animation de sortie de son fragment contenant. La solution de contournement consiste à append une animation pour le fragment de carte enfant qui le gardera visible pendant l’animation de sortie du fragment parent. L’animation du fragment enfant garde son alpha à 100% pendant sa durée.

Animation: res / animator / keep_child_fragment.xml

     

L’animation est ensuite appliquée lorsque le fragment de carte est ajouté au fragment parent.

Fragment parent

 @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.map_parent_fragment, container, false); MapFragment mapFragment = MapFragment.newInstance(); getChildFragmentManager().beginTransaction() .setCustomAnimations(R.animator.keep_child_fragment, 0, 0, 0) .add(R.id.map, mapFragment) .commit(); return view; } 

Enfin, la durée de l’animation du fragment enfant est définie dans un fichier de ressources.

values ​​/ integers.xml

  500  

vous pouvez le faire dans le fragment enfant.

 @Override public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) { if (true) {//condition ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(getView(), "alpha", 1, 1); objectAnimator.setDuration(333);//time same with parent fragment's animation return objectAnimator; } return super.onCreateAnimator(transit, enter, nextAnim); } 

Je pense avoir trouvé une meilleure solution à ce problème que d’introduire le fragment actuel dans un bitmap, comme l’a suggéré Luksprog.

L’astuce consiste à masquer le fragment en cours de suppression ou de désinstallation et ce n’est qu’une fois les animations terminées que le fragment est supprimé ou détaché dans sa propre transaction de fragment.

Imaginez que nous avons FragmentA et FragmentB , tous deux avec des sous-fragments. Maintenant, quand vous feriez normalement:

 getSupportFragmentManager() .beginTransaction() .setCustomAnimations(anim1, anim2, anim1, anim2) .add(R.id.fragmentHolder, new FragmentB()) .remove(fragmentA) <------------------------------------------- .addToBackStack(null) .commit() 

Au lieu de cela vous faites

 getSupportFragmentManager() .beginTransaction() .setCustomAnimations(anim1, anim2, anim1, anim2) .add(R.id.fragmentHolder, new FragmentB()) .hide(fragmentA) <--------------------------------------------- .addToBackStack(null) .commit() fragmentA.removeMe = true; 

Maintenant pour la mise en œuvre du fragment:

 public class BaseFragment extends Fragment { protected Boolean detachMe = false; protected Boolean removeMe = false; @Override public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) { if (nextAnim == 0) { if (!enter) { onExit(); } return null; } Animation animation = AnimationUtils.loadAnimation(getActivity(), nextAnim); assert animation != null; if (!enter) { animation.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { onExit(); } @Override public void onAnimationRepeat(Animation animation) { } }); } return animation; } private void onExit() { if (!detachMe && !removeMe) { return; } FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction(); if (detachMe) { fragmentTransaction.detach(this); detachMe = false; } else if (removeMe) { fragmentTransaction.remove(this); removeMe = false; } fragmentTransaction.commit(); } } 

Pour animer la disparition des fragments neasted, nous pouvons forcer la stack de sauvegarde sur ChildFragmentManager. Cela déclenchera une animation de transition. Pour ce faire, nous devons rattraper l’événement OnBackButtonPressed ou écouter les modifications de backstack.

Voici l’exemple avec le code.

 View.OnClickListener() {//this is from custom button but you can listen for back button pressed @Override public void onClick(View v) { getChildFragmentManager().popBackStack(); //and here we can manage other fragment operations } }); Fragment fr = MyNeastedFragment.newInstance(product); getChildFragmentManager() .beginTransaction() .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE) .replace(R.neasted_fragment_container, fr) .addToBackStack("Neasted Fragment") .commit(); 

J’ai récemment rencontré ce problème dans ma question: les fragments nesteds effectuent une transition incorrecte

J’ai une solution qui résout ce problème sans enregistrer un bitmap, ni utiliser de reflection ou toute autre méthode non satisfaisante.

Un exemple de projet peut être consulté ici: https://github.com/zafrani/NestedFragmentTransitions

Un GIF de l’effet peut être consulté ici: https://imgur.com/94AvrW4

Dans mon exemple, il y a 6 fragments d’enfants répartis entre deux fragments parents. Je suis capable de réaliser les transitions pour entrer, sortir, pop et pousser sans aucun problème. Les modifications de configuration et les retours sont également gérés avec succès.

La majeure partie de la solution se trouve dans la fonction onCreateAnimator de my BaseFragment (le fragment étendu par mes enfants et fragments parent) qui ressemble à ceci:

  override fun onCreateAnimator(transit: Int, enter: Boolean, nextAnim: Int): Animator { if (isConfigChange) { resetStates() return nothingAnim() } if (parentFragment is ParentFragment) { if ((parentFragment as BaseFragment).isPopping) { return nothingAnim() } } if (parentFragment != null && parentFragment.isRemoving) { return nothingAnim() } if (enter) { if (isPopping) { resetStates() return pushAnim() } if (isSuppressing) { resetStates() return nothingAnim() } return enterAnim() } if (isPopping) { resetStates() return popAnim() } if (isSuppressing) { resetStates() return nothingAnim() } return exitAnim() } 

L’activité et le fragment parent sont responsables de la définition des états de ces booléens. C’est plus facile de voir comment et où de mon exemple de projet.

Je n’utilise pas de fragments de support dans mon exemple, mais la même logique peut être utilisée avec eux et leur fonction onCreateAnimation

Un moyen simple de résoudre ce problème est d’utiliser la classe Fragment de cette bibliothèque au lieu de la classe de fragment de bibliothèque standard:

https://github.com/marksalpeter/contract-fragment

En outre, le package contient également un modèle de délégué utile appelé ContractFragment que vous pouvez trouver utile pour créer vos applications en tirant parti de la relation de fragment parent-enfant.

De la réponse ci-dessus de @kcoppock,

Si vous avez Activité-> Fragment-> Fragments (emstackment multiple, les aides suivantes), une modification mineure à la meilleure réponse IMHO.

 public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) { final Fragment parent = getParentFragment(); Fragment parentOfParent = null; if( parent!=null ) { parentOfParent = parent.getParentFragment(); } if( !enter && parent != null && parentOfParent!=null && parentOfParent.isRemoving()){ Animation doNothingAnim = new AlphaAnimation(1, 1); doNothingAnim.setDuration(getNextAnimationDuration(parent, DEFAULT_CHILD_ANIMATION_DURATION)); return doNothingAnim; } else if (!enter && parent != null && parent.isRemoving()) { // This is a workaround for the bug where child fragments disappear when // the parent is removed (as all children are first removed from the parent) // See https://code.google.com/p/android/issues/detail?id=55228 Animation doNothingAnim = new AlphaAnimation(1, 1); doNothingAnim.setDuration(getNextAnimationDuration(parent, DEFAULT_CHILD_ANIMATION_DURATION)); return doNothingAnim; } else { return super.onCreateAnimation(transit, enter, nextAnim); } }