Cómo animar los cambios de diseño con una transición

Prueba el estilo de Compose
Jetpack Compose es el kit de herramientas de IU recomendado para Android. Aprende a usar animaciones en Compose.

El framework de transición de Android te permite animar todo tipo de movimiento en tu IU proporcionando los diseños inicial y final. Puedes seleccionar el tipo de animación que desees, como atenuar las vistas o cambiar su tamaño, y el framework de transición determinará cómo animar desde el diseño inicial hasta el final.

En el marco de trabajo de transición, se incluyen las siguientes funciones:

  • Animaciones a nivel de grupo: Aplican efectos de animación a todas las vistas en una jerarquía de vistas.
  • Animaciones integradas: Usa animaciones predefinidas para efectos comunes, como fundido de salida o movimiento.
  • Compatibilidad con archivos de recursos: Carga jerarquías de vistas y animaciones integradas desde archivos de recursos de diseño.
  • Devoluciones de llamada de ciclo de vida: Recibe devoluciones de llamada que proporcionan control sobre el proceso de animación y cambio de jerarquía.

Para ver código de muestra con el que se anima entre los cambios de diseño, consulta BasicTransition.

El proceso básico para animar entre dos diseños es el siguiente:

  1. Crea un objeto Scene para los diseños inicial y final. Sin embargo, la escena del diseño inicial a menudo se determina automáticamente a partir del diseño actual.
  2. Crea un objeto Transition para definir el tipo de animación que deseas.
  3. Llama a TransitionManager.go(), y el sistema ejecutará la animación para cambiar los diseños.

En el diagrama de la figura 1, se ilustra la relación entre los diseños, las escenas, la transición y la animación final.

Figura 1: Ilustración básica de cómo el framework de transición crea una animación.

Crea una escena

Las escenas almacenan el estado de una jerarquía de vistas, incluidos todas sus vistas y sus valores de propiedad. El framework de transiciones puede ejecutar animaciones entre una escena inicial y una final.

Puedes crear tus escenas a partir de un archivo de recursos de diseño o de un grupo de vistas de tu código. Sin embargo, la escena inicial de la transición suele determinarse automáticamente a partir de la IU actual.

Una escena también puede definir sus propias acciones que se ejecutan cuando realizas un cambio de escena. Esta función es útil para limpiar la configuración de la vista después de la transición a una escena.

Cómo crear una escena a partir de un recurso de diseño

Puedes crear una instancia de Scene directamente desde un archivo de recursos de diseño. Usa esta técnica cuando la mayoría de la jerarquía de vistas en el archivo sea estática. La escena resultante representa el estado de la jerarquía de vistas en el momento en que creaste la instancia Scene. Si cambias la jerarquía de vistas, vuelve a crear la escena. El framework crea la escena a partir de toda la jerarquía de vistas del archivo. No puedes crear una escena a partir de una parte de un archivo de diseño.

Para crear una instancia de Scene a partir de un archivo de recursos de diseño, recupera la raíz de escena de tu diseño como un ViewGroup. Luego, llama a la función Scene.getSceneForLayout() con la raíz de la escena y el ID de recurso del archivo de diseño que contiene la jerarquía de vistas de la escena.

Cómo definir diseños para escenas

Los fragmentos de código del resto de esta sección muestran cómo crear dos escenas diferentes con el mismo elemento raíz de escena. Los fragmentos también demuestran que puedes cargar varios objetos Scene no relacionados sin implicar que estén relacionados entre sí.

En el ejemplo, se incluyen las siguientes definiciones de diseño:

  • Diseño principal de una actividad con una etiqueta de texto y un FrameLayout secundario.
  • Un objeto ConstraintLayout para la primera escena con dos campos de texto
  • Un ConstraintLayout para la segunda escena con los mismos dos campos de texto en un orden diferente

El ejemplo está diseñado para que toda la animación ocurra dentro del diseño secundario del diseño principal de la actividad. La etiqueta de texto del diseño principal sigue siendo estática.

El diseño principal de la actividad se define de la siguiente manera:

res/layout/activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/master_layout">
    <TextView
        android:id="@+id/title"
        ...
        android:text="Title"/>
    <FrameLayout
        android:id="@+id/scene_root">
        <include layout="@layout/a_scene" />
    </FrameLayout>
</LinearLayout>

