Cómo mostrar/ocultar una vista con animación

A medida que se utilice tu app, será necesario mostrar información nueva en la pantalla y quitar la información anterior. Cambiar inmediatamente lo que se muestra en la pantalla puede resultar molesto o provocar que los usuarios se pierdan el contenido nuevo en la pantalla. Con el uso de animaciones, puedes atenuar los cambios y dirigir los ojos del usuario para que las actualizaciones sean más evidentes.

Existen tres animaciones comunes que puedes usar cuando se oculta o muestra una vista. Puedes usar la animación de revelar circular, una animación de encadenado o una animación de giro de tarjetas.

Cómo crear una animación de encadenado

Las animaciones de encadenado (también conocidas como "disolución") funden gradualmente la salida de una View o un ViewGroup y, al mismo tiempo, funden la entrada de otro elemento. Esta animación resulta útil para situaciones en las que quieres cambiar contenido o vistas en tu app. La animación de encadenado que se muestra aquí usa ViewPropertyAnimator, que está disponible para Android 3.1 (API nivel 12) y versiones posteriores.

El siguiente es un ejemplo de un encadenado entre un indicador de progreso y contenido de texto.

Animación de encadenado
 

Cómo crear las vistas

Primero, debes crear las dos vistas que quieres encadenar. En el siguiente ejemplo, se crea un indicador de progreso y una vista de texto desplazable:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/content"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <TextView style="?android:textAppearanceMedium"
                android:lineSpacingMultiplier="1.2"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@string/lorem_ipsum"
                android:padding="16dp" />

        </ScrollView>

        <ProgressBar android:id="@+id/loading_spinner"
            style="?android:progressBarStyleLarge"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center" />

    </FrameLayout>
    

Cómo configurar la animación de encadenado

Para configurar la animación de encadenado, haz lo siguiente:

  1. Crea variables de miembros para las vistas que quieres encadenar. Necesitarás estas referencias más adelante cuando modifiques las vistas durante la animación.
  2. Define la visibilidad de la vista del fundido de entrada en GONE. De esta manera, se evita que la vista ocupe espacio del diseño y se la omite de los cálculos de diseño, lo que acelera el proceso.
  3. Almacena en caché la propiedad del sistema config_shortAnimTime en una variable de miembros. Esta propiedad define una duración "corta" estándar para la animación. Esta duración es ideal para animaciones sutiles o animaciones que ocurren con mucha frecuencia. Las opciones config_longAnimTime y config_mediumAnimTime también están disponibles si deseas usarlas.

A continuación, podrás ver un ejemplo en el que se usa el diseño del fragmento de código anterior como vista de contenido de la actividad:

Kotlin

    class CrossfadeActivity : Activity() {

        private lateinit var contentView: View
        private lateinit var loadingView: View
        private var shortAnimationDuration: Int = 0

        ...

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_crossfade)

            contentView = findViewById(R.id.content)
            loadingView = findViewById(R.id.loading_spinner)

            // Initially hide the content view.
            contentView.visibility = View.GONE

            // Retrieve and cache the system's default "short" animation time.
            shortAnimationDuration = resources.getInteger(android.R.integer.config_shortAnimTime)
        }
        ...
    }

    

Java

    public class CrossfadeActivity extends Activity {

        private View contentView;
        private View loadingView;
        private int shortAnimationDuration;

        ...

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_crossfade);

            contentView = findViewById(R.id.content);
            loadingView = findViewById(R.id.loading_spinner);

            // Initially hide the content view.
            contentView.setVisibility(View.GONE);

            // Retrieve and cache the system's default "short" animation time.
            shortAnimationDuration = getResources().getInteger(
                    android.R.integer.config_shortAnimTime);
        }
        ...
    }

    

Cómo encadenar las vistas

Ahora que las vistas están configuradas correctamente, haz lo siguiente para encadenarlas:

  1. Para la vista del fundido de entrada, define el valor Alfa en 0 y la visibilidad en VISIBLE. (Recuerda que, en un principio, estaba configurada en GONE.) La vista estará visible, pero 100% transparente.
  2. Para la vista del fundido de salida, anima su valor Alfa de 0 a 1. Para la vista del fundido de salida, anima el valor Alfa de 1 a 0.
  3. Usa onAnimationEnd() en un Animator.AnimatorListener y define la visibilidad de la vista del fundido de salida en GONE. Aunque el valor Alfa es 0, definir la visibilidad de la vista en GONE evita que la vista ocupe espacio del diseño y se la omite de los cálculos de diseño, lo que acelera el procesamiento.

