web-dev-qa-db-fra.com

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

Voici le scénario: L'activité contient le fragment A, qui à son tour utilise getChildFragmentManager() pour ajouter des fragments A1 et A2 dans sa onCreate comme suit:

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

Jusqu'ici, tout va bien, tout se passe 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 enter du fragment B s'exécutent correctement, mais les fragments A1 et A2 disparaissent complètement. Lorsque nous inversons la transaction avec le bouton Précédent, ils s'initialisent correctement et s'affichent normalement pendant l'animation popEnter.

Au cours de mes brefs tests, cela a été plus étrange - si je définis les animations pour les fragments enfants (voir ci-dessous), l'animation exit s'exécute de manière intermittente lorsque nous ajoutons un fragment B

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

L’effet recherché est simple: je veux que l’animation exit (ou doit-elle être popExit?) Sur le fragment A (anim2) soit exécutée, animant l’ensemble du conteneur, y compris ses enfants imbriqués. 

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 avec les animations statiques. Apparemment, vous pouvez définir des animations sur une base individuelle (non globale pour l'ensemble de la transaction), ce qui signifie que les enfants peuvent avoir un ensemble d'animation statique 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 scénario de test mis à jour .

93
Delyan

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" les fragments encore présents en fournissant une image de ces fragments, tels qu'ils apparaissent à l'écran. Cette image sera utilisée comme arrière-plan pour le conteneur de fragments imbriqués. Ainsi, même si les vues du fragment imbriqué disparaissent, l'image simulera leur présence. De plus, je ne vois pas le problème de perdre l'interactivité avec les vues du fragment imbriqué, car je ne pense pas que vous voudriez que l'utilisateur agisse en conséquence lorsqu'ils sont en train d'être supprimés (probablement comme une action bien).

J'ai fait un petit exemple avec la mise en place de l'image de fond (quelque chose de basique).

34
Luksprog

Donc, il semble y avoir beaucoup de solutions de contournement 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 le faire. un cache bitmap de la mise en page.

Ayez une classe BaseFragment qui étend Fragment et faites en sorte que tous vos fragments étendent cette classe (pas uniquement des fragments enfants). 

Dans cette classe BaseFragment, ajoutez les éléments suivants:

// 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, cela demande réflexion. Toutefois, comme cette solution de contournement concerne la bibliothèque de support, vous ne courez pas le risque que l'implémentation sous-jacente change sauf si vous mettez à jour votre bibliothèque de support. Si vous construisez la bibliothèque de support à partir de la source, vous pouvez ajouter un accesseur pour le prochain ID de ressource d'animation à Fragment.Java et supprimer le besoin de réflexion.

Cette solution supprime le besoin de "deviner" la durée d'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 créer des animations personnalisées sur des fragments enfants (par exemple, si vous ' échange de fragments d’enfant avec différentes animations).

66
kcoppock

J'ai pu proposer une solution assez propre. OMI est le moins malin, et bien que ce soit techniquement la solution "dessinez une image", au moins sa solution abstraite par le fragment lib.

Assurez-vous que les frags de votre enfant 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 disparaître. Nous pouvons l'exploiter en ayant une animation qui dessine simplement les fragments enfants à l'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 peux penser est de garder une trace de cette durée. Je pourrais peut-être définir un nombre élevé, mais je crains que des problèmes de performances ne se produisent si vous continuez à dessiner cette animation quelque part.

30
Jayd16

J'affiche ma solution pour plus de clarté ... La solution est assez simple. Si vous essayez de reproduire l'animation de la 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 (le temps d’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>
14
Maurycy

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

7
Steven Byle

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);
}
4
peng gao

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

EDIT: J'ai fini par désimplémenter cette solution car il y avait d'autres problèmes. Square a récemment publié 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 en faisant quelque chose que Google ne veut pas qu'ils fassent.

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

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

Je pensais mettre en place cette solution pour aider les personnes confrontées à ce problème à l'avenir. Si vous suivez 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 parvient finalement à la conclusion d'utiliser une animation non-op sur les fragments enfants lors de l'animation du fragment parent. Cette solution n'est pas idéale car elle vous oblige à garder une trace de tous les fragments enfants, ce qui peut s'avérer fastidieux lors de l'utilisation d'un ViewPager avec FragmentPagerAdapter.

