Cómo navegar entre fragmentos con animaciones

La API de Fragment proporciona dos formas de usar efectos de movimiento y transformaciones para conectar fragmentos visualmente durante la navegación. Una de ellas es el framework de animación, que usa tanto Animation como Animator. La otra es el framework de transición, que incluye transiciones de elementos compartidos.

Puedes especificar efectos personalizados para los fragmentos que entran y los que salen, así como para las transiciones de elementos compartidos entre fragmentos.

  • Un efecto de entrada determina cómo un fragmento ingresa a la pantalla. Por ejemplo, puedes crear un efecto para deslizar el fragmento desde el borde de la pantalla cuando navegas a él.
  • Un efecto de salida determina cómo un fragmento sale de la pantalla. Por ejemplo, puedes crear un efecto para atenuar el fragmento cuando salgas de él.
  • Una transición de elementos compartidos determina cómo una vista que se comparte entre dos fragmentos se mueve entre ellos. Por ejemplo, una imagen que se muestra en una ImageView en el fragmento A transiciona al fragmento B una vez que B se vuelve visible.

Cómo configurar animaciones

Primero, debes crear animaciones para tus efectos de entrada y salida, que se ejecutan cuando navegas a un fragmento nuevo. Puedes definir animaciones como recursos de animación de interpolación. Estos recursos te permiten definir la manera en la que los fragmentos deberán rotar, estirarse, desaparecer en forma gradual y moverse durante la animación. Por ejemplo, tal vez quieras que el fragmento actual desaparezca en forma gradual y el nuevo fragmento se deslice desde el borde derecho de la pantalla, como se muestra en la Figura 1.

Animaciones de entrada y de salida. El fragmento actual desaparece en forma gradual mientras que el fragmento siguiente se desliza desde la derecha.
Figura 1: Animaciones de entrada y de salida. El fragmento actual desaparece en forma gradual mientras que el fragmento siguiente se desliza desde la derecha.

Estas animaciones se pueden definir en el directorio res/anim:

<!-- res/anim/fade_out.xml -->
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromAlpha="1"
    android:toAlpha="0" />
<!-- res/anim/slide_in.xml -->
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromXDelta="100%"
    android:toXDelta="0%" />

También puedes especificar las animaciones de los efectos de entrada y de salida que se ejecutan al mostrar la pila de actividades, lo que puede suceder cuando el usuario presiona los botones Arriba o Atrás. Estas son las animaciones popEnter y popExit. Por ejemplo, cuando un usuario regresa a una pantalla anterior, quizás quieras que el fragmento actual se deslice fuera del borde derecho de la pantalla y que el fragmento anterior aparezca en forma gradual.

Animaciones popEnter y popExit. El fragmento actual se desliza fuera de la pantalla hacia la derecha mientras que el fragmento anterior aparece en forma gradual.
Figura 2: Animaciones popEnter y popExit. El fragmento actual se desliza fuera de la pantalla hacia la derecha mientras que el fragmento anterior aparece en forma gradual

Estas animaciones se pueden definir de la siguiente manera:

<!-- res/anim/slide_out.xml -->
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromXDelta="0%"
    android:toXDelta="100%" />
<!-- res/anim/fade_in.xml -->
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromAlpha="0"
    android:toAlpha="1" />

Una vez que hayas definido tus animaciones, utilízalas llamando a FragmentTransaction.setCustomAnimations() y pasando los recursos de animación por su ID de recurso, como se muestra en el siguiente ejemplo:

Kotlin

val fragment = FragmentB()
supportFragmentManager.commit {
    setCustomAnimations(
        enter = R.anim.slide_in,
        exit = R.anim.fade_out,
        popEnter = R.anim.fade_in,
        popExit = R.anim.slide_out
    )
    replace(R.id.fragment_container, fragment)
    addToBackStack(null)
}

Java

Fragment fragment = new FragmentB();
getSupportFragmentManager().beginTransaction()
    .setCustomAnimations(
        R.anim.slide_in,  // enter
        R.anim.fade_out,  // exit
        R.anim.fade_in,   // popEnter
        R.anim.slide_out  // popExit
    )
    .replace(R.id.fragment_container, fragment)
    .addToBackStack(null)
    .commit();

Cómo establecer transiciones