En el siguiente método, se muestra un ejemplo de cómo lograrlo:

Kotlin

    class CrossfadeActivity : Activity() {

        private lateinit var contentView: View
        private lateinit var loadingView: View
        private var shortAnimationDuration: Int = 0

        ...

        private fun crossfade() {
            contentView.apply {
                // Set the content view to 0% opacity but visible, so that it is visible
                // (but fully transparent) during the animation.
                alpha = 0f
                visibility = View.VISIBLE

                // Animate the content view to 100% opacity, and clear any animation
                // listener set on the view.
                animate()
                        .alpha(1f)
                        .setDuration(shortAnimationDuration.toLong())
                        .setListener(null)
            }
            // Animate the loading view to 0% opacity. After the animation ends,
            // set its visibility to GONE as an optimization step (it won't
            // participate in layout passes, etc.)
            loadingView.animate()
                    .alpha(0f)
                    .setDuration(shortAnimationDuration.toLong())
                    .setListener(object : AnimatorListenerAdapter() {
                        override fun onAnimationEnd(animation: Animator) {
                            loadingView.visibility = View.GONE
                        }
                    })
        }
    }

    

Java

    public class CrossfadeActivity extends Activity {

        private View contentView;
        private View loadingView;
        private int shortAnimationDuration;

        ...

        private void crossfade() {

            // Set the content view to 0% opacity but visible, so that it is visible
            // (but fully transparent) during the animation.
            contentView.setAlpha(0f);
            contentView.setVisibility(View.VISIBLE);

            // Animate the content view to 100% opacity, and clear any animation
            // listener set on the view.
            contentView.animate()
                    .alpha(1f)
                    .setDuration(shortAnimationDuration)
                    .setListener(null);

            // Animate the loading view to 0% opacity. After the animation ends,
            // set its visibility to GONE as an optimization step (it won't
            // participate in layout passes, etc.)
            loadingView.animate()
                    .alpha(0f)
                    .setDuration(shortAnimationDuration)
                    .setListener(new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationEnd(Animator animation) {
                            loadingView.setVisibility(View.GONE);
                        }
                    });
        }
    }

    

Cómo crear una animación de giro de tarjetas

Los giros de tarjetas sirven para agregar animaciones entre vistas de contenido. Para ello, se muestra una animación que simula un giro de tarjetas. La animación de giro de tarjetas en este ejemplo usa FragmentTransaction, que está disponible para Android 3.0 (API nivel 11) y versiones posteriores.

Una animación de giro de tarjetas se ve de la siguiente manera:

Animación de giro de tarjetas
 

Cómo crear objetos animadores

Si deseas crear la animación de giro de tarjetas, necesitas un total de cuatro animadores. Dos animadores para la animación de salida y hacia la izquierda del frente de la tarjeta, además de la animación de entrada y desde la izquierda. También necesitas dos animadores para la animación de entrada y desde la derecha, además de la animación de salida y hacia la derecha.

card_flip_left_in.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
        <!-- Before rotating, immediately set the alpha to 0. -->
        <objectAnimator
            android:valueFrom="1.0"
            android:valueTo="0.0"
            android:propertyName="alpha"
            android:duration="0" />

        <!-- Rotate. -->
        <objectAnimator
            android:valueFrom="-180"
            android:valueTo="0"
            android:propertyName="rotationY"
            android:interpolator="@android:interpolator/accelerate_decelerate"
            android:duration="@integer/card_flip_time_full" />

        <!-- Half-way through the rotation (see startOffset), set the alpha to 1. -->
        <objectAnimator
            android:valueFrom="0.0"
            android:valueTo="1.0"
            android:propertyName="alpha"
            android:startOffset="@integer/card_flip_time_half"
            android:duration="1" />
    </set>
    

card_flip_left_out.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
        <!-- Rotate. -->
        <objectAnimator
            android:valueFrom="0"
            android:valueTo="180"
            android:propertyName="rotationY"
            android:interpolator="@android:interpolator/accelerate_decelerate"
            android:duration="@integer/card_flip_time_full" />

        <!-- Half-way through the rotation (see startOffset), set the alpha to 0. -->
        <objectAnimator
            android:valueFrom="1.0"
            android:valueTo="0.0"
            android:propertyName="alpha"
            android:startOffset="@integer/card_flip_time_half"
            android:duration="1" />
    </set>
    

