Fermer

janvier 14, 2025

Animations avancées avec .NET MAUI

Animations avancées avec .NET MAUI


Découvrez comment implémenter des approches d’animation plus avancées dans votre application .NET MAUI, telles que des fonctions d’assouplissement pour lisser une animation.

Dans un article de blog précédent, Leomaris Reyes a expliqué comment utiliser animations de base dans .NET MAUI. Dans cet article, nous irons plus loin et verrons comment implémenter des animations avancées dans .NET MAUI, qui vous permettront d’avoir un contrôle maximum pour créer des expériences uniques. Commençons !

Qu’est-ce qu’une fonction d’assouplissement ?

Commençons par comprendre comment modifier le comportement d’une animation grâce à l’utilisation de fonctions d’assouplissement. Une fonction d’assouplissement vous permet de spécifier le taux de changement de la valeur d’une propriété au fil du temps ou, en d’autres termes, la fluidité d’une animation.

Pour mieux comprendre cela, imaginez quand on laisse tomber un ballon de basket par terre. Cette balle ne tombe pas au sol et arrête immédiatement de rebondir, mais effectue plutôt une série de rebonds avant de s’immobiliser. Cette douceur est ce que nous pouvons appliquer aux objets dans les animations que nous créons. Dans .NET MAUI, nous avons par défaut les fonctions d’assouplissement suivantes :

  • Fonctions de rebond : Créez un effet de rebond, ajoutant une touche dynamique et naturelle aux animations :

Exemple d'animation de rebond

  • Fonctions cubiques : permettent de contrôler l’accélération et la décélération.
    • CubicIn
    • CubicOut
    • CubicInOut

Exemple d'animation cubique

  • Fonctions sin (sinus) : basées sur la fonction mathématique sinus, offrent des transitions naturelles et fluides.

Exemple d'animation de péché

  • Fonctions du ressort : Fournit un effet de rebond, commençant par un étirement et se terminant par une contraction.

Exemple d'animation printanière

  • Fonction linéaire : Maintient une vitesse constante tout au long de l’animation, ce qui entraîne un mouvement moins naturel que les précédents.

Exemple d'animation linéaire

Utilisation des fonctions d’assouplissement dans .NET MAUI

Dans l’article sur les animations de base pour .NET MAUI, Leomaris a discuté d’un ensemble de méthodes existantes dans le framework qui nous permettent d’appliquer des animations aux éléments. Par exemple:

await image.TranslateTo(0, 200, 2000);

Les méthodes TranslateTo, ScaleTo, RotateTo, ScaleTo et TranslateTo permettent de spécifier un dernier paramètre pour modifier la fonction d’assouplissement utilisée, qui est par défaut Linear. Ci-dessous, je vais vous montrer un exemple d’utilisation des différentes fonctions d’accélération disponibles dans .NET MAUI, en commençant par le code d’un exemple de page XAML :

