¿Muestra de trabajo completa del escenario de animación de tres fragmentos de Gmail?

TL; DR: Estoy buscando una muestra completa de trabajo a lo que me referiré como el escenario de “animación de tres fragmentos de Gmail”. Específicamente, queremos comenzar con dos fragmentos, como este:

dos fragmentos

Sobre algún evento UI (por ejemplo, tocando algo en el Fragmento B), queremos:

  • Fragmento A para deslizar fuera de la pantalla hacia la izquierda
  • El fragmento B se desliza hacia el borde izquierdo de la pantalla y se contrae para ocupar el lugar desocupado por el Fragmento A
  • Fragmente C para deslizarse desde el lado derecho de la pantalla y ocupar el lugar dejado por el Fragmento B

Y, al presionar un botón BACK, queremos que ese conjunto de operaciones se invierta.

Ahora, he visto muchas implementaciones parciales; Voy a revisar cuatro de ellos a continuación. Más allá de ser incompleto, todos tienen sus problemas.


@Reto Meier aportó esta popular respuesta a la misma pregunta básica, indicando que usaría setCustomAnimations() con FragmentTransaction . Para un escenario de dos fragmentos (por ejemplo, solo ves el Fragmento A inicialmente, y deseas reemplazarlo con un nuevo Fragmento B usando efectos animados), estoy completamente de acuerdo. Sin embargo:

  • Como solo puede especificar una animación “en” y una “salida”, no veo cómo manejaría todas las diferentes animaciones requeridas para el escenario de tres fragmentos
  • El en su código de ejemplo utiliza posiciones cableadas en píxeles, y eso parecería poco práctico dado el tamaño de pantalla variable, pero setCustomAnimations() requiere recursos de animación, lo que excluye la posibilidad de definir estas cosas en Java
  • No sé cómo se relacionan los animadores de objetos de escala con cosas como android:layout_weight en LinearLayout para asignar espacio en porcentajes
  • No sé cómo se maneja el Fragmento C desde el principio (¿ GONE ? android:layout_weight de 0 ? Pre-animado a una escala de 0? Algo más?)

@Roman Nurik señala que puedes animar cualquier propiedad , incluidas las que tú mismo defines. Eso puede ayudar a resolver el problema de las posiciones cableadas, a costa de inventar su propia subclase de administrador de diseño personalizado. Eso ayuda a algunos, pero todavía estoy desconcertado por el rest de la solución de Reto.


El autor de esta entrada pastebin muestra algunos pseudocódigos tentadores, básicamente diciendo que los tres fragmentos residirían inicialmente en el contenedor, con el Fragmento C oculto al principio a través de una operación de transacción hide() . Luego show() C y hide() A cuando ocurre el evento UI. Sin embargo, no veo cómo eso maneja el hecho de que B cambia de tamaño. También se basa en el hecho de que aparentemente puedes agregar múltiples fragmentos al mismo contenedor, y no estoy seguro de si ese es un comportamiento confiable a largo plazo (sin mencionar que debería romper findFragmentById() , aunque puedo vivir con ese).


El autor de esta publicación de blog indica que Gmail no está utilizando setCustomAnimations() en absoluto, sino que usa directamente animadores de objetos (“simplemente cambia el margen izquierdo de la vista raíz + cambia el ancho de la vista derecha”). Sin embargo, esta sigue siendo una solución de dos fragmentos AFAICT, y la implementación muestra una vez más las dimensiones de los cables rígidos en píxeles.


Continuaré ocultándome sobre esto, así que tal vez termine respondiéndolo yo mismo algún día, pero realmente espero que alguien haya resuelto la solución de tres fragmentos para este escenario de animación y pueda publicar el código (o un enlace al respecto). Las animaciones en Android me dan ganas de sacarme el pelo, y aquellos de ustedes que me han visto saben que este es un esfuerzo en gran parte infructuoso.

Cargué mi propuesta en github (funciona con todas las versiones de Android, aunque la aceleración de hardware de vista se recomienda encarecidamente para este tipo de animaciones. Para dispositivos no acelerados por hardware, una implementación de caché de bitmap debería ajustarse mejor)

El video de demostración con la animación está aquí (la velocidad de fotogtwigs lenta es la causa del lanzamiento de la pantalla. El rendimiento real es muy rápido)


Uso:

 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 

Explicación

Creado un nuevo RelativeLayout personalizado (ThreeLayout) y 2 animaciones personalizadas (MyScalAnimation, MyTranslateAnimation)

ThreeLayout obtiene el peso del panel izquierdo como parámetro, suponiendo que la otra vista visible tiene un weight=1 .