También puedes usar transiciones para definir los efectos de entrada y de salida. Estas transiciones se pueden definir en archivos de recursos XML. Por ejemplo, quizás desees que el fragmento actual desaparezca en forma gradual y que el nuevo fragmento se deslice desde el borde derecho de la pantalla. Estas transiciones se pueden definir de la siguiente manera:

<!-- res/transition/fade.xml -->
<fade xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"/>
<!-- res/transition/slide_right.xml -->
<slide xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:slideEdge="right" />

Una vez que hayas definido tus transiciones, aplícalas llamando a setEnterTransition() en el fragmento que entra y a setExitTransition() en el que sale, y pasando los recursos de transición aumentados por su ID de recurso, como se muestra en el siguiente ejemplo:

Kotlin

class FragmentA : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val inflater = TransitionInflater.from(requireContext())
        exitTransition = inflater.inflateTransition(R.transition.fade)
    }
}

class FragmentB : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val inflater = TransitionInflater.from(requireContext())
        enterTransition = inflater.inflateTransition(R.transition.slide_right)
    }
}

Java

public class FragmentA extends Fragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TransitionInflater inflater = TransitionInflater.from(requireContext());
        setExitTransition(inflater.inflateTransition(R.transition.fade));
    }
}

public class FragmentB extends Fragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TransitionInflater inflater = TransitionInflater.from(requireContext());
        setEnterTransition(inflater.inflateTransition(R.transition.slide_right));
    }
}

Los fragmentos admiten las transiciones de AndroidX. Si bien los fragmentos también admiten transiciones de framework, te recomendamos que uses las transiciones de AndroidX, ya que son compatibles con el nivel de API 14 y versiones posteriores, y contienen correcciones de errores que no estaban presentes en las versiones anteriores de las transiciones del framework.

Cómo usar transiciones de elementos compartidos

Las transiciones de elementos compartidos forman parte del framework de transición y determinan cómo se mueven las vistas correspondientes entre dos fragmentos durante una transición de fragmentos. Por ejemplo, tal vez desees que una imagen que se muestra en una ImageView del fragmento A transicione al fragmento B una vez que B se vuelva visible, como se muestra en la Figura 3.

Transición de fragmentos con un elemento compartido
Figura 3: Transición de fragmentos con un elemento compartido

A grandes rasgos, aquí te explicamos cómo realizar una transición de fragmentos con elementos compartidos:

  1. Asigna un nombre de transición único para cada vista de elementos compartidos.
  2. Agrega vistas de elementos compartidos y nombres de transición a la FragmentTransaction.
  3. Establece una animación de transición de elementos compartidos.

En primer lugar, debes asignar un nombre de transición único a cada vista de elementos compartidos para permitir que las vistas se mapeen de un fragmento al siguiente. Establece un nombre de transición en los elementos compartidos de cada diseño de fragmento mediante ViewCompat.setTransitionName(), que proporciona compatibilidad con el nivel de API 14 y versiones posteriores. A modo de ejemplo, el nombre de transición de una ImageView en los fragmentos A y B se puede asignar de la siguiente manera:

Kotlin

class FragmentA : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        val itemImageView = view.findViewById<ImageView>(R.id.item_image)
        ViewCompat.setTransitionName(itemImageView, “item_image”)
    }
}

class FragmentB : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        val heroImageView = view.findViewById<ImageView>(R.id.hero_image)
        ViewCompat.setTransitionName(heroImageView, “hero_image”)
    }
}

Java

public class FragmentA extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        ...
        ImageView itemImageView = view.findViewById(R.id.item_image);
        ViewCompat.setTransitionName(itemImageView, “item_image”);
    }
}

public class FragmentB extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        ...
        ImageView heroImageView = view.findViewById(R.id.hero_image);
        ViewCompat.setTransitionName(heroImageView, “hero_image”);
    }
}

Para incluir tus elementos compartidos en la transición de fragmento, FragmentTransaction debe conocer la manera en que las vistas de cada elemento compartido se mapean de un fragmento al siguiente. Agrega todos tus elementos compartidos a la FragmentTransaction llamando a FragmentTransaction.addSharedElement() y pasando la vista y el nombre de transición de la vista correspondiente del fragmento siguiente, como se muestra en el ejemplo a continuación:

Kotlin