card_flip_right_in.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
        <!-- Before rotating, immediately set the alpha to 0. -->
        <objectAnimator
            android:valueFrom="1.0"
            android:valueTo="0.0"
            android:propertyName="alpha"
            android:duration="0" />

        <!-- Rotate. -->
        <objectAnimator
            android:valueFrom="180"
            android:valueTo="0"
            android:propertyName="rotationY"
            android:interpolator="@android:interpolator/accelerate_decelerate"
            android:duration="@integer/card_flip_time_full" />

        <!-- Half-way through the rotation (see startOffset), set the alpha to 1. -->
        <objectAnimator
            android:valueFrom="0.0"
            android:valueTo="1.0"
            android:propertyName="alpha"
            android:startOffset="@integer/card_flip_time_half"
            android:duration="1" />
    </set>
    

card_flip_right_out.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
        <!-- Rotate. -->
        <objectAnimator
            android:valueFrom="0"
            android:valueTo="-180"
            android:propertyName="rotationY"
            android:interpolator="@android:interpolator/accelerate_decelerate"
            android:duration="@integer/card_flip_time_full" />

        <!-- Half-way through the rotation (see startOffset), set the alpha to 0. -->
        <objectAnimator
            android:valueFrom="1.0"
            android:valueTo="0.0"
            android:propertyName="alpha"
            android:startOffset="@integer/card_flip_time_half"
            android:duration="1" />
    </set>
    

Cómo crear las vistas

Cada lado de la "tarjeta" es un diseño separado con el contenido que desees, como dos vistas de texto, dos imágenes o cualquier combinación de vistas para girar. Luego, usarás los dos diseños en los fragmentos que animarás posteriormente. Los siguientes diseños crean un lado de una tarjeta en el que se muestra texto:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:background="#a6c"
        android:padding="16dp"
        android:gravity="bottom">

        <TextView android:id="@android:id/text1"
            style="?android:textAppearanceLarge"
            android:textStyle="bold"
            android:textColor="#fff"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/card_back_title" />

        <TextView style="?android:textAppearanceSmall"
            android:textAllCaps="true"
            android:textColor="#80ffffff"
            android:textStyle="bold"
            android:lineSpacingMultiplier="1.2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/card_back_description" />

    </LinearLayout>
    

En el otro lado de la tarjeta, se muestra una ImageView:

<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/image1"
        android:scaleType="centerCrop"
        android:contentDescription="@string/description_image_1" />
    

Cómo crear los fragmentos

Crea clases de fragmentos para el anverso y el reverso de la tarjeta. Estas clases muestran los diseños que creaste anteriormente en el método onCreateView() de cada fragmento. Luego, puedes crear instancias de este fragmento en la actividad superior en la que quieres mostrar la tarjeta. En el siguiente ejemplo, se muestran clases de fragmentos anidados de la actividad principal que los usa:

Kotlin

    class CardFlipActivity : FragmentActivity() {
        ...
        /**
         * A fragment representing the front of the card.
         */
        class CardFrontFragment : Fragment() {

            override fun onCreateView(
                    inflater: LayoutInflater,
                    container: ViewGroup?,
                    savedInstanceState: Bundle?
            ): View = inflater.inflate(R.layout.fragment_card_front, container, false)
        }

        /**
         * A fragment representing the back of the card.
         */
        class CardBackFragment : Fragment() {

            override fun onCreateView(
                    inflater: LayoutInflater,
                    container: ViewGroup?,
                    savedInstanceState: Bundle?
            ): View = inflater.inflate(R.layout.fragment_card_back, container, false)
        }
    }

    

Java

    public class CardFlipActivity extends FragmentActivity {
        ...
        /**
         * A fragment representing the front of the card.
         */
        public class CardFrontFragment extends Fragment {
            @Override
            public View onCreateView(LayoutInflater inflater, ViewGroup container,
                    Bundle savedInstanceState) {
                return inflater.inflate(R.layout.fragment_card_front, container, false);
            }
        }

        /**
         * A fragment representing the back of the card.
         */
        public class CardBackFragment extends Fragment {
            @Override
            public View onCreateView(LayoutInflater inflater, ViewGroup container,
                    Bundle savedInstanceState) {
                return inflater.inflate(R.layout.fragment_card_back, container, false);
            }
        }
    }

    

Cómo animar el giro de tarjetas