Depuis que j'utilise des fragments d'enfants partout, j'ai mis au point cette solution efficace et modulaire (elle peut donc être facilement supprimée), au cas où ils la répareraient et que cette animation non opérationnelle ne soit plus nécessaire.

Vous pouvez implémenter cela de nombreuses façons. J'ai choisi d'utiliser un singleton et je l'appelle ChildFragmentAnimationManager. Fondamentalement, il gardera la trace d'un fragment enfant pour moi en fonction de son parent et appliquera une animation non-op aux enfants lorsque cela leur sera demandé.

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 et tous vos Fragments (au moins vos Fragments Enfant). J'ai déjà eu cette classe et je l'appelle BaseFragment. Lorsqu'une vue de fragments est créée, nous l'ajoutons au ChildFragmentAnimationManager et nous le supprimons lorsqu'il est détruit. Vous pouvez le faire avec Attacher/Détacher ou d’autres méthodes correspondantes dans la séquence. Ma logique pour choisir de 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 fragment de FragmentPagerAdapter contient, mais seulement de trois.

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 les animer 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();

De plus, voici le fichier no_anim.xml qui se trouve 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 exemple, vous avez un fragment d’enfant, implémentant un code personnalisé dans le fragment parent pour garder une trace de chaque enfant. J'y suis allé et ce n'est pas amusant.

2
spierce7

J'avais le même problème avec fragment de carte. Il a continué à disparaître pendant l'animation de sortie de son fragment conteneur. La solution de contournement consiste à ajouter une animation pour le fragment de carte enfant, qui restera visible pendant l'animation de sortie du fragment parent. L'animation du fragment enfant maintient son alpha à 100% pendant toute 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>
1
Evgenii

Le problème est résolu dans androidx.fragment:fragment:1.2.0-alpha02. Voir https://issuetracker.google.com/issues/11667531 pour plus de détails.

0
sergej shafarenka

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

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

Le paquet contient également un modèle de délégué utile appelé ContractFragment qui pourrait vous être utile pour créer vos applications en exploitant la relation fragment fragment-parent.

0
Mark Salpeter

Pour animer la disparition de fragments traités, nous pouvons forcer le retour de pile sur ChildFragmentManager. Cela déclenchera une animation de transition. Pour ce faire, nous devons rattraper l’événement OnBackButtonPressed ou écouter les changements de pile.

Voici un 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();
0
Mateusz Biedron

J'ai récemment rencontré ce problème dans ma question: Fragments imbriqués effectuant une transition incorrecte

J'ai une solution qui résout ce problème sans enregistrer un bitmap, ni utiliser la réflexion ou toute autre méthode peu 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 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 presses à l'envers sont également gérées avec succès.

Le gros de la solution se trouve dans la fonction onCreateAnimator de mon élément BaseFragment (le fragment prolongé par mes enfants et mes parents):

   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

0
zafrani

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

Le problème fondamental est que les fragments enfants sont immédiatement détruits avant l'animation sortant du fragment parent.

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

Comme d'autres y ont échappé, cacher le PARENT (et non l'enfant) avant son renvoi 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 à quel moment l'animation est terminée. Vous pouvez ainsi finaliser en toute sécurité une partie du fragment parent (supprimer). Si vous ne le faites pas rapidement, vous risquez de tuer l'animation. L'animation N.B est effectuée sur une file d'attente asynchrone.

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

0
Ed Manners

Je pense avoir trouvé une meilleure solution à ce problème que la capture instantanée du fragment actuel sur un bitmap, comme le suggérait Luksprog.

L'astuce consiste à masquer le fragment en cours de suppression ou de détachement et une fois les animations terminées, le fragment est supprimé ou détaché dans sa propre transaction.

Imaginons que nous ayons 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

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();
    }
}
0
Danilo

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

si vous avez Activité-> Fragment-> Fragments (empilement multiple, l'aide suivante), une modification mineure à la meilleure réponse à mon humble avis.

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);
    }
}
0
alekshandru