val fragment = FragmentB()
supportFragmentManager.commit {
    setCustomAnimations(...)
    addSharedElement(itemImageView, “hero_image”)
    replace(R.id.fragment_container, fragment)
    addToBackStack(null)
}

Java

Fragment fragment = new FragmentB();
getSupportFragmentManager().beginTransaction()
    .setCustomAnimations(...)
    .addSharedElement(itemImageView, “hero_image”)
    .replace(R.id.fragment_container, fragment)
    .addToBackStack(null)
    .commit();

Para especificar cómo será la transición de tus elementos compartidos de un fragmento al siguiente, debes configurar una transición de entrada en el fragmento hasta el que se va a navegar. Llama a Fragment.setSharedElementEnterTransition() en el método onCreate() del fragmento, como se muestra en el siguiente ejemplo:

Kotlin

class FragmentB : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        sharedElementEnterTransition = TransitionInflater.from(requireContext())
             .inflateTransition(R.transition.shared_image)
    }
}

Java

public class FragmentB extends Fragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Transition transition = TransitionInflater.from(requireContext())
            .inflateTransition(R.transition.shared_image);
        setSharedElementEnterTransition(transition);
    }
}

La transición de shared_image se define de la siguiente manera:

<!-- res/transition/shared_image.xml -->
<transitionSet>
    <changeImageTransform />
</transitionSet>

Todas las subclases de Transition se admiten como transiciones de elementos compartidos. Si deseas crear una Transition personalizada, consulta Cómo crear una animación de transición personalizada. changeImageTransform, que se usa en el ejemplo anterior, es una de las traslaciones precompiladas disponibles que puedes usar. Puedes encontrar subclases adicionales de Transition en la referencia de la API de la clase Transition.

De forma predeterminada, la transición de entrada del elemento compartido también se usa como la transición de retorno para los elementos compartidos. La transición de retorno determina cómo los elementos compartidos vuelven al fragmento anterior cuando se quita la transacción de la pila de actividades. Si quieres especificar una transición de retorno diferente, puedes usar Fragment.setSharedElementReturnTransition() en el método onCreate() del fragmento.

Cómo posponer transiciones

En algunos casos, es posible que debas posponer la transición de tu fragmento durante un breve período. Por ejemplo, es posible que debas esperar hasta que se midan y se muestren todas las vistas en el fragmento que entra para que Android pueda capturar con precisión sus estados de inicio y fin para la transición.

Además, tal vez se deba posponer tu transición hasta que se hayan cargado algunos datos necesarios. Por ejemplo, quizás debas esperar hasta que las imágenes se hayan cargado para los elementos compartidos. De lo contrario, la transición puede resultar molesta si una imagen termina de cargarse durante la transición o después de ella.

A fin de posponer una transición, primero debes asegurarte de que la transacción del fragmento permita reordenar los cambios de estado de los fragmentos. Para permitir el reordenamiento de cambios de estado de fragmentos, llama a FragmentTransaction.setReorderingAllowed(), como se muestra en el siguiente ejemplo:

Kotlin

val fragment = FragmentB()
supportFragmentManager.commit {
    setReorderingAllowed(true)
    setCustomAnimation(...)
    addSharedElement(view, view.transitionName)
    replace(R.id.fragment_container, fragment)
    addToBackStack(null)
}

Java

Fragment fragment = new FragmentB();
getSupportFragmentManager().beginTransaction()
    .setReorderingAllowed(true)
    .setCustomAnimations(...)
    .addSharedElement(view, view.getTransitionName())
    .replace(R.id.fragment_container, fragment)
    .addToBackStack(null)
    .commit();

Para posponer la transición de entrada, llama a Fragment.postponeEnterTransition() en el método onViewCreated() del fragmento que ingresa:

Kotlin

class FragmentB : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        postponeEnterTransition()
    }
}

Java

public class FragmentB extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        ...
        postponeEnterTransition();
    }
}

Una vez que hayas cargado los datos y estés listo para comenzar la transición, llama a Fragment.startPostponedEnterTransition(). En el siguiente ejemplo, se usa la biblioteca de Glide a fin de cargar una imagen en una ImageView compartida, lo que pospone la transición correspondiente hasta que se complete la carga de las imágenes.

Kotlin

