Les fragments imbriqués disparaissent pendant l'animation de transition

99

Voici le scénario: l'activité contient un fragment A, qui à son tour utilise getChildFragmentManager()pour ajouter des fragments A1et A2en onCreatequelque sorte:

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

Jusqu'à présent, 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 enteranimations du fragment Bs'exécutent correctement mais les fragments A1 et A2 disparaissent entièrement . Lorsque nous annulons la transaction avec le bouton Retour, ils s'initialisent correctement et s'affichent normalement pendant l' popEnteranimation.

Lors de mes brefs tests, cela est devenu plus étrange - si j'ai défini les animations pour les fragments enfants (voir ci-dessous), l' exitanimation s'exécute par intermittence lorsque nous ajoutons un fragmentB

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 imbriqués.

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

Edit : Veuillez trouver un cas de test ici

Edit2 : Merci à @StevenByle de m'avoir poussé à continuer d'essayer avec les animations statiques. Apparemment, vous pouvez définir des animations par opération (et non globales pour l'ensemble de la transaction), ce qui signifie que les enfants peuvent avoir un ensemble d'animations statiques indéfini, tandis que leur parent peut avoir une animation différente et que le tout peut être validé en une seule transaction . Voir la discussion ci-dessous et le projet de cas de test mis à jour .

Delyan
la source
Qu'est-ce que R.id.fragmentHolderpar rapport à A, A1, A2, etc.?
CommonsWare
fragmentHolder est un identifiant dans la mise en page de l'activité, fragment {One, Two} Holder dans la mise en page du fragment A. Les trois sont distincts. Le fragment A a été initialement ajouté dans fragmentHolder (c'est-à-dire que le fragment B remplace le fragment A).
Delyan
J'ai créé un exemple de projet ici: github.com/BurntBrunch/NestedFragmentsAnimationsTest , il y a aussi un apk inclus dans le référentiel. C'est un bogue vraiment ennuyeux et je cherche un moyen de le contourner (en supposant que ce ne soit pas dans mon code).
Delyan
J'en sais un peu plus sur cette question maintenant. La raison pour laquelle les fragments disparaissent est que les enfants gèrent les événements du cycle de vie avant le parent. Essentiellement, A1 et A2 sont supprimés avant A et comme ils n'ont pas d'animations définies, ils disparaissent brusquement. Un moyen d'atténuer quelque peu cela est de supprimer explicitement A1 et A2 dans la transaction qui remplace A. De cette façon, ils s'animent lorsqu'ils quittent, mais leur vitesse d'animation est au carré, puisque le conteneur parent est également en train de s'animer. Une solution qui ne produit pas cet artefact serait appréciée.
Delyan
Le changement (en remplaçant le fragment de départ) que vous mentionnez dans la question est le vrai que vous voulez faire ou c'est juste un exemple? Vous n'appellerez la changeFragmentméthode qu'une seule fois?
Luksprog

Réponses:

37

Afin d'éviter que l'utilisateur ne voie les fragments imbriqués disparaître lorsque le fragment parent est supprimé / remplacé dans une transaction, vous pouvez "simuler" ces fragments toujours présents en fournissant une image d'eux, tels qu'ils apparaissent à l'écran. Cette image sera utilisée comme arrière-plan pour le conteneur de fragments imbriqués donc même si les vues du fragment imbriqué disparaissent, l'image simulera leur présence. De plus, je ne vois pas la perte de l'interactivité avec les vues du fragment imbriqué comme un problème car je ne pense pas que vous voudriez que l'utilisateur agisse sur eux alors qu'ils sont juste en train d'être supprimés (probablement comme une action de l'utilisateur comme bien).

J'ai fait un petit exemple avec la configuration de l'image d'arrière-plan (quelque chose de basique).

Luksprog
la source
1
J'ai décidé de vous attribuer la prime, puisque c'était la solution que j'ai fini par utiliser. Merci beaucoup pour votre temps!
Delyan
17
Wow, tellement sale. Les longueurs que nous, les développeurs Android, devons faire juste pour un peu de finesse
Dean Wild
1
J'ai un Viewpager à partir du deuxième onglet Je remplace un autre fragment et quand j'appuie dessus, je dois afficher le deuxième onglet du viewpager, il s'ouvre mais il affiche une page vierge. J'ai essayé ce que vous avez suggéré dans le fil ci-dessus, mais c'est toujours la même chose.
Harish
Le problème persiste lorsque l'utilisateur revient comme @Harish l'a écrit
Ewoks
1
Wow sérieusement? son 2018 et c'est toujours une chose? :(
Archie G. Quiñones
69

Il semble donc y avoir beaucoup de solutions de contournement différentes pour cela, mais sur la base de la réponse de @ Jayd16, je pense avoir trouvé une solution fourre-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.

Ayez une BaseFragmentclasse qui s'étend Fragmentet faites en sorte que tous vos fragments étendent cette classe (pas seulement les fragments enfants).

Dans cette BaseFragmentclasse, 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;
    }
}

