Cómo revelar u ocultar una vista con animación

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

Mientras se usa tu app, aparece información nueva en la pantalla y se quita la información anterior. Cambiar lo que se muestra en la pantalla de inmediato puede resultar molesto, y los usuarios pueden perderse el contenido nuevo que aparece de repente. Las animaciones atenúan los cambios y dirigen la atención del usuario con movimiento para que las actualizaciones sean más evidentes.

Existen tres animaciones comunes que puedes usar para mostrar u ocultar una vista: animaciones de revelar, animaciones de encadenado y animaciones de giro de tarjetas.

Cómo crear una animación de encadenado

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

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

Figura 1. Animación de encadenado.

Cómo crear las vistas

Crea 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. Establece 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 config_shortAnimTime propiedad del sistema 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 frecuencia. config_longAnimTime y config_mediumAnimTime también están disponibles.

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

Cuando las vistas estén configuradas correctamente, haz lo siguiente para encadenarlas:

  1. Para la vista del fundido de entrada, configura el valor Alfa como 0 y la visibilidad como VISIBLE desde su configuración inicial de GONE. La vista estará visible, pero transparente.
  2. Para la vista del fundido de entrada, 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, para establecer la visibilidad de la vista que se desvanecía en GONE. Aunque el valor Alfa es 0, configurar la visibilidad de la vista como 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 so it doesn't
        // participate in layout passes.
        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 so it doesn't
        // participate in layout passes.
        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 que se muestra aquí usa FragmentTransaction.

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

Figura 2. Animación de giro de tarjetas.

Cómo crear objetos animadores

Para crear la animación de giro de tarjetas, necesitas cuatro animadores. Dos animadores son 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. Los otros dos animadores son para cuando la parte trasera de la tarjeta se anima desde la derecha y cuando se anima 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" />

    <!-- Halfway through the rotation, set the alpha to 1. See startOffset. -->
    <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" />

    <!-- Halfway through the rotation, set the alpha to 0. See startOffset. -->
    <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" />

    <!-- Halfway through the rotation, set the alpha to 1. See startOffset. -->
    <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" />

    <!-- Halfway through the rotation, set the alpha to 0. See startOffset. -->
    <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. Usa los dos diseños en los fragmentos que animarás más adelante. El siguiente diseño crea un lado de una tarjeta, que 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>

Y el siguiente diseño crea el otro lado de la tarjeta, que muestra un 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. En tus clases de fragmentos, muestra los diseños que creaste desde el onCreateView() método. 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

Muestra los fragmentos dentro de una actividad superior. Para ello, 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 creas como la vista de contenido. Es una buena práctica mostrar un fragmento predeterminado cuando se crea la actividad. 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();
        }
    }
    ...
}

Con el frente de la tarjeta visible, puedes mostrar el reverso de la tarjeta con la animación de giro en el momento adecuado. Crea un método para mostrar el otro lado de la tarjeta que haga lo siguiente:

  • Establece las animaciones personalizadas que creaste para las transiciones de fragmentos.
  • Reemplaza el fragmento que se muestra 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, such as when the
                // system Back button is tapped.
                .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 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, letting users press
                // the Back button 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, such as 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 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, letting users 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 ocultas o muestras un grupo de elementos de la IU. El ViewAnimationUtils.createCircularReveal() método te permite animar un círculo de recorte para ocultar o mostrar una vista. Esta animación se proporciona en la ViewAnimationUtils clase, que está disponible para Android 5.0 (nivel de API 21) y versiones posteriores.

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

Kotlin

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

// Check whether the runtime version is at least Android 5.0.
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 0.
    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
    // Android 5.0.
    myView.visibility = View.INVISIBLE
}

Java

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

// Check whether the runtime version is at least Android 5.0.
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 0.
    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
    // Android 5.0.
    myView.setVisibility(View.INVISIBLE);
}

La animación ViewAnimationUtils.createCircularReveal() toma 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 cero 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, haz lo siguiente:

Kotlin

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

// Check whether the runtime version is at least Android 5.0.
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 0.
    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
    // Android 5.0.
    myView.visibility = View.VISIBLE
}

Java

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

// Check whether the runtime version is at least Android 5.0.
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 0.
    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 Android
    // 5.0.
    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 cero, por lo que la vista se ocultará cuando termine la animación. Agrega 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.

Recursos adicionales