Entonces, new ThreeLayout(context,3) crea una nueva vista con 3 hijos y el panel izquierdo con 1/3 de la pantalla total. La otra vista ocupa todo el espacio disponible.

Calcula el ancho en tiempo de ejecución, una implementación más segura es que las dimensiones se calculan por primera vez en draw (). en lugar de en la publicación ()

Las animaciones Scale y Translate realmente cambian de tamaño y mueven la vista y no pseudo- [scale, move]. Observe que fillAfter(true) no se usa en ninguna parte.

View2 is right_of View1

y

View3 is right_of View2

Habiendo establecido estas reglas, RelativeLayout se ocupa de todo lo demás. Las animaciones alteran los margins (en movimiento) y [width,height] en la escala

Para acceder a cada niño (para poder inflarlo con su Fragmento, puede llamar

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

A continuación se muestran las 2 animaciones


Nivel 1

--- EN pantalla ----------! ----- OUT ----

[Ver1] [_____ Vista2 _____] [_____ Vista3_____]

Etapa 2

--OUT -! -------- EN Pantalla ------

[Ver1] [Ver2] [_____ Ver3_____]

OK, aquí está mi propia solución, derivada de la aplicación Email AOSP, según la sugerencia de @ Christopher en los comentarios de la pregunta.

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

La solución de @ weakwire me recuerda a la mía, aunque utiliza Animation clásica en lugar de animadores, y usa las reglas RelativeLayout para imponer el posicionamiento. Desde el punto de vista de la recompensa, es probable que obtenga la recompensa, a menos que alguien más con una solución slicker aún publique una respuesta.


En pocas palabras, el ThreePaneLayout en ese proyecto es una subclase LinearLayout , diseñada para trabajar en el paisaje con tres niños. El ancho de esos niños se puede establecer en el formato XML, a través de los medios deseados; lo muestro utilizando pesos, pero podría tener anchos específicos establecidos por recursos de dimensión o lo que sea. El tercer hijo – Fragmento C en la pregunta – debe tener un ancho de cero.

 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; } } 

Sin embargo, en tiempo de ejecución, no importa cómo configuraste los anchos originalmente, ThreePaneLayout la ThreePaneLayout del ThreePaneLayout la primera vez que utilizas hideLeft() para pasar de mostrar lo que la pregunta llama Fragmentos A y B a los Fragmentos B y C. En la terminología de ThreePaneLayout , que no tiene vínculos específicos con los fragmentos, las tres piezas son left , middle y right . En el momento en que llamas a hideLeft() , registramos los tamaños de left y middle y ponemos a cero todos los pesos que se usaron en cualquiera de los tres, para que podamos controlar completamente los tamaños. En el momento de hideLeft() , configuramos el tamaño de la right para que sea el tamaño original de la middle .

Las animaciones son dobles:

  • Use un ViewPropertyAnimator para realizar una traducción de los tres widgets a la izquierda por el ancho de la left , usando una capa de hardware
  • Use un ObjectAnimator en una pseudo-propiedad personalizada de middleWidth para cambiar el ancho del middle de cualquier cosa que comenzó con el ancho original de la left

(es posible que sea una mejor idea usar un AnimatorSet y ObjectAnimators para todos estos, aunque esto funciona por ahora)

(También es posible que el middleWidth ObjectAnimator el valor de la capa de hardware, ya que eso requiere una invalidación bastante continua)

( Definitivamente es posible que todavía tenga lagunas en mi comprensión de la animación, y que me gustan las declaraciones entre paréntesis)

El efecto neto es que la left desliza fuera de la pantalla, middle diapositivas del middle a la posición original y el tamaño de la left y la right traduce justo detrás de la middle .

showLeft() simplemente invierte el proceso, con la misma combinación de animadores, solo con las direcciones invertidas.

La actividad utiliza un ThreePaneLayout para contener un par de widgets ListFragment y un Button . Seleccionar algo en el fragmento de la izquierda agrega (o actualiza el contenido de) el fragmento del medio. Al seleccionar algo en el fragmento del medio se establece el título del Button , y se ejecuta hideLeft() en el ThreePaneLayout . Presionando BACK, si showLeft() el lado izquierdo, se ejecutará showLeft() ; de lo contrario, BACK sale de la actividad. Como esto no usa FragmentTransactions para afectar las animaciones, estamos bloqueados manejando esa “stack de respaldo” nosotros mismos.

El proyecto vinculado anteriormente usa fragmentos nativos y el marco del animador nativo. Tengo otra versión del mismo proyecto que usa los fragmentos de Soporte de Android backport y NineOldAndroids para la animación:

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