Cela nécessite malheureusement une réflexion; cependant, puisque cette solution de contournement concerne la bibliothèque de prise en charge, vous ne courez pas le risque que l'implémentation sous-jacente change à moins que vous ne mettiez à jour votre bibliothèque de prise en charge. Si vous créez la bibliothèque de support à partir de la source, vous pouvez ajouter un accesseur pour l'ID de ressource d'animation suivant àFragment.java et supprimer le besoin de réflexion.

Cette solution supprime le besoin de "deviner" la durée de l'animation du parent (pour que l'animation "ne rien faire" ait la même durée que l'animation de sortie du parent), et vous permet de toujours faire des animations personnalisées sur des fragments enfants (par exemple si vous ' re échange de fragments enfants avec différentes animations).

Kevin Coppock
la source
5
C'est ma solution préférée dans le fil. Ne nécessite pas de bitmap, ne nécessite aucun code supplémentaire dans le fragment enfant et ne divulgue pas vraiment d'informations du fragment parent vers le fragment enfant.
jacobhyphenated le
1
@EugenPechanec Vous avez besoin du nextAnim du fragment parent - pas de celui de l'enfant. Exactement.
Kevin Coppock
1
Malheureusement, cette approche provoque une fuite de mémoire pour les fragments enfants et implicitement pour le fragment parent ainsi que sur les versions Android inférieures à Lollipop :(
Cosmin
8
Merci pour cette solution très utile, mais elle nécessite une petite mise à jour pour fonctionner avec la bibliothèque de support actuelle (27.0.2, je ne sais pas quelle version a cassé ce code). mNextAnimest maintenant à l'intérieur d'un mAnimationInfoobjet. Vous pouvez y accéder comme ceci:Field animInfoField = Fragment.class.getDeclaredField("mAnimationInfo"); animInfoField.setAccessible(true); Object animationInfo = animInfoField.get(fragment); Field nextAnimField = animationInfo.getClass().getDeclaredField("mNextAnim");
David Lericolais
5
@DavidLericolais, souhaite ajouter une autre ligne de code après la vôtre. val nextAnimResource = nextAnimField.getInt(animationInfo);pour remplacer la ligneint nextAnimResource = nextAnimField.getInt(fragment);
tingyik90
32

J'ai pu trouver une solution assez propre. IMO c'est le moins hacky, et bien que ce soit techniquement la solution "dessiner un bitmap" au moins son abstraite par le fragment lib.

Assurez-vous que vos frags enfants remplacent une classe parent 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, ils seront animés au lieu de clignoter. Nous pouvons exploiter cela en ayant une animation qui dessine simplement les fragments enfants en alpha complet pendant une durée. De cette façon, ils resteront visibles dans le fragment parent pendant qu'il s'anime, donnant le comportement souhaité.

Le seul problème auquel je pense est de garder une trace de cette durée. Je pourrais peut-être le définir sur un nombre élevé, mais j'ai peur que cela puisse avoir des problèmes de performances s'il dessine toujours cette animation quelque part.

Jayd16
la source
Cela aide, merci. La valeur de la durée n'a pas d'importance
iscariot
solution la plus propre à ce jour
Liran Cohen
16

Je poste 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 (la durée de l'animation d'entrée / sortie de mes parents est de 250 ms)

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="0" android:toXDelta="0" android:duration="250" />
</set>
Maurycy
la source
J'ai fait quelque chose de très similaire, mais j'ai utilisé "show" plutôt que "add" lors de la mise à jour de l'enfant. J'ai également ajouté "getChildFragmentManager (). ExecutePendingTransactions ()", bien que je ne sois pas sûr que ce soit strictement nécessaire. Cette solution fonctionne très bien, cependant, et ne nécessite pas de "fournir une image" du fragment comme certains le suggèrent.
Brian Yencho
C'était génial. Cependant, je recevais le délai lors du changement de fragments de mon enfant. pour éviter cela, il suffit de définir le 2ème paramètre sans animation:fragmentTransaction.setCustomAnimations(R.anim.none, 0, R.anim.none, R.anim.none)
ono
7

Je comprends que cela ne résoudra peut-être pas complètement votre problème, mais peut-être que cela conviendra aux besoins de quelqu'un d'autre, vous pouvez ajouter des animations enter/ exitet popEnter/ popExità vos enfants Fragmentqui ne déplacent / n'animent pas réellement les Fragments. Tant que les animations ont la même durée / décalage que leurs Fragmentanimations parentes , elles sembleront se déplacer / s'animer avec l'animation du parent.

Steven Byle
la source
1
J'ai attribué la prime à Luksprog car sa solution fonctionne universellement. J'ai essayé l'astuce des animations statiques (la durée n'a pas d'importance - une fois que le parent est parti, les vues disparaissent évidemment) mais elles ne fonctionnent pas dans tous les cas possibles (voir mes commentaires sous la question). En outre, cette approche fuit l'abstraction, car le parent d'un fragment avec des enfants doit être informé de ce fait et prendre des mesures supplémentaires pour définir les animations des enfants. En tout cas, merci beaucoup pour votre temps!
Delyan
D'accord, il s'agit plus d'une solution de contournement qu'une solution étanche et peut être considérée comme légèrement fragile. Mais cela fonctionnera pour les cas simples.
Steven Byle
4

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);
}
peng gao
la source
Je vous remercie! Peut-être pas la meilleure solution mais peut-être la plus simple.
bug56 du
2

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

