18 votes

Transition incorrecte des fragments imbriqués

Bonjour les bons programmeurs de stack overflow ! J'ai passé une bonne semaine avec ce problème et je suis maintenant très désespérée pour une solution.


Le scénario

J'utilise Android.app.Fragment, à ne pas confondre avec les fragments de support.

J'ai 6 fragments d'enfants nommés :

  • FragmentOne
  • FragmentTwo
  • FragmentThree
  • FragmentA
  • FragmentB
  • FragmentC

J'ai 2 fragments de parents nommés :

  • FragmentNumeric
  • FragmentAlpha

J'ai une activité nommée :

  • MainActivity

Ils se comportent de la manière suivante :

  • Les fragments enfants sont des fragments qui ne montrent qu'une vue, ils ne montrent ni ne contiennent de fragments.
  • Les fragments parents remplissent leur vue entière avec un seul fragment enfant. Ils sont capables de remplacer le fragment enfant par d'autres fragments enfants.
  • L'activité remplit la majeure partie de sa vue avec un fragment parent. Elle peut le remplacer par d'autres fragments parents. Ainsi, à tout moment, l'écran n'affiche qu'un seul fragment enfant.

Comme vous l'avez probablement deviné,

FragmentNumeric montre les fragments d'enfants FragmentOne , FragmentTwo y FragmentThree .

FragmentAlpha montre les fragments d'enfants FragmentA , FragmentB et FragmentC .


Le problème

J'essaie d'animer des fragments de parents et d'enfants. La transition des fragments enfants se fait en douceur et comme prévu. Cependant, lorsque je fais la transition vers un nouveau fragment parent, l'aspect est terrible. Le fragment enfant semble exécuter une transition indépendante de son fragment parent. Et le fragment enfant semble être retiré du fragment parent également. Un gif de ce phénomène peut être visualisé ici https://imgur.com/kOAotvk . Remarquez ce qui se passe lorsque je clique sur Show Alpha.

Les questions et réponses les plus proches que j'ai pu trouver sont ici : Les fragments imbriqués disparaissent pendant l'animation de transition mais toutes les réponses sont des bidouillages insatisfaisants.


Fichiers XML de l'animateur

J'ai les effets d'animation suivants (la durée est longue à des fins de test) :

fragment_enter.xml

<?xml version="1.0" encoding="utf-8"?>
<set>
    <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="4000"
        android:interpolator="@android:anim/linear_interpolator"
        android:propertyName="xFraction"
        android:valueFrom="1.0"
        android:valueTo="0" />
</set>

fragment_exit.xml

<?xml version="1.0" encoding="utf-8"?>
<set>
    <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="4000"
        android:interpolator="@android:anim/linear_interpolator"
        android:propertyName="xFraction"
        android:valueFrom="0"
        android:valueTo="-1.0" />
</set>

fragment_pop.xml

<?xml version="1.0" encoding="utf-8"?>
<set>
    <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="4000"
        android:interpolator="@android:anim/linear_interpolator"
        android:propertyName="xFraction"
        android:valueFrom="0"
        android:valueTo="1.0" />
</set>

fragment_push.xml

<?xml version="1.0" encoding="utf-8"?>
<set>
    <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="4000"
        android:interpolator="@android:anim/linear_interpolator"
        android:propertyName="xFraction"
        android:valueFrom="-1.0"
        android:valueTo="0" />
</set>

fragment_rien.xml

<?xml version="1.0" encoding="utf-8"?>
<set>
    <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="4000" />
</set>

MainActivity.kt

Les choses à considérer : Le premier fragment parent, FragmentNumeric, n'a pas d'effets d'entrée, il est donc toujours prêt avec l'activité et n'a pas d'effets de sortie car rien ne sort. J'utilise également FragmentTransaction#add avec elle, alors que FragmentAlpha utilise FragmentTransaction#replace

class MainActivity : AppCompatActivity {

    fun showFragmentNumeric(){
        this.fragmentManager.beginTransaction()
                .setCustomAnimations(R.animator.fragment_nothing,
                                     R.animator.fragment_nothing,
                                     R.animator.fragment_push,
                                     R.animator.fragment_pop)
                .add(this.contentId, FragmentNumeric(), "FragmentNumeric")
                .addToBackStack("FragmentNumeric")
                .commit()
}

    fun showFragmentAlpha(){
        this.fragmentManager.beginTransaction()
                .setCustomAnimations(R.animator.fragment_enter,
                                     R.animator.fragment_exit,
                                     R.animator.fragment_push,
                                     R.animator.fragment_pop)
                .replace(this.contentId, FragmentAlpha(), "FragmentAlpha")
                .addToBackStack("FragmentAlpha")
                .commit()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (savedInstanceState == null) {
            showFragmentNumeric()
        }
    }
}

FragmentNumérique

Fait la même chose que l'activité en termes d'affichage rapide de son premier fragment enfant.

class FragmentNumeric : Fragment {

    fun showFragmentOne(){
            this.childFragmentManager.beginTransaction()
                    .setCustomAnimations(R.animator.fragment_nothing,
                                         R.animator.fragment_nothing,
                                         R.animator.fragment_push,
                                         R.animator.fragment_pop)
                    .add(this.contentId, FragmentOne(), "FragmentOne")
                    .addToBackStack("FragmentOne")
                    .commit()
    }