Ahora, deberás mostrar los fragmentos dentro de una actividad principal. Para ello, primero crea el diseño de tu actividad. En el siguiente ejemplo, se crea un FrameLayout que te permite agregar fragmentos en el tiempo de ejecución:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    

En el código de la actividad, configura el diseño que acabas de crear como la vista de contenido. También recomendamos mostrar un fragmento predeterminado cuando se crea la actividad. Por lo tanto, en el siguiente ejemplo, podrás ver cómo mostrar el frente de la tarjeta de manera predeterminada:

Kotlin

    class CardFlipActivity : FragmentActivity() {

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_activity_card_flip)
            if (savedInstanceState == null) {
                supportFragmentManager.beginTransaction()
                        .add(R.id.container, CardFrontFragment())
                        .commit()
            }
        }
        ...
    }

    

Java

    public class CardFlipActivity extends FragmentActivity {

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_activity_card_flip);

            if (savedInstanceState == null) {
                getSupportFragmentManager()
                        .beginTransaction()
                        .add(R.id.container, new CardFrontFragment())
                        .commit();
            }
        }
        ...
    }

    

Ahora que se muestra el frente de la tarjeta, puedes mostrar la parte posterior con la animación de giro en el momento apropiado. Crea un método para mostrar el otro lado de la tarjeta que realice las siguientes acciones:

  • Establece las animaciones personalizadas que creaste para las transiciones de fragmentos.
  • Reemplaza el fragmento que se muestra actualmente con un fragmento nuevo y anima este evento con las animaciones personalizadas que creaste.
  • Agrega el fragmento que se mostraba a la pila de actividades del fragmento, de manera que cuando el usuario presiona el botón Atrás, las tarjetas giran.

Kotlin

    class CardFlipActivity : FragmentActivity() {

        ...

        private fun flipCard() {
            if (showingBack) {
                supportFragmentManager.popBackStack()
                return
            }

            // Flip to the back.

            showingBack = true

            // Create and commit a new fragment transaction that adds the fragment for
            // the back of the card, uses custom animations, and is part of the fragment
            // manager's back stack.

            supportFragmentManager.beginTransaction()

                    // Replace the default fragment animations with animator resources
                    // representing rotations when switching to the back of the card, as
                    // well as animator resources representing rotations when flipping
                    // back to the front (e.g. when the system Back button is pressed).
                    .setCustomAnimations(
                            R.animator.card_flip_right_in,
                            R.animator.card_flip_right_out,
                            R.animator.card_flip_left_in,
                            R.animator.card_flip_left_out
                    )

                    // Replace any fragments currently in the container view with a
                    // fragment representing the next page (indicated by the
                    // just-incremented currentPage variable).
                    .replace(R.id.container, CardBackFragment())

                    // Add this transaction to the back stack, allowing users to press
                    // Back to get to the front of the card.
                    .addToBackStack(null)

                    // Commit the transaction.
                    .commit()
        }
    }

    

Java

    public class CardFlipActivity extends FragmentActivity {

        ...

        private void flipCard() {
            if (showingBack) {
                getSupportFragmentManager().popBackStack();
                return;
            }

            // Flip to the back.

            showingBack = true;

            // Create and commit a new fragment transaction that adds the fragment for
            // the back of the card, uses custom animations, and is part of the fragment
            // manager's back stack.

            getSupportFragmentManager()
                    .beginTransaction()

                    // Replace the default fragment animations with animator resources
                    // representing rotations when switching to the back of the card, as
                    // well as animator resources representing rotations when flipping
                    // back to the front (e.g. when the system Back button is pressed).
                    .setCustomAnimations(
                            R.animator.card_flip_right_in,
                            R.animator.card_flip_right_out,
                            R.animator.card_flip_left_in,
                            R.animator.card_flip_left_out)

                    // Replace any fragments currently in the container view with a
                    // fragment representing the next page (indicated by the
                    // just-incremented currentPage variable).
                    .replace(R.id.container, new CardBackFragment())

                    // Add this transaction to the back stack, allowing users to press
                    // Back to get to the front of the card.
                    .addToBackStack(null)

                    // Commit the transaction.
                    .commit();
        }
    }

    

Cómo crear una animación de revelar circular

Las animaciones del efecto revelar proporcionan a los usuarios una continuidad visual cuando muestras u ocultas un grupo de elementos de la IU. El método ViewAnimationUtils.createCircularReveal() te permite animar un círculo de recorte para revelar u ocultar una vista. Esta animación se proporciona en la clase ViewAnimationUtils, que está disponible para Android 5.0 (API nivel 21) y versiones posteriores.

