Anima los cambios de diseño con una transición

El marco de trabajo de transición de Android te permite animar todo tipo de movimiento en tu IU con solo proporcionar el diseño inicial y el diseño final. Puedes seleccionar qué tipo de animación deseas (por ejemplo, fundido de entrada/salida o cambio del tamaño de las vistas) y el marco de trabajo de transición determina 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 de nivel de grupo: Permite aplicar uno o más efectos de animación a todas las vistas en una jerarquía de vistas.
  • Animaciones integradas: Permite usar animaciones predefinidas para efectos comunes, como fundido de salida o movimiento.
  • Compatibilidad con archivos de recursos: Permite cargar jerarquías de vistas y animaciones integradas desde archivos de recursos de diseño.
  • Devoluciones de llamada de ciclo de vida: Permite recibir 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 de Scene para el diseño inicial y el diseño final. La escena del diseño inicial suele determinarse 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 intercambiar los diseños.

El diagrama de la figura 1 ilustra la relación entre tus diseños, las escenas, la transición y la animación final.

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

Crea una escena

Las escenas almacenan el estado de una jerarquía de vistas, incluidos los valores de propiedad y todas las vistas. El marco de trabajo de transición puede ejecutar animaciones entre una escena inicial y una escena final.

Puedes crear las escenas desde un archivo de recursos de diseño o un grupo de vistas en tu código. Sin embargo, la escena inicial para la transición a menudo se determina 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. Por ejemplo, esta función es útil para limpiar la configuración de la vista después de que realizas la transición a una escena.

Nota: El marco de trabajo puede animar cambios en una jerarquía de vistas única sin usar escenas, como se describe en Aplica una transición sin escenas. Sin embargo, es esencial comprender las escenas para trabajar con transiciones.

Crea 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 jerarquía de vistas en el archivo sea en su mayoría estática. La escena resultante representa el estado de la jerarquía de vistas en el momento en que creaste la instancia de Scene. Si cambias la jerarquía de vistas, debes volver a crear la escena. El marco de trabajo crea la escena a partir de toda la jerarquía de vistas del archivo; no puedes crear una escena usando una parte de un archivo de diseño.

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

Define diseños para escenas

Los fragmentos de código en el 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:

  • El diseño principal de una actividad con una etiqueta de texto y un diseño secundario.
  • Un diseño relativo para la primera escena con dos campos de texto.
  • Un diseño relativo para la segunda escena con los mismos dos campos de texto en diferente orden.

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 en el diseño principal permanece 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 diseño secundario para el elemento raíz de escena. El diseño para la primera escena se incluye en el archivo de diseño principal. De esa forma, la app puede mostrarlo como parte de la interfaz de usuario inicial y también cargarlo en una escena, ya que el marco de trabajo puede cargar solamente 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

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/scene_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
        <TextView
            android:id="@+id/text_view1
            android:text="Text Line 1" />
        <TextView
            android:id="@+id/text_view2
            android:text="Text Line 2" />
    </RelativeLayout>
    

El diseño de la segunda escena contiene los mismos dos campos de texto (con los mismos ID) colocados en un orden diferente y se define de la siguiente manera:

res/layout/another_scene.xml

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/scene_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
        <TextView
            android:id="@+id/text_view2
            android:text="Text Line 2" />
        <TextView
            android:id="@+id/text_view1
            android:text="Text Line 1" />
    </RelativeLayout>
    

Genera escenas a partir de diseños

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

En el siguiente fragmento de código, se muestra cómo obtener una referencia al elemento raíz de la 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 el elemento raíz de escena definido por el elemento FrameLayout en res/layout/activity_main.xml.

Crea una escena en tu código

También puedes crear una instancia de Scene en tu código a partir de 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 de tu código:

Kotlin

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

    

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 marco de trabajo permite definir acciones de escena personalizadas que el sistema ejecuta cuando entra o sale una escena. En muchos casos, no es necesario definir acciones de escena personalizadas, ya que el marco de trabajo anima automáticamente el cambio entre escenas.

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

  • Animar vistas que no están en la misma jerarquía. Puedes animar vistas de la escena inicial y la final mediante acciones de escena de salida y entrada.
  • Animar vistas que el marco de trabajo de transiciones no pueda animar automáticamente, como los objetos ListView. Para obtener más información, consulta 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 marco de trabajo 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.

Nota: No uses acciones de escena para pasar datos entre vistas en las escenas inicial y final. Para obtener más información, consulta Define devoluciones de llamada del ciclo de vida de transición.

Aplica una transición

El marco de trabajo de transición representa el estilo de animación entre escenas con un objeto Transition. Puedes crear instancias de una Transition usando varias subclases integradas, como AutoTransition y Fade, o definiendo tu propia transición. Luego, para ejecutar la animación entre escenas, puedes pasar tu Scene final y la Transition a TransitionManager.go().

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

Crea una transición

En la sección anterior, aprendiste a crear escenas que representan el estado de diferentes jerarquías de vistas. Una vez que definiste la escena inicial y la escena final entre las que deseas cambiar, debes crear un objeto Transition que defina una animación. El marco de trabajo 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 Atributos Efecto
AutoTransition <autoTransition/> - Transición predeterminada. Aplica fundido de salida, movimiento, cambio de tamaño y fundido de entrada en las vistas, en ese orden.
Fade <fade/> android:fadingMode="[fade_in |
fade_out |
fade_in_out]"
fade_in aplica fundido de entrada en las vistas.
fade_out aplica fundido de salida en las vistas.
fade_in_out (opción predeterminada) hace un fade_out seguido de un fade_in.
ChangeBounds <changeBounds/> - Mueve las vistas y cambia su tamaño.