El backport funciona bien en un Kindle Fire de 1ra generación, aunque la animación es un poco desigual dado las menores especificaciones de hardware y la falta de soporte de aceleración de hardware. Ambas implementaciones parecen sin problemas en un Nexus 7 y otras tabletas de generación actual.

Ciertamente estoy abierto a ideas sobre cómo mejorar esta solución, u otras soluciones que ofrecen claras ventajas sobre lo que hice aquí (o lo que utilizó @weakwire).

¡Gracias de nuevo a todos los que contribuyeron!

Creamos una biblioteca llamada PanesLibrary que resuelve este problema. Es incluso más flexible de lo que se ofreció anteriormente porque:

  • Cada panel puede ser de tamaño dynamic
  • Permite cualquier cantidad de paneles (no solo 2 o 3)
  • Los fragmentos dentro de los paneles se conservan correctamente en los cambios de orientación.

Puede consultarlo aquí: https://github.com/Mapsaurus/Android-PanesLibrary

Aquí hay una demostración: http://www.youtube.com/watch?v=UA-lAGVXoLU&feature=youtu.be

Básicamente, le permite agregar fácilmente cualquier número de paneles de tamaño dynamic y adjuntar fragmentos a esos paneles. ¡Esperamos que te sea útil! 🙂

Basándose en uno de los ejemplos a los que se ha vinculado ( http://android.amberfog.com/?p=758 ), ¿qué le layout_weight animar la propiedad layout_weight ? De esta forma, puedes animar el cambio de peso de los 3 fragmentos juntos, Y obtienes la ventaja de que todos se deslizan muy bien juntos:

Comience con un diseño simple. Como vamos a animar layout_weight , necesitamos un LinearLayout como vista raíz para los 3 paneles .:

       

Luego la clase de demostración:

 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; } } } 

Además, este ejemplo no usa FragmentTransactions para lograr las animaciones (más bien, anima las vistas a las que están unidos los fragmentos), por lo que necesitaría hacer todas las transacciones de backstack / fragmento usted mismo, pero comparado con el esfuerzo de lograr que las animaciones funcionen muy bien, esto no parece una mala compensación 🙂

Horrible video de baja resolución en acción: http://youtu.be/Zm517j3bFCo

Esto no usa fragmentos … Es un diseño personalizado con 3 hijos. Cuando hace clic en un mensaje, compensa los 3 hijos utilizando offsetLeftAndRight() y un animador.

En JellyBean puede habilitar “Mostrar límites de diseño” en la configuración de “Opciones de desarrollador”. Cuando se completa la animación de diapositivas, aún puede ver que el menú de la izquierda todavía está allí, pero debajo del panel central.

Es similar al menú de la aplicación Fly-in de Cyril Mottier , pero con 3 elementos en lugar de 2.

Además, el ViewPager de los terceros hijos es otra indicación de este comportamiento: ViewPager usualmente usa Fragments (sé que no es necesario, pero nunca he visto una implementación que no sea Fragment ), y como no puedes usar Fragments inside otro Fragment los 3 niños probablemente no sean fragmentos …

Actualmente estoy tratando de hacer algo así, excepto que la escala Fragmento B toma el espacio disponible y que el panel 3 se puede abrir al mismo tiempo si hay espacio suficiente. Aquí está mi solución hasta ahora, pero no estoy seguro de si voy a seguir con eso. Espero que alguien proporcione una respuesta que muestre The Right Way.

En lugar de usar LinearLayout y animar el peso, utilizo RelativeLayout y animo los márgenes. No estoy seguro de que sea la mejor manera porque requiere una llamada a requestLayout () en cada actualización. Sin embargo, es suave en todos mis dispositivos.

Entonces, yo animo el diseño, no estoy usando la transacción de fragmentos. Manejo el botón Atrás manualmente para cerrar el fragmento C si está abierto.

FragmentB usa layout_toLeftOf / ToRightOf para mantenerlo alineado con los fragmentos A y C.

Cuando mi aplicación activa un evento para mostrar el fragmento C, inserto el fragmento C y elimino el fragmento A al mismo tiempo. (2 animaciones separadas). Inversamente, cuando el Fragmento A se abre, cierro C al mismo tiempo.

En modo retrato o en una pantalla más pequeña, utilizo un diseño ligeramente diferente y deslizo el Fragmento C sobre la pantalla.

Para usar el porcentaje del ancho del Fragmento A y C, creo que tendrías que calcularlo en tiempo de ejecución … (?)

Aquí está el diseño de la actividad:

          

La animación para deslizar FragmentC dentro o fuera:

 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; }