<Grid ColumnDefinitions="*,*,*" RowDefinitions="*,*,*,*">
    <Grid.GestureRecognizers>
        <TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped" />
    </Grid.GestureRecognizers>
    <Grid
        Background="#F5F5F5"
        HeightRequest="150"
        WidthRequest="400">
        <Label
            FontSize="45"
            HorizontalOptions="Center"
            Text="BounceIn"
            TextColor="LightSeaGreen"
            VerticalOptions="Center" />
        <Ellipse
            x:Name="Ellipse"
            Fill="DarkViolet"
            HeightRequest="50"
            HorizontalOptions="Start"
            VerticalOptions="Start"
            WidthRequest="50" />
    </Grid>
    <Grid
        Grid.Column="1"
        Background="#F0F8FF"
        HeightRequest="150"
        WidthRequest="400">
        <Label
            FontSize="45"
            HorizontalOptions="Center"
            Text="BounceOut"
            TextColor="LightSeaGreen"
            VerticalOptions="Center" />
        <Ellipse
            x:Name="Ellipse2"
            Fill="DarkViolet"
            HeightRequest="50"
            HorizontalOptions="Start"
            VerticalOptions="Start"
            WidthRequest="50" />
    </Grid>
    <Grid
        Grid.Column="2"
        Background="#F5FFFA"
        HeightRequest="150"
        WidthRequest="400">
        <Label
            FontSize="45"
            HorizontalOptions="Center"
            Text="CubicIn"
            TextColor="LightSeaGreen"
            VerticalOptions="Center" />
        <Ellipse
            x:Name="Ellipse3"
            Fill="DarkViolet"
            HeightRequest="50"
            HorizontalOptions="Start"
            VerticalOptions="Start"
            WidthRequest="50" />
    </Grid>
    <Grid
        Grid.Row="1"
        Background="#FFF5EE"
        HeightRequest="150"
        WidthRequest="400">
        <Label
            FontSize="45"
            HorizontalOptions="Center"
            Text="CubicOut"
            TextColor="LightSeaGreen"
            VerticalOptions="Center" />
        <Ellipse
            x:Name="Ellipse4"
            Fill="DarkViolet"
            HeightRequest="50"
            HorizontalOptions="Start"
            VerticalOptions="Start"
            WidthRequest="50" />
    </Grid>
    <Grid
        Grid.Row="1"
        Grid.Column="1"
        Background="#FDF5E6"
        HeightRequest="150"
        WidthRequest="400">
        <Label
            FontSize="45"
            HorizontalOptions="Center"
            Text="CubicInOut"
            TextColor="LightSeaGreen"
            VerticalOptions="Center" />
        <Ellipse
            x:Name="Ellipse5"
            Fill="DarkViolet"
            HeightRequest="50"
            HorizontalOptions="Start"
            VerticalOptions="Start"
            WidthRequest="50" />
    </Grid>
    <Grid
        Grid.Row="1"
        Grid.Column="2"
        Background="#F0FFF0"
        HeightRequest="150"
        WidthRequest="400">
        <Label
            FontSize="45"
            HorizontalOptions="Center"
            Text="Linear"
            TextColor="LightSeaGreen"
            VerticalOptions="Center" />
        <Ellipse
            x:Name="Ellipse6"
            Fill="DarkViolet"
            HeightRequest="50"
            HorizontalOptions="Start"
            VerticalOptions="Start"
            WidthRequest="50" />
    </Grid>
    <Grid
        Grid.Row="2"
        Background="#F8F8FF"
        HeightRequest="150"
        WidthRequest="400">
        <Label
            FontSize="45"
            HorizontalOptions="Center"
            Text="SinIn"
            TextColor="LightSeaGreen"
            VerticalOptions="Center" />
        <Ellipse
            x:Name="Ellipse7"
            Fill="DarkViolet"
            HeightRequest="50"
            HorizontalOptions="Start"
            VerticalOptions="Start"
            WidthRequest="50" />
    </Grid>
    <Grid
        Grid.Row="2"
        Grid.Column="1"
        Background="#FAF0E6"
        HeightRequest="150"
        WidthRequest="400">
        <Label
            FontSize="45"
            HorizontalOptions="Center"
            Text="SinOut"
            TextColor="LightSeaGreen"
            VerticalOptions="Center" />
        <Ellipse
            x:Name="Ellipse8"
            Fill="DarkViolet"
            HeightRequest="50"
            HorizontalOptions="Start"
            VerticalOptions="Start"
            WidthRequest="50" />
    </Grid>
    <Grid
        Grid.Row="2"
        Grid.Column="2"
        Background="#F0FFFF"
        HeightRequest="150"
        WidthRequest="400">
        <Label
            FontSize="45"
            HorizontalOptions="Center"
            Text="SinInOut"
            TextColor="LightSeaGreen"
            VerticalOptions="Center" />
        <Ellipse
            x:Name="Ellipse9"
            Fill="DarkViolet"
            HeightRequest="50"
            HorizontalOptions="Start"
            VerticalOptions="Start"
            WidthRequest="50" />
    </Grid>
    <Grid
        Grid.Row="3"
        Background="#FFF0F5"
        HeightRequest="150"
        WidthRequest="400">
        <Label
            FontSize="45"
            HorizontalOptions="Center"
            Text="SpringIn"
            TextColor="LightSeaGreen"
            VerticalOptions="Center" />
        <Ellipse
            x:Name="Ellipse10"
            Fill="DarkViolet"
            HeightRequest="50"
            HorizontalOptions="Start"
            VerticalOptions="Start"
            WidthRequest="50" />
    </Grid>
    <Grid
        Grid.Row="3"
        Grid.Column="1"
        Background="#FFFAF0"
        HeightRequest="150"
        WidthRequest="400">
        <Label
            FontSize="45"
            HorizontalOptions="Center"
            Text="SpringOut"
            TextColor="LightSeaGreen"
            VerticalOptions="Center" />
        <Ellipse
            x:Name="Ellipse11"
            Fill="DarkViolet"
            HeightRequest="50"
            HorizontalOptions="Start"
            VerticalOptions="Start"
            WidthRequest="50" />
    </Grid>
</Grid>

Le code qui lancera les animations est le suivant :

public partial class MainPage : ContentPage
{        
    public MainPage()
    {
        InitializeComponent();           
    }