Crea una instancia de transición desde un archivo de recursos

Esta técnica permite modificar la definición de transición sin tener que cambiar el código de tu actividad. También es útil para separar las definiciones de transición complejas del código de la aplicación, como se muestra en Especifica varias transiciones.

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

  1. Agrega el directorio res/transition/ al proyecto.
  2. Crea un archivo de recursos XML nuevo dentro de este directorio.
  3. 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 dinámicamente si modificas la interfaz de usuario en tu código y para crear instancias de transición integradas simples con pocos parámetros o ninguno.

Para crear una instancia de una transición integrada, invoca a 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();

    

Aplica una transición

Por lo general, se aplica 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 y hace clic en el botón de búsqueda, la app cambia a la escena que representa el diseño de resultados y aplica una transición que funde la salida del botón de búsqueda y la entrada de los resultados.

Si quieres hacer un cambio de escena mientras se aplica una transición en respuesta a algún evento en tu actividad, llama a la función de clase TransitionManager.go() con la escena final y a la instancia de transición para usar en la animación, como se muestra en el siguiente fragmento:

Kotlin

    TransitionManager.go(endingScene, fadeTransition)

    

Java

    TransitionManager.go(endingScene, fadeTransition);

    

El marco de trabajo cambia la jerarquía de vistas dentro del elemento raíz de la escena con la jerarquía de vistas de la escena final mientras se ejecuta la animación especificada por la instancia de transición. La escena inicial es la escena final de la última transición. Si no hubo una transición previa, 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 transición puede aplicar una transición automática que haga algo razonable para la mayoría de las situaciones. Para obtener más información, consulta la referencia de la API para la clase TransitionManager.

Elige vistas de orientación específicas

El marco de trabajo 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. Por ejemplo, el marco de trabajo no admite la animación de cambios en objetos ListView, por lo que no debes intentar animarlos durante una transición. El marco de trabajo te permite seleccionar vistas específicas que quieres animar.

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

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

Especifica varias transiciones

Para obtener el máximo impacto de una animación, debes hacerla 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 entrada/salida proporciona una indicación evidente de que algunas vistas ya no están disponibles. Si mueves vistas a diferentes puntos en la pantalla, una mejor opción sería 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 marco de trabajo de transiciones permite combinar efectos de animación en un conjunto de transición 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.

Aplica una transición sin escenas

Cambiar las jerarquías de vistas no es la única manera de modificar la interfaz de usuario. Para realizar cambios, también puedes agregar, modificar y borrar vistas secundarias dentro de la jerarquía actual. Por ejemplo, puedes implementar una interacción de búsqueda con un solo diseño. Comienza con el diseño que muestre un campo de búsqueda y un ícono de búsqueda. Para cambiar la interfaz de usuario y mostrar los resultados, cuando el usuario haga clic en el botón de búsqueda, quita ese botón llamando a la función ViewGroup.removeView() y agrega los resultados llamando a la función ViewGroup.addView().

Es posible que desees utilizar este enfoque si la alternativa es tener dos jerarquías que sean casi idénticas. En lugar de tener que 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 utilizando una transición demorada. Esta característica del marco de trabajo de transición comienza con el estado de la jerarquía de vistas actual, 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 demorada dentro de una jerarquía de vistas única, sigue estos pasos:

  1. Cuando se produzca el evento que desencadene la transición, llama a la función TransitionManager.beginDelayedTransition(). Para ello, proporciona la vista principal de todas las vistas que deseas cambiar y la transición que quieres usar. El marco de trabajo 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 marco de trabajo 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 los cambios, el marco de trabajo 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 utilizando una transición demorada. En el primer fragmento, se muestra el archivo de definición de diseño:

res/layout/activity_main.xml

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        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" />
        ...
    </RelativeLayout>
    

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 fade: 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 will animate 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 ciclo de vida de la actividad. Representa los estados de la transición que el marco de trabajo supervisa durante el tiempo 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 marco de trabajo invoca las devoluciones de llamada definidas por la interfaz TransitionListener.

Las devoluciones de llamada del ciclo de vida de la 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 la vista inicial a la vista en la jerarquía de vista final porque la jerarquía de vista final no aumenta hasta que se completa 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 marco de trabajo haya finalizado la transición. Para recibir una notificación cuando se complete la transición, puedes implementar la función TransitionListener.onTransitionEnd() en tu actividad.

A fin de 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 marco de trabajo de transiciones:

  • Es posible que las animaciones aplicadas a una 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.
  • Algunos tipos de transición específicos pueden no producir el efecto de animación deseado cuando se aplican a una TextureView.
  • Las clases que amplían AdapterView, como ListView, administran sus vistas secundarias de maneras que son incompatibles con el marco de trabajo de transiciones. Si intentas animar una vista basada en AdapterView, es posible que se cuelgue la pantalla del dispositivo.
  • Si intentas cambiar el tamaño de una TextView con una animación, el texto aparecerá en una nueva ubicación antes de que se haya cambiado el tamaño del objeto por completo. Para evitar este problema, no animes el cambio de tamaño de las vistas que incluyan texto.