Esta definición de diseño contiene un campo de texto y un FrameLayout secundario para la raíz de la escena. El diseño de la primera escena se incluye en el archivo de diseño principal. Esto permite que la app lo muestre como parte de la interfaz de usuario inicial y también lo cargue en una escena, ya que el framework solo puede cargar un archivo de diseño completo en una escena.

El diseño de la primera escena se define de la siguiente manera:

res/layout/a_scene.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/scene_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    
    
</androidx.constraintlayout.widget.ConstraintLayout>

El diseño de la segunda escena contiene los mismos dos campos de texto, con los mismos IDs, en un orden diferente. Se define de la siguiente manera:

res/layout/another_scene.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/scene_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    
    
</androidx.constraintlayout.widget.ConstraintLayout>

Genera escenas a partir de diseños

Después de crear definiciones para los dos diseños de restricciones, puedes obtener una escena para cada uno de ellos. Esto te permite hacer la transición entre las dos configuraciones de la IU. Para obtener una escena, necesitas una referencia a la raíz de la escena y al ID de recurso de diseño.

En el siguiente fragmento de código, se muestra cómo obtener una referencia a la raíz de escena y crear dos objetos Scene a partir de los archivos de diseño:

Kotlin

val sceneRoot: ViewGroup = findViewById(R.id.scene_root)
val aScene: Scene = Scene.getSceneForLayout(sceneRoot, R.layout.a_scene, this)
val anotherScene: Scene = Scene.getSceneForLayout(sceneRoot, R.layout.another_scene, this)

Java

Scene aScene;
Scene anotherScene;

// Create the scene root for the scenes in this app.
sceneRoot = (ViewGroup) findViewById(R.id.scene_root);

// Create the scenes.
aScene = Scene.getSceneForLayout(sceneRoot, R.layout.a_scene, this);
anotherScene =
    Scene.getSceneForLayout(sceneRoot, R.layout.another_scene, this);

En la app, ahora hay dos objetos Scene basados en jerarquías de vistas. Ambas escenas usan la raíz de escena definida por el elemento FrameLayout en res/layout/activity_main.xml.

Cómo crear una escena en tu código

También puedes crear una instancia de Scene en tu código desde un objeto ViewGroup. Usa esta técnica cuando modifiques las jerarquías de vistas directamente en tu código o cuando las generes de forma dinámica.

Para crear una escena a partir de una jerarquía de vistas en tu código, usa el constructor Scene(sceneRoot, viewHierarchy). Llamar a este constructor es equivalente a llamar a la función Scene.getSceneForLayout() cuando ya aumentaste un archivo de diseño.

En el siguiente fragmento de código, se muestra cómo crear una instancia de Scene a partir del elemento raíz de la escena y la jerarquía de vistas de la escena del código:

Kotlin

val sceneRoot = someLayoutElement as ViewGroup
val viewHierarchy = someOtherLayoutElement as ViewGroup
val scene: Scene = Scene(sceneRoot, viewHierarchy)

Java

Scene mScene;

// Obtain the scene root element.
sceneRoot = (ViewGroup) someLayoutElement;

// Obtain the view hierarchy to add as a child of
// the scene root when this scene is entered.
viewHierarchy = (ViewGroup) someOtherLayoutElement;

// Create a scene.
mScene = new Scene(sceneRoot, mViewHierarchy);

Crea acciones de escena

El framework te permite definir acciones de escena personalizadas que el sistema ejecuta cuando entra a una escena o sale de ella. En muchos casos, definir acciones de escena personalizadas no es necesario, ya que el framework anima automáticamente el cambio entre escenas.

Las acciones de escena son útiles para manejar los siguientes casos:

  • Para animar vistas que no estén en la misma jerarquía Puedes animar vistas de las escenas inicial y final mediante acciones de escena de salida y entrada.
  • Para animar vistas que el framework de transiciones no puede animar automáticamente, como los objetos ListView Para obtener más información, consulta la sección sobre limitaciones.

Para proporcionar acciones de escena personalizadas, define tus acciones como objetos Runnable y pásalas a las funciones Scene.setExitAction() o Scene.setEnterAction(). El framework llama a la función setExitAction() en la escena inicial antes de ejecutar la animación de transición y a la función setEnterAction() en la escena final después de ejecutar la animación de transición.

Cómo aplicar una transición

El framework de transición representa el estilo de animación entre escenas con un objeto Transition. Puedes crear una instancia de Transition con subclases integradas, como AutoTransition y Fade, o definir tu propia transición. Luego, puedes ejecutar la animación entre escenas pasando tu Scene final y Transition a TransitionManager.go().

