Afficher ou masquer une vue à l'aide d'une animation

Essayer Compose
Jetpack Compose est le kit d'outils d'interface utilisateur recommandé pour Android. Découvrez comment utiliser des animations dans Compose.

Pendant l'utilisation de votre application, de nouvelles informations apparaissent à l'écran et les anciennes sont supprimées. Modifier immédiatement ce qui s'affiche à l'écran peut être troublant, et les utilisateurs peuvent passer à côté de nouveaux contenus qui apparaissent soudainement. Les animations ralentissent les modifications et attirent l'attention de l'utilisateur avec des mouvements afin que les mises à jour soient plus évidentes.

Il existe trois animations courantes que vous pouvez utiliser pour afficher ou masquer une vue: des animations d'affichage, des animations en fondu enchaîné et des animations cardflip.

Créer une animation de fondu enchaîné

Une animation de fondu enchaîné, également appelée dissolve, disparaît progressivement en fondu d'un View ou d'un ViewGroup tout en fondant dans un autre. Cette animation est utile lorsque vous souhaitez changer de contenu ou de vue dans votre application. L'animation de fondu enchaîné présentée ici utilise ViewPropertyAnimator, disponible pour Android 3.1 (niveau d'API 12) ou version ultérieure.

Voici un exemple de fondu enchaîné entre un indicateur de progression et un contenu textuel:

Figure 1 : Animation en fondu enchaîné

Créer les vues

Créez les deux vues pour lesquelles vous souhaitez effectuer un fondu enchaîné. L'exemple suivant crée un indicateur de progression et une vue de texte déroulante:

<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>

Configurer l'animation de fondu enchaîné

Pour configurer l'animation de fondu enchaîné, procédez comme suit:

  1. Créez des variables de membre pour les vues pour lesquelles vous souhaitez effectuer un fondu enchaîné. Vous aurez besoin de ces références ultérieurement pour modifier les vues pendant l'animation.
  2. Définissez la visibilité de la vue en fondu sur GONE. Cela évite que la vue utilise l'espace de mise en page et l'omet des calculs de mise en page, ce qui accélère le traitement.
  3. Mettez en cache la propriété système config_shortAnimTime dans une variable de membre. Cette propriété définit une durée standard "courte" pour l'animation. Cette durée est idéale pour les animations subtiles ou fréquentes. config_longAnimTime et config_mediumAnimTime sont également disponibles.

Voici un exemple utilisant la mise en page de l'extrait de code précédent comme vue du contenu de l'activité:

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);
    }
    ...
}

Fondu enchaîné sur les vues

Lorsque les vues sont correctement configurées, effectuez un fondu enchaîné en procédant comme suit:

  1. Pour la vue qui apparaît en fondu, définissez la valeur alpha sur 0 et la visibilité sur VISIBLE à partir de sa valeur initiale définie sur GONE. La vue est ainsi visible, mais transparente.
  2. Pour la vue qui apparaît en fondu, animez sa valeur alpha de 0 à 1. Pour la vue qui disparaît en fondu, animez la valeur alpha de 1 à 0.
  3. En utilisant onAnimationEnd() dans un Animator.AnimatorListener, définissez la visibilité de la vue qui disparaît en fondu sur GONE. Même si la valeur alpha est de 0, le fait de définir la visibilité de la vue sur GONE l'empêche d'utiliser l'espace de mise en page et l'omet des calculs de mise en page, ce qui accélère le traitement.

La méthode suivante montre comment procéder:

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);
                    }
                });
    }
}

Créer une animation de retournement de cartes

Les cartes permettent de passer d'une vue de contenu à l'autre en affichant une animation qui émule une carte retournée. L'animation de retournement de carte présentée ici utilise FragmentTransaction.

Voici un exemple de retournement de carte:

Figure 2 : Animation lors du retournement des cartes.

Créer les objets animator

Pour créer l'animation de retournement de cartes, vous avez besoin de quatre animateurs. Deux animateurs sont utilisés lorsque le recto de la carte s'anime vers l'extérieur et vers la gauche, et lorsqu'il s'anime vers et depuis la gauche. Les deux autres animateurs sont utilisés lorsque le dos de la carte s'anime à l'intérieur et à la droite, et lorsqu'il s'anime vers l'extérieur et vers la droite.

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>

