Exemple de travail complet du scénario d’animation à trois fragments Gmail?

TL; DR: Je cherche un échantillon complet de ce que je vais appeler le scénario “Animation par trois fragments de Gmail”. Plus précisément, nous voulons commencer avec deux fragments, comme ceci:

deux fragments

Sur certains événements de l’interface utilisateur (par exemple, en tapant quelque chose dans le fragment B), nous voulons:

  • Fragment A pour faire glisser l’écran vers la gauche
  • Le fragment B glisse sur le bord gauche de l’écran et rétrécit pour prendre la place laissée par le fragment A
  • Fragment C à glisser du côté droit de l’écran et à prendre la place laissée par le fragment B

Et, sur une pression sur le bouton BACK, nous voulons que cet ensemble d’opérations soit inversé.

Maintenant, j’ai vu beaucoup d’implémentations partielles; Je vais en examiner quatre ci-dessous. Au-delà d’être incomplets, ils ont tous leurs problèmes.


@Reto Meier a consortingbué à cette réponse populaire à la même question de base, indiquant que vous utiliseriez setCustomAnimations() avec une FragmentTransaction . Pour un scénario à deux fragments (par exemple, vous ne voyez initialement que le fragment A et souhaitez le remplacer par un nouveau fragment B utilisant des effets animés), je suis tout à fait d’accord. Toutefois:

  • Comme vous ne pouvez spécifier qu’une animation “in” et une animation “out”, je ne vois pas comment vous gériez toutes les différentes animations requirejses pour le scénario à trois fragments
  • Le dans son exemple de code utilise des positions câblées en pixels, ce qui semblerait peu pratique compte tenu des différentes tailles d’écran, mais setCustomAnimations() nécessite des ressources d’animation, empêchant la possibilité de définir ces éléments en Java.
  • Je ne comprends pas comment les animateurs d’objects pour l’échelle android:layout_weight avec des choses comme android:layout_weight dans un LinearLayout pour allouer de l’espace en pourcentage
  • Je ne comprends pas comment le fragment C est traité au départ ( GONE ? android:layout_weight de 0 ? Pré-animé à une échelle de 0? Autre chose?)

@Roman Nurik souligne que vous pouvez animer n’importe quelle propriété , y compris celles que vous définissez vous-même. Cela peut aider à résoudre le problème des positions câblées, au prix de l’invention de votre propre sous-classe de gestionnaire de disposition personnalisée. Cela aide certains, mais je suis toujours déconcerté par le rest de la solution de Reto.


L’auteur de cette entrée pastebin montre un pseudo-code alléchant, disant essentiellement que les trois fragments résideraient initialement dans le conteneur, avec le fragment C masqué au début via une opération de transaction hide() . Nous affichons ensuite show() C et hide() A lorsque l’événement d’interface utilisateur se produit. Cependant, je ne vois pas comment cela gère le fait que B change de taille. Cela dépend aussi du fait que vous pouvez apparemment append plusieurs fragments au même conteneur, et je ne suis pas sûr que ce soit un comportement fiable à long terme (sans parler de casser findFragmentById() , bien que je puisse vivre avec cette).


L’auteur de cet article de blog indique que Gmail n’utilise pas setCustomAnimations() , mais utilise directement des animateurs d’object (“il vous suffit de modifier la marge gauche de la vue racine + de modifier la largeur de la vue droite”). Cependant, il s’agit toujours d’une solution à deux fragments AFAICT, et l’implémentation montre une fois de plus les dimensions en pixels durs.


Je vais continuer à twigr cela, donc je vais peut-être y répondre moi-même un jour, mais j’espère vraiment que quelqu’un a mis au point la solution à trois fragments pour ce scénario d’animation et peut poster le code (ou un lien). Les animations sous Android me donnent envie de me tirer les cheveux, et ceux d’entre vous qui m’ont vu savent que c’est une entreprise largement infructueuse.

J’ai téléchargé ma proposition sur github (fonctionne avec toutes les versions Android, mais l’accélération matérielle de la vue est fortement recommandée pour ce type d’animations. Pour les périphériques non accélérés par le matériel, une implémentation de la mise en cache bitmap devrait être mieux adaptée)

La vidéo de démonstration avec l’animation est ici (cause du taux de trame lent de la cadence d’images. La performance réelle est très rapide)