El ciclo de vida de la transición es similar al de la actividad y representa los estados de transición que el framework supervisa entre el inicio y la finalización de una animación. En los estados importantes del ciclo de vida, el framework invoca las funciones de devolución de llamada que puedes implementar para ajustar la interfaz de usuario en diferentes fases de la transición.

Crea una transición

En la sección anterior, se muestra cómo crear escenas que representan el estado de diferentes jerarquías de vistas. Una vez que definas las escenas inicial y final entre las que quieres cambiar, crea un objeto Transition que defina una animación. El framework te permite especificar una transición integrada en un archivo de recursos y aumentarla en el código o crear una instancia de una transición integrada directamente en el código.

Tabla 1: Tipos de transición integrados.

Clase Etiqueta Efecto
AutoTransition <autoTransition/> Transición predeterminada. Aplica atenuación, se mueve, cambia de tamaño y aplica atenuaciones de entrada en las vistas, en ese orden.
ChangeBounds <changeBounds/> Mueve las vistas y cambia su tamaño.
ChangeClipBounds <changeClipBounds/> Captura el View.getClipBounds() antes y después del cambio de escena y anima esos cambios durante la transición.
ChangeImageTransform <changeImageTransform/> Captura la matriz de una ImageView antes y después del cambio de escena, y la anima durante la transición.
ChangeScroll <changeScroll/> Captura las propiedades de desplazamiento de los objetivos antes y después del cambio de escena y anima cualquier cambio.
ChangeTransform <changeTransform/> Captura la escala y la rotación de las vistas antes y después del cambio de escena, y anima esos cambios durante la transición.
Explode <explode/> Realiza un seguimiento de los cambios en la visibilidad de las vistas de destino en las escenas inicial y final, y traslada las vistas hacia adentro o hacia afuera de los bordes de la escena.
Fade <fade/> fade_in atenúa las vistas.
fade_out aplica fundido de salida en las vistas.
fade_in_out (predeterminado) hace un fade_out seguido de un fade_in.
Slide <slide/> Realiza un seguimiento de los cambios en la visibilidad de las vistas de destino en las escenas inicial y final, y mueve las vistas hacia adentro o hacia afuera de uno de los bordes de la escena.

Cómo crear una instancia de transición desde un archivo de recursos

Esta técnica te permite modificar tu definición de transición sin cambiar el código de tu actividad. Esta técnica también es útil para separar las definiciones de transición complejas del código de la aplicación, como se muestra en la sección sobre cómo especificar varias transiciones.

Para especificar una transición integrada en un archivo de recursos, sigue estos pasos:

  • Agrega el directorio res/transition/ al proyecto.
  • Crea un archivo de recursos XML nuevo dentro del directorio.
  • Agrega un nodo XML para una de las transiciones integradas.

Por ejemplo, el siguiente archivo de recursos especifica la transición Fade:

res/transition/fade_transition.xml

<fade xmlns:android="http://schemas.android.com/apk/res/android" />

En el siguiente fragmento de código, se muestra cómo aumentar una instancia de Transition dentro de la actividad desde un archivo de recursos:

Kotlin

var fadeTransition: Transition =
    TransitionInflater.from(this)
                      .inflateTransition(R.transition.fade_transition)

Java

Transition fadeTransition =
        TransitionInflater.from(this).
        inflateTransition(R.transition.fade_transition);

Crea una instancia de transición en tu código

Esta técnica es útil para crear objetos de transición de forma dinámica si modificas la interfaz de usuario en tu código y para crear instancias de transición integradas simples con pocos o ningún parámetro.

Para crear una instancia de una transición integrada, invoca uno de los constructores públicos en las subclases de la clase Transition. Por ejemplo, en el siguiente fragmento de código, se crea una instancia de la transición Fade:

Kotlin

var fadeTransition: Transition = Fade()

Java

Transition fadeTransition = new Fade();

Cómo aplicar una transición

Por lo general, debes aplicar una transición para cambiar entre diferentes jerarquías de vistas en respuesta a un evento, como una acción del usuario. Por ejemplo, considera una app de búsqueda: cuando el usuario ingresa un término de búsqueda y presiona el botón de búsqueda, la app cambia a una escena que representa el diseño de los resultados y aplica una transición que atenúa el botón de búsqueda y la entrada en los resultados de la búsqueda.