Créer les vues

Chaque côté de la fiche correspond à une mise en page distincte pouvant inclure le contenu de votre choix, par exemple deux affichages de texte, deux images ou toute combinaison de vues à basculer. Utilisez les deux mises en page dans les fragments que vous animerez par la suite. La mise en page suivante crée un côté d'une carte, qui affiche du texte:

<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>

La mise en page suivante crée l'autre côté de la fiche, qui affiche 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" />

Créer les fragments

Créez des classes de fragment pour le recto et le verso de la carte. Dans vos classes de fragments, renvoyez les mises en page que vous avez créées à l'aide de la méthode onCreateView(). Vous pouvez ensuite créer des instances de ce fragment dans l'activité parent où vous souhaitez afficher la carte.

L'exemple suivant montre des classes de fragment imbriquées dans l'activité parente qui les utilise:

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);
    }
    }
}

Animer le retournement des cartes

Affichez les fragments dans une activité parente. Pour ce faire, créez la mise en page de votre activité. L'exemple suivant crée un objet FrameLayout auquel vous pouvez ajouter des fragments lors de l'exécution:

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

Dans le code de l'activité, définissez la vue de contenu comme la mise en page que vous créez. Il est recommandé d'afficher un fragment par défaut lors de la création de l'activité. L'exemple d'activité suivant montre comment afficher le recto de la carte par défaut:

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();
        }
    }
    ...
}

Une fois le recto de la carte affiché, vous pouvez afficher le verso de la carte avec l'animation de retournement au moment opportun. Créez une méthode permettant d'afficher l'autre côté de la carte qui effectue les opérations suivantes:

  • Définit les animations personnalisées que vous avez créées pour les transitions de fragment.
  • Remplace le fragment affiché par un nouveau fragment et anime cet événement avec les animations personnalisées que vous avez créées.
  • Ajoute le fragment précédemment affiché à la pile "Retour" du fragment. Ainsi, lorsque l'utilisateur appuie sur le bouton "Retour", la carte se retourne.

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();
    }
}

Créer une animation d'affichage circulaire

Les animations de révélation offrent aux utilisateurs une continuité visuelle lorsque vous affichez ou masquez un groupe d'éléments d'interface utilisateur. La méthode ViewAnimationUtils.createCircularReveal() vous permet d'animer un cercle de rognage pour afficher ou masquer une vue. Cette animation est fournie dans la classe ViewAnimationUtils, disponible pour Android 5.0 (niveau d'API 21) ou version ultérieure.

Voici un exemple montrant comment afficher une vue précédemment 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);
}

L'animation ViewAnimationUtils.createCircularReveal() comporte cinq paramètres. Le premier paramètre correspond à la vue que vous souhaitez masquer ou afficher à l'écran. Les deux paramètres suivants sont les coordonnées X et Y du centre du cercle d'écrêtage. En règle générale, il s'agit du centre de la vue, mais vous pouvez également utiliser le point sur lequel l'utilisateur appuie pour que l'animation commence là où il sélectionne l'élément. Le quatrième paramètre correspond au rayon de départ du cercle de rognage.

Dans l'exemple précédent, le rayon initial est défini sur zéro afin que la vue affichée soit masquée par le cercle. Le dernier paramètre est le rayon final du cercle. Lorsque vous affichez une vue, définissez le rayon final plus grand que la vue afin qu'elle puisse être entièrement révélée avant la fin de l'animation.

Pour masquer une vue précédemment visible, procédez comme suit:

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);
}

Dans ce cas, le rayon initial du cercle de rognage est défini pour être aussi grand que la vue afin qu'elle soit visible avant le début de l'animation. Le rayon final est défini sur zéro afin que la vue soit masquée à la fin de l'animation. Ajoutez un écouteur à l'animation afin que la visibilité de la vue puisse être définie sur INVISIBLE une fois l'animation terminée.

Ressources supplémentaires