TL; DR: Je recherche un exemple de travail complet de ce que je qualifierai de scénario "Animation Gmail à trois fragments". Spécifiquement, nous voulons commencer avec deux fragments, comme ceci:
Lors de certains événements d'interface utilisateur (par exemple, en tapotant sur quelque chose dans le fragment B), nous voulons:
Et, en appuyant 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 contribué cette réponse populaire à la même question de base, indiquant que vous utiliseriez setCustomAnimations()
avec un FragmentTransaction
. Pour un scénario à deux fragments (par exemple, vous ne voyez que le fragment A au début et souhaitez le remplacer par un nouveau fragment B utilisant des effets animés), je suis tout à fait d'accord. Toutefois:
<objectAnimator>
De son exemple utilise des positions câblées en pixels, et cela semblerait peu pratique compte tenu de la taille de l'écran, mais setCustomAnimations()
nécessite des ressources d'animation, excluant ainsi la possibilité de définir ces éléments. en JavaAndroid:layout_weight
Dans un LinearLayout
pour allouer de l'espace en pourcentageGONE
? Android:layout_weight
Sur 0
? Pré-animé à une échelle de 0? Autre chose?)@ Roman Nurik fait remarquer 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 encore dérouté par le reste de la solution de Reto.
L'auteur de cette entrée Pastebin montre un pseudo-code alléchant, indiquant que les trois fragments résideraient initialement dans le conteneur, avec le fragment C masqué au départ via une opération de transaction hide()
. Nous avons 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. Il repose également sur le fait que vous pouvez apparemment ajouter plusieurs fragments au même conteneur, et je ne suis pas sûr que ce soit un comportement fiable à long terme (pour ne pas le dire, cela devrait casser findFragmentById()
, bien que Je peux vivre avec ça).
L'auteur de this blog post indique que Gmail n'utilise pas du tout setCustomAnimations()
, mais utilise directement des animateurs d'objet ("il suffit de changer la marge de gauche de la vue racine + de modifier la largeur de la vue de droite "). Toutefois, il s'agit toujours d'une solution AFAICT en deux fragments, et la mise en œuvre a montré une nouvelle fois les dimensions des câbles en pixels.
Je vais continuer à m'en tenir à cela, donc je vais peut-être finir par y répondre moi-même un jour, mais j'espère vraiment que quelqu'un aura mis au point la solution en trois fragments pour ce scénario d'animation et pourra poster le code (ou un lien vers celle-ci). Les animations dans Android me donnent envie de me tirer les cheveux, et ceux d’entre vous qui m’ont vu savent qu’il s’agit là d’une entreprise en grande partie stérile.
OK, voici ma propre solution, dérivée de l'application de messagerie AOSP, conformément à la suggestion de @ Christopher dans les commentaires de la question.
https://github.com/commonsguy/cw-omnibus/tree/master/Animation/ThreePane
La solution de @ faiblewire me rappelle la mienne, même s'il utilise des règles classiques Animation
plutôt que des animateurs et utilise des règles RelativeLayout
pour imposer le positionnement. Du point de vue de la prime, il obtiendra probablement la prime, à moins que quelqu'un d'autre avec une solution plus astucieuse publie encore une réponse.
En résumé, le ThreePaneLayout
de ce projet est une sous-classe LinearLayout
, conçue pour fonctionner en mode paysage avec trois enfants. Les largeurs de ces enfants peuvent être définies dans le XML de mise en page, via n'importe quel moyen souhaité - je le 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 - 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.AttributeSet;
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, AttributeSet 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, au moment de l'exécution, quelle que soit 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 ne plus afficher ce que la question appelle des fragments. A et B aux fragments B et C. Dans la terminologie de ThreePaneLayout
- qui n’a aucun lien spécifique avec les fragments - les trois pièces sont left
, middle
et right
. Au moment où vous appelez hideLeft()
, nous enregistrons les tailles de left
et middle
et mettons à zéro tous les poids utilisés sur l'un des trois, afin de pouvoir contrôler complètement les tailles. Au moment de hideLeft()
, nous avons défini la taille de right
sur la taille d'origine de middle
.
Les animations sont doubles:
ViewPropertyAnimator
pour convertir les trois widgets situés à gauche de la largeur de left
, à l'aide d'une couche matérielle.ObjectAnimator
sur une pseudo-propriété personnalisée de middleWidth
pour modifier la largeur middle
de ce qui a commencé par la largeur d'origine de left
(il est possible que ce soit une meilleure idée d'utiliser un AnimatorSet
et un ObjectAnimators
pour tous ces éléments, bien que cela fonctionne pour l'instant)
(il est également possible que middleWidth
ObjectAnimator
annule la valeur de la couche matérielle, car une invalidation relativement continue est nécessaire)
(il est définitivement possible qu'il me reste des lacunes dans la compréhension de l'animation et que j'aime les énoncés entre parenthèses)
L’effet net est que left
disparaît de l’écran, middle
jusqu’à la position et à la taille originales de left
et que right
est traduit juste derrière middle
.
showLeft()
inverse simplement le processus, avec le même mélange d'animateurs, mais avec les directions inversées.
L'activité utilise un ThreePaneLayout
pour contenir une paire de widgets ListFragment
et un Button
. Sélectionner quelque chose dans le fragment de gauche ajoute (ou met à jour le contenu) du fragment du milieu. Sélectionner quelque chose dans le fragment du milieu définit la légende du Button
, plus exécute hideLeft()
sur le ThreePaneLayout
. En appuyant sur BACK, si nous avons caché le côté gauche, exécutera showLeft()
; sinon, BACK quitte l'activité. Puisque ceci n'utilise pas FragmentTransactions
pour affecter les animations, nous sommes bloqués à gérer cette "pile arrière" nous-mêmes.
Le projet lié ci-dessus utilise des fragments natifs et le framework d'animateur natif. J'ai une autre version du même projet qui utilise le backport de fragments de support Android) 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 moins élevées et du manque de prise en charge de l’accélération matérielle. Les deux implémentations semblent lisses 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 à d’autres solutions qui offrent des avantages évidents par rapport à ce que j’ai fait ici (ou à ce que @weakwire a utilisé).
Merci encore à tous ceux qui ont contribué!
Téléchargé ma proposition sur github (Travaille avec toutes les versions Android bien que l'accélération matérielle de visualisation soit fortement recommandée pour ce type d'animations. Pour les périphériques dont l'accélération n'est pas matérielle, une implémentation de la mise en cache bitmap devrait correspondre mieux)
La vidéo de démonstration avec l’animation est Ici (La cadence de prise de vue est un facteur lent de la diffusion de l’écran. Les performances réelles sont très rapides)
Utilisation:
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
Explaination:
Création d'une nouvelle coutume RelativeLayout (ThreeLayout) et de 2 animations personnalisées (MyScalAnimation, MyTranslateAnimation).
ThreeLayout
obtient le poids du volet gauche en tant que paramètre, en supposant que l'autre vue visible a weight=1
.
Ainsi, new ThreeLayout(context,3)
crée une nouvelle vue avec 3 enfants et le volet de gauche avec 1/3 de l’écran total. L'autre vue occupe tout l'espace disponible.
Il calcule la largeur au moment de l'exécution. Une implémentation plus sûre consiste à calculer les dimensions en premier dans draw (). au lieu de post ()
Les animations Scale and Translate redimensionnent et déplacent réellement la vue et non pas pseudo- [scale, move]. 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 s’occupe de tout le reste. Les animations modifient le margins
(en déplacement) et le [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 démontrés les 2 animations
Étape 1
--- IN Screen ----------! ----- OUT ----
[Vue1] [_____ Vue2 _____] [_____ Vue3_____]
Stage2
--OUT -! -------- IN Screen ------
[Vue1] [Vue2] [_____ Vue3_____]
Nous avons construit une bibliothèque appelée PanesLibrary qui résout ce problème. C'est encore plus flexible que ce qui a été offert auparavant parce que:
Vous pouvez le vérifier ici: https://github.com/Mapsaurus/Android-PanesLibrary
Voici une démo: http://www.youtube.com/watch?v=UA-lAGVXoLU&feature=youtu.be
Il vous permet en principe d'ajouter facilement un nombre quelconque de volets de taille dynamique et d'attacher des fragments à ces volets. J'espère que vous le trouverez utile! :)
En vous basant sur l'un des exemples auxquels vous avez lié ( http://Android.amberfog.com/?p=758 ), que diriez-vous d'animer le layout_weight
propriété? De cette façon, vous pouvez animer le changement de poids des 3 fragments ensemble, ET vous obtenez le bonus qu'ils glissent tous ensemble:
Commencez avec une mise en page simple. Puisque nous allons animer layout_weight
, nous avons besoin de LinearLayout
comme vue racine pour les 3 panneaux .:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:Android="http://schemas.Android.com/apk/res/Android"
Android:id="@+id/container"
Android:orientation="horizontal"
Android:layout_width="match_parent"
Android:layout_height="match_parent">
<LinearLayout
Android:id="@+id/panel1"
Android:layout_width="0dip"
Android:layout_weight="1"
Android:layout_height="match_parent"/>
<LinearLayout
Android:id="@+id/panel2"
Android:layout_width="0dip"
Android:layout_weight="2"
Android:layout_height="match_parent"/>
<LinearLayout
Android:id="@+id/panel3"
Android:layout_width="0dip"
Android:layout_weight="0"
Android:layout_height="match_parent"/>
</LinearLayout>
Puis 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 attributed 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); bien, cela ne semble pas être un mauvais compromis :)
Horrible vidéo basse résolution de celle-ci en action: http://youtu.be/Zm517j3bFCo
Cela n'utilise pas de fragments ... C'est une mise en page personnalisée avec 3 enfants. Lorsque vous cliquez sur un message, vous décalez les 3 enfants à l'aide de offsetLeftAndRight()
et d'un animateur.
Dans JellyBean
, vous pouvez activer "Afficher les limites de la présentation" dans les paramètres "Options du développeur". 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.
Cela ressemble à 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 des fragments (je sais qu'ils ne sont pas obligés, mais je n'ai jamais vu d'implémentation autre que Fragment
), et comme vous ne pouvez pas utiliser Fragments
dans un autre Fragment
, les 3 enfants ne sont probablement pas des fragments ....
J'essaie actuellement de faire quelque chose comme ça, sauf que l'échelle Fragment B prend 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'à présent, mais je ne suis pas sûr de pouvoir 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 j'anime les marges. Je ne suis pas sûr que ce soit la meilleure solution car elle 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 de retour manuellement pour fermer le fragment C s'il est ouvert.
FragmentB utilise layout_toLeftOf/ToRightOf pour le maintenir aligné sur les fragments A et C.
Lorsque mon application déclenche un événement pour afficher le fragment C, j'insère le fragment C et i extrait le fragment A en même temps. (2 animations séparées). Inversement, quand le fragment A est ouvert, je ferme le C en même temps.
En mode portrait ou sur un écran plus petit, j’utilise une disposition légèrement différente et je fais glisser le fragment C sur l’écran.
Pour utiliser le pourcentage de largeur des fragments A et C, je pense que vous devriez le calculer au moment de l'exécution ... (?)
Voici la présentation de l'activité:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:Android="http://schemas.Android.com/apk/res/Android"
Android:id="@+id/rootpane"
Android:layout_width="fill_parent"
Android:layout_height="fill_parent">
<!-- FRAGMENT A -->
<fragment
Android:id="@+id/fragment_A"
Android:layout_width="300dp"
Android:layout_height="fill_parent"
class="com.xyz.fragA" />
<!-- FRAGMENT C -->
<fragment
Android:id="@+id/fragment_C"
Android:layout_width="600dp"
Android:layout_height="match_parent"
Android:layout_alignParentRight="true"
class="com.xyz.fragC"/>
<!-- FRAGMENT B -->
<fragment
Android:id="@+id/fragment_B"
Android:layout_width="match_parent"
Android:layout_height="fill_parent"
Android:layout_marginLeft="0dip"
Android:layout_marginRight="0dip"
Android:layout_toLeftOf="@id/fragment_C"
Android:layout_toRightOf="@id/fragment_A"
class="com.xyz.fragB" />
</RelativeLayout>
L'animation pour glisser FragmentC dedans ou dehors:
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;
}