Usage:

 layout = new ThreeLayout(this, 3); layout.setAnimationDuration(1000); setContentView(layout); layout.getLeftView(); //<---inflate FragmentA here layout.getMiddleView(); //<---inflate FragmentB here layout.getRightView(); //<---inflate FragmentC here //Left Animation set layout.startLeftAnimation(); //Right Animation set layout.startRightAnimation(); //You can even set interpolators 

Explication :

Création d'un nouveau RelativeLayout personnalisé (ThreeLayout) et de deux animations personnalisées (MyScalAnimation, MyTranslateAnimation)

ThreeLayout obtient le poids du volet gauche en tant que param, en supposant que l'autre vue visible ait un weight=1 .

Donc, le new ThreeLayout(context,3) crée une nouvelle vue avec 3 enfants et le volet gauche avec 1/3 de l’écran total. L'autre vue occupe tout l'espace disponible.

Il calcule la largeur à l'exécution, une implémentation plus sûre est que les dimensions sont calculées la première fois dans draw (). au lieu de en post ()

Les animations Scale et Translate redimensionnent et déplacent la vue, et non la pseudo-échelle. Notez que fillAfter(true) n'est utilisé nulle part.

View2 a raison_de View1

et

View3 a raison_de View2

Après avoir défini ces règles, RelativeLayout prend en charge tout le rest. Les animations modifient les margins (en mouvement) et [width,height] à l’échelle

Pour accéder à chaque enfant (afin que vous puissiez le gonfler avec votre fragment, vous pouvez appeler

 public FrameLayout getLeftLayout() {} public FrameLayout getMiddleLayout() {} public FrameLayout getRightLayout() {} 

Ci-dessous sont présentées les 2 animations


Étape 1

--- EN écran ----------! ----- OUT ----

[View1] [_____ View2 _____] [_____ View3_____]

Stage2

--OUT -! -------- IN Screen ------

[View1] [View2] [_____ View3_____]

OK, voici ma propre solution, dérivée de l’application Email AOSP, selon la suggestion de Christopher dans les commentaires de la question.

https://github.com/commonsguy/cw-omnibus/tree/master/Animation/ThreePane

La solution de @windowswire rappelle la mienne, bien qu’il utilise de l’ Animation classique plutôt que des animateurs, et il utilise les règles de RelativeLayout pour imposer le positionnement. Du sharepoint vue de la prime, il obtiendra probablement la prime, à moins que quelqu’un d’autre avec une solution plus lisse ne publie encore une réponse.


En un mot, le ThreePaneLayout dans ce projet est une sous-classe LinearLayout , conçue pour fonctionner avec trois enfants dans le paysage. Les largeurs de ces enfants peuvent être définies dans le XML de la mise en page, par n’importe quel moyen désiré – je montre en utilisant des poids, mais vous pouvez avoir des largeurs spécifiques définies par des ressources de dimension ou autre. Le troisième enfant – le fragment C dans la question – devrait avoir une largeur de zéro.

 package com.commonsware.android.anim.threepane; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.content.Context; import android.util.AtsortingbuteSet; import android.view.View; import android.widget.LinearLayout; public class ThreePaneLayout extends LinearLayout { private static final int ANIM_DURATION=500; private View left=null; private View middle=null; private View right=null; private int leftWidth=-1; private int middleWidthNormal=-1; public ThreePaneLayout(Context context, AtsortingbuteSet attrs) { super(context, attrs); initSelf(); } void initSelf() { setOrientation(HORIZONTAL); } @Override public void onFinishInflate() { super.onFinishInflate(); left=getChildAt(0); middle=getChildAt(1); right=getChildAt(2); } public View getLeftView() { return(left); } public View getMiddleView() { return(middle); } public View getRightView() { return(right); } public void hideLeft() { if (leftWidth == -1) { leftWidth=left.getWidth(); middleWidthNormal=middle.getWidth(); resetWidget(left, leftWidth); resetWidget(middle, middleWidthNormal); resetWidget(right, middleWidthNormal); requestLayout(); } translateWidgets(-1 * leftWidth, left, middle, right); ObjectAnimator.ofInt(this, "middleWidth", middleWidthNormal, leftWidth).setDuration(ANIM_DURATION).start(); } public void showLeft() { translateWidgets(leftWidth, left, middle, right); ObjectAnimator.ofInt(this, "middleWidth", leftWidth, middleWidthNormal).setDuration(ANIM_DURATION) .start(); } public void setMiddleWidth(int value) { middle.getLayoutParams().width=value; requestLayout(); } private void translateWidgets(int deltaX, View... views) { for (final View v : views) { v.setLayerType(View.LAYER_TYPE_HARDWARE, null); v.animate().translationXBy(deltaX).setDuration(ANIM_DURATION) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { v.setLayerType(View.LAYER_TYPE_NONE, null); } }); } } private void resetWidget(View v, int width) { LinearLayout.LayoutParams p= (LinearLayout.LayoutParams)v.getLayoutParams(); p.width=width; p.weight=0; } } 