    private void StartAnimations()
    {
        uint lenght = 5000;
        double maxWidth = 350;
        double maxHeight = 100;
        Ellipse.TranslateTo(maxWidth, maxHeight, lenght, Easing.BounceIn);
        Ellipse2.TranslateTo(maxWidth, maxHeight, lenght, Easing.BounceOut);
        Ellipse3.TranslateTo(maxWidth, maxHeight, lenght, Easing.CubicIn);
        Ellipse4.TranslateTo(maxWidth, maxHeight, lenght, Easing.CubicOut);
        Ellipse5.TranslateTo(maxWidth, maxHeight, lenght, Easing.CubicInOut);
        Ellipse6.TranslateTo(maxWidth, maxHeight, lenght, Easing.Linear);
        Ellipse7.TranslateTo(maxWidth, maxHeight, lenght, Easing.SinIn);
        Ellipse8.TranslateTo(maxWidth, maxHeight, lenght, Easing.SinOut);
        Ellipse9.TranslateTo(maxWidth, maxHeight, lenght, Easing.SinInOut);
        Ellipse10.TranslateTo(maxWidth, maxHeight, lenght, Easing.SpringIn);
        Ellipse11.TranslateTo(maxWidth, maxHeight, lenght, Easing.SpringOut);
    }

    private void TapGestureRecognizer_Tapped(object sender, TappedEventArgs e)
    {
        StartAnimations();
    }
}

Lors de l’exécution de l’application, voici le résultat :

L'exécution d'une application affichant toutes les animations disponibles sur .NET MAUI

Bien que les fonctions d’assouplissement par défaut soient utiles dans la plupart des cas, il est également possible de créer vos propres fonctions d’assouplissement en suivant les différentes méthodes décrites dans le documentation officielle.

Animations personnalisées

Si nous voulons créer des animations personnalisées dans nos applications, nous devons créer une variable de type Animationdont le premier constructeur ne prend aucun paramètre, alors que le second le fait. La signature de la méthode qui prend des paramètres est la suivante :

public Animation(Action<double> callback, double start = 0.0f, double end = 1.0f, Easing easing = null, Action finished = null) : base(callback, start, end - start, easing, finished)

Les paramètres ci-dessus répondent aux objectifs suivants :

  • callback: Définit un Action qui sera exécuté avec des valeurs d’animation successives
  • start: La fraction de l’animation en cours à laquelle l’animation démarrera
  • end: La fraction de l’animation en cours à laquelle l’animation se terminera
  • easing: Une fonction d’assouplissement qui sera utilisée dans l’animation
  • finished: Une action appelée lorsque l’animation est terminée

Comprendre comment utiliser tous ces paramètres ensemble peut être un peu trivial, il est donc important de commencer par les bases. Commençons par créer une animation personnalisée de base :

var animation = new Animation(v => Debug.WriteLine(v), 0, 1);

L’animation ci-dessus définit un callback qui sera appelé le nombre de fois calculé en fonction de la durée de l’animation, et qui affichera dans la console un ensemble de valeurs comprises entre 0 et 1, qui sont les start et end valeurs.

Vous vous demandez probablement combien de valeurs seront affichées dans la console. Cela dépend du temps total d’exécution de l’animation, que vous pouvez spécifier via le Commit méthode, qui porte la signature suivante :

public void Commit(IAnimatable owner, string name, uint rate = 16, uint length = 250, Easing easing = null, Action<double, bool> finished = null, Func<bool> repeat = null)

Les valeurs de la méthode ci-dessus signifient ce qui suit :

  • owner: Le propriétaire de l’animation
  • name: Un identifiant qui servira à identifier l’animation et à effectuer des opérations sur celle-ci
  • rate: Le temps entre les images (en millisecondes)
  • length: La durée de l’animation (en millisecondes)
  • easing: Une fonction d’assouplissement qui sera utilisée dans l’animation
  • finished: Une action qui sera appelée une fois l’animation terminée
  • repeat: Une fonction qui retournera true si vous souhaitez répéter l’animation

Dans notre exemple, nous démarrerons l’animation en indiquant que le owner est la classe où l’animation est définie, la name est CustomAnimationun cadre rate de 16 ans et length de 1 seconde :

animation.Commit(this, "CustomAnimation", 16, 1000);

Lors de l’exécution de l’animation ci-dessus, nous recevrons dans la console un ensemble de valeurs similaires à celles-ci :

0
0.016
0.032
0.063
0.078
...
0.938
0.953
0.969
0.985
1

En résumé, les valeurs comprises entre 0 et 1 ont été calculées sur une période de 1 seconde, à 16 images par seconde. Les valeurs ci-dessus peuvent être utilisées pour les attribuer aux propriétés des éléments visuels. Pour faire quelque chose de plus pratique, supposons que nous souhaitions faire pivoter une image de 360 ​​​​degrés sur l’axe Y pendant cinq secondes. Reprenant les connaissances précédentes, redéfinissons le code comme suit :