En el siguiente ejemplo, podrás ver cómo mostrar una vista que anteriormente era invisible:

Kotlin

    // previously invisible view
    val myView: View = findViewById(R.id.my_view)

    // Check if the runtime version is at least Lollipop
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        // get the center for the clipping circle
        val cx = myView.width / 2
        val cy = myView.height / 2

        // get the final radius for the clipping circle
        val finalRadius = Math.hypot(cx.toDouble(), cy.toDouble()).toFloat()

        // create the animator for this view (the start radius is zero)
        val anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, 0f, finalRadius)
        // make the view visible and start the animation
        myView.visibility = View.VISIBLE
        anim.start()
    } else {
        // set the view to invisible without a circular reveal animation below Lollipop
        myView.visibility = View.INVISIBLE
    }

    

Java

    // previously invisible view
    View myView = findViewById(R.id.my_view);

    // Check if the runtime version is at least Lollipop
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        // get the center for the clipping circle
        int cx = myView.getWidth() / 2;
        int cy = myView.getHeight() / 2;

        // get the final radius for the clipping circle
        float finalRadius = (float) Math.hypot(cx, cy);

        // create the animator for this view (the start radius is zero)
        Animator anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, 0f, finalRadius);

        // make the view visible and start the animation
        myView.setVisibility(View.VISIBLE);
        anim.start();
    } else {
        // set the view to invisible without a circular reveal animation below Lollipop
        myView.setVisibility(View.INVISIBLE);
    }

    

La animación ViewAnimationUtils.createCircularReveal() necesita cinco parámetros. El primer parámetro es la vista que quieres ocultar o mostrar en la pantalla. Los siguientes dos parámetros son las coordenadas x e y para el centro del círculo de recorte. Por lo general, este será el centro de la vista, pero también puedes usar el punto que tocó el usuario para que la animación comience en el lugar que seleccionó. El cuarto parámetro es el radio de inicio del círculo de recorte.

En el ejemplo anterior, el radio inicial se establece en 0 para que el círculo oculte la vista que se mostrará. El último parámetro es el radio final del círculo. Cuando muestres una vista, asegúrate de que el radio final sea mayor que la vista en sí, de modo que la vista se pueda mostrar por completo antes de que finalice la animación.

Para ocultar una vista que anteriormente estaba visible:

Kotlin

    // previously visible view
    val myView: View = findViewById(R.id.my_view)

    // Check if the runtime version is at least Lollipop
    if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) {
        // get the center for the clipping circle
        val cx = myView.width / 2
        val cy = myView.height / 2

        // get the initial radius for the clipping circle
        val initialRadius = Math.hypot(cx.toDouble(), cy.toDouble()).toFloat()

        // create the animation (the final radius is zero)
        val anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, initialRadius, 0f)

        // make the view invisible when the animation is done
        anim.addListener(object : AnimatorListenerAdapter() {

            override fun onAnimationEnd(animation: Animator) {
                super.onAnimationEnd(animation)
                myView.visibility = View.INVISIBLE
            }
        })

        // start the animation
        anim.start()
    } else {
        // set the view to visible without a circular reveal animation below Lollipop
        myView.visibility = View.VISIBLE
    }

    

Java

    // previously visible view
    final View myView = findViewById(R.id.my_view);

    // Check if the runtime version is at least Lollipop
    if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) {
        // get the center for the clipping circle
        int cx = myView.getWidth() / 2;
        int cy = myView.getHeight() / 2;

        // get the initial radius for the clipping circle
        float initialRadius = (float) Math.hypot(cx, cy);

        // create the animation (the final radius is zero)
        Animator anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, initialRadius, 0f);

        // make the view invisible when the animation is done
        anim.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                myView.setVisibility(View.INVISIBLE);
            }
        });

        // start the animation
        anim.start();
    } else {
        // set the view to visible without a circular reveal animation below Lollipop
        myView.setVisibility(View.VISIBLE);
    }

    

En este caso, el radio inicial del círculo de recorte es tan grande como la vista, por lo que la vista estará visible antes de que comience la animación. El radio final se establece en 0, por lo que la vista se ocultará cuando termine la animación. Es importante agregar un objeto de escucha a la animación para que la visibilidad de la vista se pueda configurar como INVISIBLE cuando se complete la animación.