Cependant, à l’exécution, peu importe la configuration initiale des largeurs, la gestion de la largeur est prise en charge par ThreePaneLayout la première fois que vous utilisez hideLeft() pour afficher les fragments A et B des fragments B et C. Dans la terminologie de ThreePaneLayout – qui n’a pas de lien spécifique avec les fragments – les trois pièces sont à left , au middle et à right . Au moment où vous appelez hideLeft() , nous enregistrons les tailles de left et de middle et hideLeft() tous les poids utilisés sur chacun des trois, afin de pouvoir contrôler complètement les tailles. Au moment de hideLeft() , nous définissons la taille de right comme étant la taille d’origine du middle .

Les animations sont doubles:

  • Utilisez un ViewPropertyAnimator pour effectuer une traduction des trois widgets vers la gauche par la largeur de left , en utilisant un calque matériel
  • Utilisez un ObjectAnimator sur une pseudo-propriété personnalisée de middleWidth pour modifier la largeur middle largeur initiale de left

(il est possible que ce soit une meilleure idée d’utiliser AnimatorSet et ObjectAnimators pour tous ces éléments, même si cela fonctionne pour l’instant)

(il est également possible que l’ middleWidth ObjectAnimator annule la valeur de la couche matérielle, car cela nécessite une invalidation assez continue)

(il est tout à fait possible que j’ai encore des lacunes dans ma compréhension de l’animation, et que j’aime les déclarations entre parenthèses)

L’effet net est que la left glisse hors de l’écran, le middle glisse vers la position d’origine et la taille de left et la right traduit directement derrière le middle .

showLeft() inverse simplement le processus, avec le même mélange d’animateurs, juste avec les directions inversées.

L’activité utilise un ThreePaneLayout pour contenir une paire de widgets ListFragment et un Button . La sélection de quelque chose dans le fragment de gauche ajoute (ou met à jour le contenu de) le fragment du milieu. Sélectionner quelque chose dans le fragment du milieu définit la légende du Button et exécute hideLeft() sur ThreePaneLayout . En appuyant sur BACK, si nous avons caché le côté gauche, vous exécuterez showLeft() ; sinon, BACK quitte l’activité. Comme cela n’utilise pas FragmentTransactions pour affecter les animations, nous gérons nous-mêmes cette “stack arrière”.

Le projet lié ci-dessus utilise des fragments natifs et le framework d’animation natif. J’ai une autre version du même projet qui utilise le backport de fragments Android Support et NineOldAndroids pour l’animation:

https://github.com/commonsguy/cw-omnibus/tree/master/Animation/ThreePaneBC

Le backport fonctionne bien sur un Kindle Fire de 1ère génération, bien que l’animation soit un peu saccadée compte tenu des spécifications matérielles plus faibles et du manque de prise en charge de l’accélération matérielle. Les deux implémentations semblent fluides sur un Nexus 7 et d’autres tablettes de la génération actuelle.

Je suis certainement ouvert aux idées sur la façon d’améliorer cette solution, ou sur d’autres solutions offrant des avantages évidents par rapport à ce que j’ai fait ici (ou ce que @weakwire a utilisé).

Merci encore à tous ceux qui ont consortingbué!

Nous avons construit une bibliothèque appelée PanesLibrary qui résout ce problème. C’est encore plus flexible que ce qui a déjà été proposé parce que:

  • Chaque volet peut être dynamicment dimensionné
  • Il permet un nombre illimité de volets (pas seulement 2 ou 3)
  • Les fragments à l’intérieur des panneaux sont correctement conservés lors des changements d’orientation.

Vous pouvez le consulter ici: https://github.com/Mapsaurus/Android-PanesLibrary

Voici une démonstration: http://www.youtube.com/watch?v=UA-lAGVXoLU&feature=youtu.be

Il vous permet essentiellement d’append facilement un nombre quelconque de volets de taille dynamic et d’attacher des fragments à ces volets. J’espère que vous le trouverez utile! 🙂