var animation = new Animation(v => Image.RotationY = v, 0, 360);
animation.Commit(this, "CustomAnimation", 16, 5000);

Dans le code ci-dessus, vous pouvez remarquer qu’au lieu d’imprimer les valeurs sur la console, nous les appliquons au RotationY propriété d’un champ Image, ce qui nous donne le résultat suivant :

Une animation personnalisée appliquée à la propriété RotationY d'un champ Image

Animations pour enfants

Il est bon de savoir que nous pouvons également obtenir un comportement de type « storyboard » avec des animations dans .NET MAUI. Cela signifie être capable de synchroniser plusieurs animations pour déterminer quand chacune commencera à s’exécuter. Par exemple, supposons que nous souhaitions une animation sur une image qui dure 6 secondes, pendant laquelle :

  1. L’image doit pivoter de 360 ​​​​degrés RotationX
    • Début : à la seconde 1
    • Fin : à la seconde 6
    • Durée : 6 secondes
  2. L’image doit être mise à l’échelle pour doubler sa taille
    • Début : à la seconde 1
    • Fin : à la seconde 3
    • Durée : 3 secondes
  3. L’image doit s’estomper à 50 %
    • Début : à la seconde 1
    • Fin : à la seconde 3
    • Durée : 3 secondes
  4. L’image doit être éclaircie à 100 %
    • Début : à la seconde 4
    • Fin : à la seconde 6
    • Durée : 3 secondes
  5. L’image doit revenir à sa taille normale
    • Début : à la seconde 4
    • Fin : à la seconde 6
    • Durée : 3 secondes

Cela implique 5 animations qui se dérouleront à des moments différents. Pour réaliser la synchronisation, nous définirons une première animation qui sera l’animation parent. Ensuite, nous définirons une série d’animations personnalisées en modifiant les valeurs selon les besoins, comme dans l’exemple suivant :

var parentAnimation = new Animation();
var rotateXAnimation = new Animation(v => Image.RotationX = v, 0, 360);
var scaleUpAnimation = new Animation(v => Image.Scale = v, 1, 2);
var opacityFadeAnimation = new Animation(v => Image.Opacity = v, 1, 0.5);
var scaleDownAnimation = new Animation(v => Image.Scale = v, 2, 1);
var opacityFadeInAnimation = new Animation(v => Image.Opacity = v, 0.5, 1);

Ensuite, nous devons utiliser le Add méthode de l’animation parent pour ajouter toutes les animations enfants. La signature du Add la méthode est la suivante :

public void Add(double beginAt, double finishAt, Animation animation)

Les paramètres ci-dessus signifient ce qui suit :

  • beginAt: La fraction dans l’animation parent à laquelle l’animation enfant commencera à jouer
  • finishAt: La fraction dans l’animation parent à laquelle l’animation enfant terminera la lecture
  • animation: L’animation à ajouter

Le beginAt et finishAt les valeurs vont de 0 à 1, nous devons donc calculer la fraction de temps en valeur décimale, que nous pouvons obtenir avec la formule :

1 / # of seconds

Dans notre cas, puisque la durée totale de l’animation est de 6 secondes, le résultat est .1666. Une fois que nous avons cette valeur, nous pouvons ajouter les animations, en déterminant le début et la fin en multipliant la fraction par la seconde pendant laquelle chaque animation doit commencer ou se terminer, plus en ajoutant l’animation enfant comme dans l’exemple suivant :

var fraction = .1666;

parentAnimation.Add((fraction * 0), (fraction * 6), rotateXAnimation);
parentAnimation.Add((fraction * 0), (fraction * 3), scaleUpAnimation);
parentAnimation.Add((fraction * 0), (fraction * 3), opacityFadeAnimation);
parentAnimation.Add((fraction * 3), (fraction * 6), scaleDownAnimation);
parentAnimation.Add((fraction * 3), (fraction * 6), opacityFadeInAnimation);

Enfin, exécutons le Commit méthode de l’animation parent pour démarrer l’animation finale qui doit durer 6 secondes :

parentAnimation.Commit(this, "childAnimations", 16, 6000);

Le résultat de l’exécution ci-dessus nous donne une synchronisation exacte de chaque animation enfant :

Animations enfants exécutées et synchronisées grâce à la définition d'heures de début et de fin de lecture pour chacune d'elles

Conclusion

Tout au long de cet article, vous avez pu approfondir les concepts avancés concernant les animations dans .NET MAUI. Vous avez compris ce que sont les fonctions d’easing et comment les utiliser, ainsi que comment créer des animations personnalisées. Enfin, vous avez vu comment il est possible de créer une animation avec des animations enfants, en obtenant un effet de type storyboard pour maintenir une synchronisation parfaite dans leur exécution.




Source link