class FragmentB : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        Glide.with(this)
            .load(url)
            .listener(object : RequestListener<Drawable> {
                override fun onLoadFailed(...): Boolean {
                    startPostponedEnterTransition()
                    return false
                }

                override fun onResourceReady(...): Boolean {
                    startPostponedEnterTransition()
                    return false
                }
            })
            .into(headerImage)
    }
}

Java

public class FragmentB extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        ...
        Glide.with(this)
            .load(url)
            .listener(new RequestListener<Drawable>() {
                @Override
                public boolean onLoadFailed(...) {
                    startPostponedEnterTransition();
                    return false;
                }

                @Override
                public boolean onResourceReady(...) {
                    startPostponedEnterTransition();
                    return false;
                }
            })
            .into(headerImage)
    }
}

Cuando la conexión a Internet de un usuario sea lenta, es posible que necesites que la transición pospuesta comience después de un período determinado en lugar de esperar a que se carguen todos los datos. En estos casos, puedes llamar a Fragment.postponeEnterTransition(long, TimeUnit) en el método onViewCreated() del fragmento que ingresa y pasar la duración y la unidad de tiempo. La transición pospuesta se iniciará automáticamente una vez transcurrido el tiempo especificado.

Cómo usar transiciones de elementos compartidos con una RecyclerView

Las transiciones de entrada pospuestas no deben comenzar hasta que se hayan medido y mostrado todas las vistas del fragmento que ingresa. Cuando usas una RecyclerView, debes esperar a que se carguen los datos y que los elementos de RecyclerView estén listos para dibujar antes de iniciar la transición. Por ejemplo:

Kotlin

class FragmentA : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        postponeEnterTransition()

        // Wait for the data to load
        viewModel.data.observe(viewLifecycleOwner) {
            // Set the data on the RecyclerView adapter
            adapter.setData(it)
            // Start the transition once all views have been
            // measured and laid out
            (view.parent as? ViewGroup)?.doOnPreDraw {
                startPostponedEnterTransition()
            }
        }
    }
}

Java

public class FragmentA extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        postponeEnterTransition();

        final ViewGroup parentView = (ViewGroup) view.getParent();
        // Wait for the data to load
        viewModel.getData()
            .observe(getViewLifecycleOwner(), new Observer<List<String>>() {
                @Override
                public void onChanged(List<String> list) {
                    // Set the data on the RecyclerView adapter
                    adapter.setData(it);
                    // Start the transition once all views have been
                    // measured and laid out
                    parentView.getViewTreeObserver()
                        .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                            @Override
                            public boolean onPreDraw(){
                                parentView.getViewTreeObserver()
                                        .removeOnPreDrawListener(this);
                                startPostponedEnterTransition();
                                return true;
                            }
                    });
                }
        });
    }
}

Observa que se establece un ViewTreeObserver.OnPreDrawListener en el elemento superior de la vista del fragmento. Esto ocurre a los efectos de garantizar que se hayan medido y mostrado todas las vistas del fragmento que, por lo tanto, están listas para dibujarse antes de iniciar la transición de entrada pospuesta.

Otro punto que debes tener en cuenta cuando usas transiciones de elementos compartidos con una RecyclerView es que no puedes establecer el nombre de transición en el diseño XML del elemento de la RecyclerView porque una cantidad arbitraria de elementos comparten ese diseño. Se debe asignar un nombre de transición único de modo que la animación de transición use la vista correcta.

Puedes asignar un nombre de transición único a cada elemento compartido asignándolos cuando se vincule el ViewHolder. Por ejemplo, si los datos de cada elemento incluyen un ID único, se lo podría usar como nombre de transición, como se muestra en el siguiente ejemplo:

Kotlin

class ExampleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val image = itemView.findViewById<ImageView>(R.id.item_image)

    fun bind(id: String) {
        ViewCompat.setTransitionName(image, id)
        ...
    }
}

Java

public class ExampleViewHolder extends RecyclerView.ViewHolder {
    private final ImageView image;

    ExampleViewHolder(View itemView) {
        super(itemView);
        image = itemView.findViewById(R.id.item_image);
    }

    public void bind(String id) {
        ViewCompat.setTransitionName(image, id);
        ...
    }
}

Recursos adicionales

Para obtener más información sobre las transiciones de fragmentos, consulta los siguientes recursos adicionales.

Ejemplos

Entradas de blog