Si deseas realizar un cambio de escena mientras aplicas una transición en respuesta a un evento en tu actividad, llama a la función de clase TransitionManager.go() con la escena final y la instancia de transición para usarla en la animación, como se muestra en el siguiente fragmento:

Kotlin

TransitionManager.go(endingScene, fadeTransition)

Java

TransitionManager.go(endingScene, fadeTransition);

El framework cambia la jerarquía de vistas dentro de la raíz de la escena con la jerarquía de vistas de la escena final mientras se ejecuta la animación que especifica la instancia de transición. La escena inicial es la escena final de la última transición. Si no hay una transición anterior, la escena inicial se determina automáticamente a partir del estado actual de la interfaz de usuario.

Si no especificas una instancia de transición, el administrador de transiciones puede aplicar una transición automática que realice una acción razonable para la mayoría de las situaciones. Si deseas obtener más información, consulta la referencia de la API para la clase TransitionManager.

Cómo elegir vistas de objetivo específicas

El framework aplica transiciones a todas las vistas en las escenas inicial y final de forma predeterminada. En algunos casos, es posible que solo quieras aplicar una animación a un subconjunto de vistas en una escena. El framework te permite seleccionar vistas específicas que deseas animar. Por ejemplo, el framework no admite la animación de cambios en objetos ListView, así que no intentes animarlos durante una transición.

Cada vista que anima la transición se llama objetivo. Solo puedes seleccionar objetivos que formen parte de la jerarquía de vistas asociada con una escena.

Para quitar una o más vistas de la lista de destinos, llama al método removeTarget() antes de comenzar la transición. Para agregar solo las vistas que especificas a la lista de objetivos, llama a la función addTarget(). Para obtener más información, consulta la referencia de la API para la clase Transition.

Cómo especificar varias transiciones

Para obtener el mayor impacto de una animación, hazla coincidir con el tipo de cambios que ocurren entre las escenas. Por ejemplo, si quitas algunas vistas y agregas otras entre escenas, una animación de fundido de salida o entrada proporciona una indicación notable de que algunas vistas ya no están disponibles. Si mueves vistas a diferentes puntos de la pantalla, es mejor animar el movimiento para que los usuarios noten la nueva ubicación de las vistas.

No tienes que elegir solo una animación, ya que el framework de transiciones te permite combinar efectos de animación en un conjunto de transiciones que contiene un grupo de transiciones individuales integradas o personalizadas.

Para definir un conjunto de transiciones a partir de una colección de transiciones en XML, crea un archivo de recursos en el directorio res/transitions/ y enumera las transiciones bajo el elemento TransitionSet. Por ejemplo, en el siguiente fragmento, se muestra cómo especificar un conjunto de transiciones que tenga el mismo comportamiento que la clase AutoTransition:

<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
    android:transitionOrdering="sequential">
    <fade android:fadingMode="fade_out" />
    <changeBounds />
    <fade android:fadingMode="fade_in" />
</transitionSet>

Para aumentar la transición establecida en un objeto TransitionSet en tu código, llama a la función TransitionInflater.from() en tu actividad. La clase TransitionSet se extiende desde la clase Transition, por lo que puedes usarla con un administrador de transiciones como cualquier otra instancia de Transition.

Cómo aplicar una transición sin escenas

Cambiar las jerarquías de vistas no es la única forma de modificar la interfaz de usuario. Para realizar cambios, también puedes agregar, modificar y quitar vistas secundarias dentro de la jerarquía actual.

Por ejemplo, puedes implementar una interacción de búsqueda con un diseño único. Comienza con un diseño que muestre un campo de búsqueda y un ícono de búsqueda. Si deseas cambiar la interfaz de usuario para mostrar los resultados, quita el botón de búsqueda cuando el usuario lo presione llamando a la función ViewGroup.removeView() y agrega los resultados de la búsqueda llamando a la función ViewGroup.addView().

Puedes usar este enfoque si la alternativa es tener dos jerarquías que sean casi idénticas. En lugar de crear y mantener dos archivos de diseño separados para una diferencia menor en la interfaz de usuario, puedes tener un archivo de diseño que contenga una jerarquía de vistas que modifiques en el código.

Si realizas cambios en la jerarquía de vistas actual de esta manera, no es necesario que crees una escena. En su lugar, puedes crear y aplicar una transición entre dos estados de una jerarquía de vistas con una transición demorada. Esta función del framework de transiciones comienza con el estado actual de la jerarquía de vistas, registra los cambios que realizas en sus vistas y aplica una transición que anima los cambios cuando el sistema vuelve a dibujar la interfaz de usuario.