    fun showFragmentTwo(){
        this.childFragmentManager.beginTransaction()
                .setCustomAnimations(R.animator.fragment_enter,
                                     R.animator.fragment_exit,
                                     R.animator.fragment_push,
                                     R.animator.fragment_pop)
                .replace(this.contentId, FragmentTwo(), "FragmentTwo")
                .addToBackStack("FragmentTwo")
                .commit()
    }

    fun showFragmentThree(){
        this.childFragmentManager.beginTransaction()
                .setCustomAnimations(R.animator.fragment_enter,
                                     R.animator.fragment_exit,
                                     R.animator.fragment_push,
                                     R.animator.fragment_pop)
                .replace(this.contentId, FragmentThree(), "FragmentThree")
                .addToBackStack("FragmentThree")
                .commit()
    }

    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        if (savedInstanceState == null) {
            if (this.childFragmentManager.backStackEntryCount <= 1) {
                showFragmentOne()
            }
        }
    }
}

Autres fragments

Le FragmentAlpha suit le même schéma que le FragmentNumeric, en remplaçant les Fragments Un, Deux et Trois par les Fragments A, B et C respectivement.

Les fragments enfants affichent simplement la vue XML suivante et définissent dynamiquement son texte et son écouteur de clic de bouton pour appeler une fonction du fragment ou de l'activité parent.

vue_enfant_exemple.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/background"
    android:clickable="true"
    android:focusable="true"
    android:orientation="vertical">

    <TextView
        android:id="@+id/view_child_example_header"
        style="@style/Header"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/view_child_example_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"  />
</LinearLayout>

En utilisant dagger et quelques contrats, j'ai fait en sorte que les fragments enfants fassent un appel à leurs fragments parents et accueillent des activités en faisant quelque chose comme ce qui suit :

FragmentOne définit l'écouteur de clic de bouton à faire :

(parentFragment as FragmentNumeric).showFragmentTwo()

FragmentTwo définit l'écouteur de clic de bouton à faire :

(parentFragment as FragmentNumeric).showFragmentThree()

FragmentThree est différent, il définira l'écouteur de clic à faire :

(activity as MainActivity).showFragmentAlpha()

Quelqu'un a-t-il une solution pour ce problème ?


Mise à jour 1

J'ai ajouté un projet d'exemple comme demandé : https://github.com/zafrani/NestedFragmentTransitions

Une différence entre ce fragment et celui de ma vidéo originale est que le fragment parent n'utilise plus une vue avec la propriété xFraction. Il semble donc que l'animation d'entrée n'ait plus cet effet de chevauchement. Cependant, elle retire le fragment enfant du parent et les anime côte à côte. Une fois l'animation terminée, le Fragment Trois est remplacé par le Fragment A instantanément.

Mise à jour 2

Les deux vues de fragment parent et enfant utilisent la propriété xFraction. La clé est de supprimer l'animation de l'enfant lorsque le parent s'anime.

4voto

zafrani Points 2156

Je pense avoir trouvé un moyen de résoudre ce problème en utilisant Fragment#onCreateAnimator. Un gif de la transition peut être visualisé ici : https://imgur.com/94AvrW4 .

J'ai fait un PR pour le test, jusqu'à présent il fonctionne comme je l'attends et survit aux changements de configuration et supporte le bouton retour. Voici le lien https://github.com/zafrani/NestedFragmentTransitions/pull/1/files#diff-c120dd82b93c862b01c2548bdcafcb20R25

Le BaseFragment pour les fragments Parent et Child fait ceci pour onCreateAnimator()

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()
}

Les booléens sont définis dans différents scénarios qui sont plus faciles à voir dans le RP.

Les fonctions d'animation sont les suivantes :

private fun enterAnim(): Animator { 
        return AnimatorInflater.loadAnimator(activity, R.animator.fragment_enter)
    }

    private fun exitAnim(): Animator { 
        return AnimatorInflater.loadAnimator(activity, R.animator.fragment_exit)
    }

    private fun pushAnim(): Animator { 
        return AnimatorInflater.loadAnimator(activity, R.animator.fragment_push)
    }

    private fun popAnim(): Animator { 
        return AnimatorInflater.loadAnimator(activity, R.animator.fragment_pop)
    }

    private fun nothingAnim(): Animator { 
        return AnimatorInflater.loadAnimator(activity, R.animator.fragment_nothing)
    }

Je laisse la question ouverte au cas où quelqu'un trouverait une meilleure solution.

0voto

GensaGames Points 1513

Vous obtenez un tel résultat parce que vous utilisez des animations de sortie pour votre fragment. En fait, nous avons eu de tels problèmes avec l'animation du fragment à chaque fois, et nous avons finalement abandonné l'animation du fragment source pour utiliser la nôtre, chaque fois que nous effectuons une transition.

1) Pour vérifier ce comportement, vous pouvez simplement supprimer l'animation de sortie pour le fragment, et tout sera ok. Dans la plupart des cas, cela devrait suffire, car l'animation de sortie est très spécifique et utilisée uniquement pour la gestion d'un seul fragment (pas dans votre cas, avec des enfants).

getFragmentManager().beginTransaction()
                .setCustomAnimations(R.animator.enter_anim_frag1,
                                     0,
                                     R.animator.enter_anim_frag2,
                                     0)
                .replace(xxx, Xxx1, Xxx2)
                .addToBackStack(null)
                .commit()

2) Une autre option, qui pourrait convenir, est de penser à la structure de l'application et d'essayer d'éviter de remplacer les fragments par des animations. Si vous avez besoin de remplacer, n'utilisez pas d'animation, et vous pouvez ajouter n'importe quelle animation (y compris l'entrée et la sortie), mais seulement en ajoutant.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X