En vous basant sur l’un des exemples que vous avez liés ( http://android.amberfog.com/?p=758 ), layout_weight pas animer la propriété layout_weight ? De cette façon, vous pouvez animer le changement de poids des 3 fragments ensemble, et vous obtenez le bonus qu’ils glissent bien ensemble:

Commencez par une mise en page simple. Comme nous allons animer layout_weight , nous avons besoin d’un LinearLayout comme vue racine pour les 3 panneaux:

       

Ensuite, le cours de démonstration:

 public class DemoActivity extends Activity implements View.OnClickListener { public static final int ANIM_DURATION = 500; private static final Interpolator interpolator = new DecelerateInterpolator(); boolean isCollapsed = false; private Fragment frag1, frag2, frag3; private ViewGroup panel1, panel2, panel3; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); panel1 = (ViewGroup) findViewById(R.id.panel1); panel2 = (ViewGroup) findViewById(R.id.panel2); panel3 = (ViewGroup) findViewById(R.id.panel3); frag1 = new ColorFrag(Color.BLUE); frag2 = new InfoFrag(); frag3 = new ColorFrag(Color.RED); final FragmentManager fm = getFragmentManager(); final FragmentTransaction trans = fm.beginTransaction(); trans.replace(R.id.panel1, frag1); trans.replace(R.id.panel2, frag2); trans.replace(R.id.panel3, frag3); trans.commit(); } @Override public void onClick(View view) { toggleCollapseState(); } private void toggleCollapseState() { //Most of the magic here can be atsortingbuted to: http://android.amberfog.com/?p=758 if (isCollapsed) { PropertyValuesHolder[] arrayOfPropertyValuesHolder = new PropertyValuesHolder[3]; arrayOfPropertyValuesHolder[0] = PropertyValuesHolder.ofFloat("Panel1Weight", 0.0f, 1.0f); arrayOfPropertyValuesHolder[1] = PropertyValuesHolder.ofFloat("Panel2Weight", 1.0f, 2.0f); arrayOfPropertyValuesHolder[2] = PropertyValuesHolder.ofFloat("Panel3Weight", 2.0f, 0.0f); ObjectAnimator localObjectAnimator = ObjectAnimator.ofPropertyValuesHolder(this, arrayOfPropertyValuesHolder).setDuration(ANIM_DURATION); localObjectAnimator.setInterpolator(interpolator); localObjectAnimator.start(); } else { PropertyValuesHolder[] arrayOfPropertyValuesHolder = new PropertyValuesHolder[3]; arrayOfPropertyValuesHolder[0] = PropertyValuesHolder.ofFloat("Panel1Weight", 1.0f, 0.0f); arrayOfPropertyValuesHolder[1] = PropertyValuesHolder.ofFloat("Panel2Weight", 2.0f, 1.0f); arrayOfPropertyValuesHolder[2] = PropertyValuesHolder.ofFloat("Panel3Weight", 0.0f, 2.0f); ObjectAnimator localObjectAnimator = ObjectAnimator.ofPropertyValuesHolder(this, arrayOfPropertyValuesHolder).setDuration(ANIM_DURATION); localObjectAnimator.setInterpolator(interpolator); localObjectAnimator.start(); } isCollapsed = !isCollapsed; } @Override public void onBackPressed() { //TODO: Very basic stack handling. Would probably want to do something relating to fragments here.. if(isCollapsed) { toggleCollapseState(); } else { super.onBackPressed(); } } /* * Our magic getters/setters below! */ public float getPanel1Weight() { LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) panel1.getLayoutParams(); return params.weight; } public void setPanel1Weight(float newWeight) { LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) panel1.getLayoutParams(); params.weight = newWeight; panel1.setLayoutParams(params); } public float getPanel2Weight() { LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) panel2.getLayoutParams(); return params.weight; } public void setPanel2Weight(float newWeight) { LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) panel2.getLayoutParams(); params.weight = newWeight; panel2.setLayoutParams(params); } public float getPanel3Weight() { LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) panel3.getLayoutParams(); return params.weight; } public void setPanel3Weight(float newWeight) { LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) panel3.getLayoutParams(); params.weight = newWeight; panel3.setLayoutParams(params); } /** * Crappy fragment which displays a toggle button */ public static class InfoFrag extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { LinearLayout layout = new LinearLayout(getActivity()); layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); layout.setBackgroundColor(Color.DKGRAY); Button b = new Button(getActivity()); b.setOnClickListener((DemoActivity) getActivity()); b.setText("Toggle Me!"); layout.addView(b); return layout; } } /** * Crappy fragment which just fills the screen with a color */ public static class ColorFrag extends Fragment { private int mColor; public ColorFrag() { mColor = Color.BLUE; //Default } public ColorFrag(int color) { mColor = color; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { FrameLayout layout = new FrameLayout(getActivity()); layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); layout.setBackgroundColor(mColor); return layout; } } } 

De plus, cet exemple n’utilise pas FragmentTransactions pour réaliser les animations (il anime plutôt les vues auxquelles les fragments sont attachés), vous devrez donc effectuer vous-même toutes les transactions backstack / fragment, par rapport à la tâche de faire fonctionner les animations. bien, cela ne semble pas être un mauvais compromis 🙂

Horrible vidéo en basse résolution en action: http://youtu.be/Zm517j3bFCo

Ce n’est pas en utilisant des fragments …. C’est une mise en page personnalisée avec 3 enfants. Lorsque vous cliquez sur un message, vous offsetLeftAndRight() les 3 enfants à l’aide de offsetLeftAndRight() et d’un animateur.

Dans JellyBean vous pouvez activer “Afficher les limites de présentation” dans les parameters “Options de développement”. Lorsque l’animation de la diapositive est terminée, vous pouvez toujours voir que le menu de gauche est toujours là, mais sous le panneau du milieu.

Il est similaire au menu de l’application Fly-in de Cyril Mottier , mais avec 3 éléments au lieu de 2.

De plus, le ViewPager du troisième enfant est une autre indication de ce comportement: ViewPager utilise généralement Fragments (je sais qu’il n’est pas obligé de le faire, mais je n’ai jamais vu d’implémentation autre que Fragment ), un autre Fragment les 3 enfants ne sont probablement pas des fragments ….

Je suis en train d’essayer de faire quelque chose comme ça, sauf que le fragment B est mis à l’échelle pour prendre l’espace disponible et que le volet 3 peut être ouvert en même temps s’il y a assez de place. Voici ma solution jusqu’ici, mais je ne sais pas si je vais m’en tenir à cela. J’espère que quelqu’un fournira une réponse montrant The Right Way.

Au lieu d’utiliser un LinearLayout et d’animer le poids, j’utilise un RelativeLayout et anime les marges. Je ne suis pas sûr que ce soit le meilleur moyen car il nécessite un appel à requestLayout () à chaque mise à jour. C’est lisse sur tous mes appareils cependant.

Donc, j’anime la mise en page, je n’utilise pas de transaction de fragments. Je manipule le bouton arrière manuellement pour fermer le fragment C s’il est ouvert.

FragmentB utilise layout_toLeftOf / ToRightOf pour le garder aligné sur les fragments A et C.

Lorsque mon application déclenche un événement pour afficher le fragment C, je glisse le fragment C et glisse le fragment A en même temps. (2 animations séparées). Inversement, lorsque Fragment A est ouvert, je ferme C en même temps.

En mode portrait ou sur un écran plus petit, j’utilise une disposition légèrement différente et faites glisser le fragment C sur l’écran.

Pour utiliser le pourcentage de la largeur des fragments A et C, je pense que vous devrez le calculer au moment de l’exécution … (?)

Voici la mise en page de l’activité:

          

L’animation pour faire glisser FragmentC dans ou hors:

 private ValueAnimator createFragmentCAnimation(final View fragmentCRootView, boolean slideIn) { ValueAnimator anim = null; final RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) fragmentCRootView.getLayoutParams(); if (slideIn) { // set the rightMargin so the view is just outside the right edge of the screen. lp.rightMargin = -(lp.width); // the view's visibility was GONE, make it VISIBLE fragmentCRootView.setVisibility(View.VISIBLE); anim = ValueAnimator.ofInt(lp.rightMargin, 0); } else // slide out: animate rightMargin until the view is outside the screen anim = ValueAnimator.ofInt(0, -(lp.width)); anim.setInterpolator(new DecelerateInterpolator(5)); anim.setDuration(300); anim.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { Integer rightMargin = (Integer) animation.getAnimatedValue(); lp.rightMargin = rightMargin; fragmentCRootView.requestLayout(); } }); if (!slideIn) { // if the view was sliding out, set visibility to GONE once the animation is done anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { fragmentCRootView.setVisibility(View.GONE); } }); } return anim; }