EDIT: J'ai fini par désimplémenter cette solution car il y avait d'autres problèmes que cela posait. Square a récemment sorti 2 bibliothèques qui remplacent des fragments. Je dirais que cela pourrait en fait ê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

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

J'ai pensé que je proposerais cette solution pour aider les personnes qui auront ce problème à l'avenir. Si vous retracez la conversation des affiches originales avec d'autres personnes et que vous regardez le code qu'il a publié, vous verrez que l'affiche originale arrive finalement à la conclusion de l'utilisation d'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 fastidieux lors de l'utilisation d'un ViewPager avec FragmentPagerAdapter.

Depuis que j'utilise Child Fragments partout, j'ai proposé cette solution qui est efficace et modulaire (afin qu'elle puisse être facilement supprimée) au cas où ils le répareraient un jour et que cette animation sans opération ne serait plus nécessaire.

Il existe de nombreuses façons de mettre cela en œuvre. J'ai choisi d'utiliser un singleton, et je l'appelle ChildFragmentAnimationManager. Il gardera essentiellement une trace d'un fragment enfant pour moi en fonction de son parent et appliquera une animation sans opération aux enfants lorsqu'on le lui demandera.

public class ChildFragmentAnimationManager {

private static ChildFragmentAnimationManager instance = null;

private Map<Fragment, List<Fragment>> fragmentMap;

private ChildFragmentAnimationManager() {
    fragmentMap = new HashMap<Fragment, List<Fragment>>();
}

public static ChildFragmentAnimationManager instance() {
    if (instance == null) {
        instance = new ChildFragmentAnimationManager();
    }
    return instance;
}

public FragmentTransaction animate(FragmentTransaction ft, Fragment parent) {
    List<Fragment> 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<Fragment> children = getChildren(parent);
    children.add(child);
}

public void removeChild(Fragment parent, Fragment child) {
    List<Fragment> children = getChildren(parent);
    children.remove(child);
}

private List<Fragment> getChildren(Fragment parent) {
    List<Fragment> children;

    if ( fragmentMap.containsKey(parent) ) {
        children = fragmentMap.get(parent);
    } else {
        children = new ArrayList<Fragment>(3);
        fragmentMap.put(parent, children);
    }

    return children;
}

}

Ensuite, vous devez avoir une classe qui étend Fragment que tous vos fragments étendent (au moins vos fragments enfants). J'avais déjà cette classe, et je l'appelle BaseFragment. Lorsqu'une vue de fragments est créée, nous l'ajoutons à ChildFragmentAnimationManager, et nous la supprimons lorsqu'elle est détruite. Vous pouvez le faire onAttach / Detach ou d'autres méthodes de correspondance dans la séquence. Ma logique pour choisir Créer / Détruire une vue était parce 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 mieux fonctionner avec les ViewPagers qui utilisent des fragments, car vous ne garderez pas trace de chaque fragment qu'un FragmentPagerAdapter contient, mais plutôt de 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 en mémoire par le fragment parent, vous pouvez appeler animate sur eux 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, pour que vous l'ayez, voici le fichier no_anim.xml qui va dans votre dossier res / anim:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:anim/linear_interpolator">
    <translate android:fromXDelta="0" android:toXDelta="0"
        android:duration="1000" />
</set>

Encore une fois, je ne pense pas que cette solution soit parfaite, mais elle est bien meilleure que pour chaque instance, vous avez un fragment enfant, implémentant un code personnalisé dans le fragment parent pour suivre chaque enfant. J'y suis allé et ce n'est pas amusant.

spierce7
la source
1

Je pense avoir trouvé une meilleure solution à ce problème que de créer un instantané du fragment actuel en une image bitmap, comme Luksprog l'a suggéré.

L'astuce est de se cacher le fragment en cours de suppression ou de détachement 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 ayons FragmentAet 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 tu fais

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

fragmentA.removeMe = true;

Passons maintenant à 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();
    }
}
Danilo
la source
Le popBackStack ne provoquerait-il pas une erreur parce qu'il essaie d'afficher un fragment qui a été détaché?
Alexandre
1