Para crear una transición retrasada dentro de una jerarquía de vistas única, sigue estos pasos:

  1. Cuando se produzca el evento que active la transición, llama a la función TransitionManager.beginDelayedTransition() y proporciona la vista superior de todas las vistas que deseas cambiar y la transición que se usará. El framework almacena el estado actual de las vistas secundarias y sus valores de propiedad.
  2. Haz cambios en las vistas secundarias según lo requiera su caso práctico. El framework registra los cambios que realizas en las vistas secundarias y sus propiedades.
  3. Cuando el sistema vuelve a dibujar la interfaz de usuario según tus cambios, el framework anima los cambios entre el estado original y el nuevo.

En el siguiente ejemplo, se muestra cómo animar la adición de una vista de texto a una jerarquía de vistas con una transición retrasada. En el primer fragmento, se muestra el archivo de definición de diseño:

res/layout/activity_main.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/mainLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <EditText
        android:id="@+id/inputText"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
    ...
</androidx.constraintlayout.widget.ConstraintLayout>

En el siguiente fragmento, se muestra el código que anima la adición de la vista de texto:

MainActivity

Kotlin

setContentView(R.layout.activity_main)
val labelText = TextView(this).apply {
    text = "Label"
    id = R.id.text
}
val rootView: ViewGroup = findViewById(R.id.mainLayout)
val mFade: Fade = Fade(Fade.IN)
TransitionManager.beginDelayedTransition(rootView, mFade)
rootView.addView(labelText)

Java

private TextView labelText;
private Fade mFade;
private ViewGroup rootView;
...
// Load the layout.
setContentView(R.layout.activity_main);
...
// Create a new TextView and set some View properties.
labelText = new TextView(this);
labelText.setText("Label");
labelText.setId(R.id.text);

// Get the root view and create a transition.
rootView = (ViewGroup) findViewById(R.id.mainLayout);
mFade = new Fade(Fade.IN);

// Start recording changes to the view hierarchy.
TransitionManager.beginDelayedTransition(rootView, mFade);

// Add the new TextView to the view hierarchy.
rootView.addView(labelText);

// When the system redraws the screen to show this update,
// the framework animates the addition as a fade in.

Define devoluciones de llamada del ciclo de vida de la transición

El ciclo de vida de la transición es similar al de la actividad. Representa los estados de transición que el framework supervisa durante el período entre una llamada a la función TransitionManager.go() y la finalización de la animación. En los estados importantes del ciclo de vida, el framework invoca las devoluciones de llamada definidas por la interfaz TransitionListener.

Las devoluciones de llamada de ciclo de vida de transición son útiles, por ejemplo, para copiar un valor de propiedad de vista desde la jerarquía de vistas inicial a la jerarquía de vistas final durante un cambio de escena. No puedes simplemente copiar el valor de su vista inicial a la vista en la jerarquía de la vista final, ya que la jerarquía de vista final no aumenta hasta que se complete la transición. En su lugar, debes almacenar el valor en una variable y, luego, copiarlo en la jerarquía de la vista final cuando el framework haya finalizado la transición. Para recibir una notificación cuando se complete la transición, implementa la función TransitionListener.onTransitionEnd() en tu actividad.

Para obtener más información, consulta la referencia de la API para la clase TransitionListener.

Limitaciones

En esta sección, se enumeran algunas limitaciones conocidas del framework de transiciones:

  • Es posible que las animaciones aplicadas a un SurfaceView no aparezcan correctamente. Las instancias de SurfaceView se actualizan desde un subproceso que no es de IU, por lo que es posible que las actualizaciones no estén sincronizadas con las animaciones de otras vistas.
  • Es posible que algunos tipos de transición específicos no produzcan el efecto de animación deseado cuando se aplican a una TextureView.
  • Las clases que extienden AdapterView, como ListView, administran sus vistas secundarias de maneras que son incompatibles con el framework de transiciones. Si intentas animar una vista basada en AdapterView, es posible que la pantalla del dispositivo deje de responder.
  • Si intentas cambiar el tamaño de una TextView con una animación, el texto aparecerá en una ubicación nueva antes de que se cambie el tamaño del objeto por completo. Para evitar este problema, no animes el cambio de tamaño de las vistas que contengan texto.