J'avais le même problème avec le fragment de carte. Il a continué à disparaître pendant l'animation de sortie de son fragment contenant. La solution de contournement consiste à ajouter 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

<?xml version="1.0" encoding="utf-8"?>    
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <objectAnimator
        android:propertyName="alpha"
        android:valueFrom="1.0"
        android:valueTo="1.0"
        android:duration="@integer/keep_child_fragment_animation_duration" />
</set>

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

<resources>
  <integer name="keep_child_fragment_animation_duration">500</integer>
</resources>
Evgenii
la source
0

Pour animer la disparition des fragments imbriqués, nous pouvons forcer le pop back stack sur ChildFragmentManager. Cela déclenchera l'animation de transition. Pour ce faire, nous devons rattraper l'événement OnBackButtonPressed ou écouter les changements de backstack.

Voici un exemple avec du 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();
Mateusz Biedron
la source
0

J'ai récemment rencontré ce problème dans ma question: les fragments imbriqués ne transitent pas correctement

J'ai une solution qui résout ce problème sans enregistrer une image bitmap, ni utiliser la réflexion ou toute autre méthode insatisfaisante.

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 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 contre-pressions sont également gérées avec succès.

La majeure partie de la solution se trouve dans la fonction onCreateAnimator de mon BaseFragment (le fragment étendu par mes enfants et les fragments parents) 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. Il est plus facile de voir comment et où à partir 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

zafrani
la source
0

Un moyen simple de résoudre ce problème consiste à utiliser la Fragmentclasse de cette bibliothèque au lieu de la classe de fragment de bibliothèque standard:

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

En remarque, le package contient également un modèle de délégué utile appelé ContractFragmentque vous pourriez trouver utile pour créer vos applications en exploitant la relation de fragment parent-enfant.

Mark Salpeter
la source
0

D'après la réponse ci-dessus de @kcoppock,

si vous avez Activity-> Fragment-> Fragments (empilement multiple, ce qui suit aide), 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);
    }
}
Alekshandru
la source
0

Mon problème concernait la suppression du fragment parent (ft.remove (fragment)), les animations enfants ne se produisaient pas.

Le problème de base est que les fragments enfants sont immédiatement DÉTRUITS AVANT que le fragment parents quitte l'animation.

Les animations personnalisées des fragments enfants ne sont pas exécutées lors de la suppression du fragment parent

Comme d'autres l'ont éludé, cacher le PARENT (et non l'enfant) avant le retrait du PARENT est la voie à suivre.

            val ft = fragmentManager?.beginTransaction()
            ft?.setCustomAnimations(R.anim.enter_from_right,
                    R.anim.exit_to_right)
            if (parentFragment.isHidden()) {
                ft?.show(vehicleModule)
            } else {
                ft?.hide(vehicleModule)
            }
            ft?.commit()

Si vous souhaitez réellement supprimer le parent, vous devez probablement configurer un écouteur sur votre animation personnalisée pour savoir quand l'animation est terminée, vous pouvez donc en toute sécurité effectuer une finalisation sur le fragment parent (supprimer). Si vous ne le faites pas, en temps opportun, vous pourriez finir par tuer l'animation. NB l'animation se fait sur sa propre file d'attente asynchrone.

BTW, vous n'avez pas besoin d'animations personnalisées sur le fragment enfant, car elles hériteront des animations parent.

Ed Manners
la source
0

Vieux fil, mais au cas où quelqu'un trébucherait ici:

Toutes les approches ci-dessus me semblent très peu attrayantes, la solution bitmap est très sale et non performante; les autres exigent que les fragments enfants connaissent la durée de la transition utilisée dans la transaction utilisée pour créer le fragment enfant en question. Une meilleure solution à mes yeux, c'est quelque chose comme ce qui suit:

val currentFragment = supportFragmentManager.findFragmentByTag(TAG)
val transaction = supportFragmentManager
    .beginTransaction()
    .setCustomAnimations(anim1, anim2, anim1, anim2)
    .add(R.id.fragmentHolder, FragmentB(), TAG)
if (currentFragment != null) {
    transaction.hide(currentFragment).commit()
    Handler().postDelayed({
        supportFragmentManager.beginTransaction().remove(currentFragment).commit()
    }, DURATION_OF_ANIM)
} else {
    transaction.commit()
}

Nous masquons simplement le fragment actuel et ajoutons le nouveau fragment, lorsque l'animation est terminée, nous supprimons l'ancien fragment. De cette façon, il est géré en un seul endroit et aucun bitmap n'est créé.

jeppeman
la source