Esta entrada forma parte de la iniciativa del calendario de Adviento organizado por mi buen amigo Ricardo Pérez. Te animo a echar un vistazo a las otras publicaciones del calendario.
En una publicación del blog de Telerik, se ha explicado cómo utilizar animaciones básicas en .NET MAUI. En este artículo, iremos un paso más allá y veremos cómo implementar animaciones avanzadas en .NET MAUI, lo que te permitirá tener el máximo control para crear experiencias únicas. ¡Comencemos!
Contents
¿Qué es una Easing Function?
Una Easing Function permite especificar la velocidad de cambio de un valor de una propiedad a lo largo del tiempo, o en otras palabras, la suavidad de una animación. Para comprender esto mejor, imagina cuando dejamos caer en el suelo una pelota de basketball. Esta pelota no cae al suelo y deja de botar inmediatamente, sino que realiza una serie de rebotes antes de quedarse quieta. Esta suavidad es la que podemos aplicar a los objetos en las animaciones que creemos. En .NET MAUI, contamos con las siguientes easing functions por default:
- Funciones Bounce: Crean un efecto de rebote, añadiendo un toque dinámico y natural a las animaciones:
- `BounceIn`
- `BounceOut`
- Funciones Cubic: Proporcionan un control sobre la aceleración y desaceleración.
- CubicIn
- CubicOut
- CubicInOut
- Funciones Sin: Basadas en la función matemática Sin, ofrecen transiciones naturales y suaves.
- SinIn
- SinOut
- SinInOut
- Funciones Spring: Proporcionan un efecto de rebote, iniciando con un estiramiento y finalizando con una contracción.
- SpringIn
- SpringOut
- Función Lineal: Mantiene una velocidad constante a lo largo de toda la animación, resultando menos natural que las anteriores.
- Linear
Utilizando Easing Functions en .NET MAUI
En el artículo de animaciones básicas de .NET MAUI, se ha hablado sobre un conjunto de métodos existentes en el framework, que nos permite aplicar animaciones sobre los elementos, por ejemplo:
await image.TranslateToAsync(0, 200, 2000); Los métodos TranslateTo, ScaleTo, RotateTo, ScaleTo y TranslateTo permiten especificar un último parámetro para cambiar el easing function utilizado, que por default es Linear. A continuación, te muestro un ejemplo del uso de las diferentes Easing Functions disponibles en .NET MAUI, empezando por el código en una página XAML de ejemplo:
<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> El código que iniciará las animaciones es el siguiente:
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
private void StartAnimations()
{
uint lenght = 5000;
double maxWidth = 350;
double maxHeight = 100;
Ellipse.TranslateToAsync(maxWidth, maxHeight, lenght, Easing.BounceIn);
Ellipse2.TranslateToAsync(maxWidth, maxHeight, lenght, Easing.BounceOut);
Ellipse3.TranslateToAsync(maxWidth, maxHeight, lenght, Easing.CubicIn);
Ellipse4.TranslateToAsync(maxWidth, maxHeight, lenght, Easing.CubicOut);
Ellipse5.TranslateToAsync(maxWidth, maxHeight, lenght, Easing.CubicInOut);
Ellipse6.TranslateToAsync(maxWidth, maxHeight, lenght, Easing.Linear);
Ellipse7.TranslateToAsync(maxWidth, maxHeight, lenght, Easing.SinIn);
Ellipse8.TranslateToAsync(maxWidth, maxHeight, lenght, Easing.SinOut);
Ellipse9.TranslateToAsync(maxWidth, maxHeight, lenght, Easing.SinInOut);
Ellipse10.TranslateToAsync(maxWidth, maxHeight, lenght, Easing.SpringIn);
Ellipse11.TranslateToAsync(maxWidth, maxHeight, lenght, Easing.SpringOut);
}
private void TapGestureRecognizer_Tapped(object sender, TappedEventArgs e)
{
StartAnimations();
}
} Al ejecutar la aplicación, este es el resultado:
Aunque las Easing Functions por default serán útiles en la mayoría de los casos, también es posible crear tus propias Easing Functions siguiendo los diferentes métodos descritos en la documentación oficial.
Animaciones personalizadas
En caso de querer crear animaciones personalizadas en nuestras aplicaciones, debemos crear una variable del tipo Animation, cuyo primer constructor no recibe parámetros, mientras el segundo sí. La firma del método que sí recibe parámetros es la siguiente:
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) Los parámetros anteriores sirven para lo siguiente:
– callback: Define un `Action` que será ejecutada con los valores sucesivos de la animación
– start: La fracción de la animación actual en la que se iniciará la animación
– end: La fracción de la animación actual en la que se terminará la animación
– easing: Una Easing Function que será utilizada en la animación
– finished: Una acción que se llama cuando la animación ha terminado
Comprender cómo utilizar todos estos parámetros con conjunto puede resultar un poco trivial, por lo que es importante iniciar con lo más básico. Iniciemos con la creación de una animación personalizada básica:
var animation = new Animation(v => Debug.WriteLine(v), 0, 1); La animación anterior define un callback que será llamado el número de veces calculado de acuerdo a la duración de la animación, y el cual va a mostrar en la consola un conjunto de valores entre el 0 y el 1, que son los valores start y end. Seguramente te estés preguntando cuántos valores van a ser mostrados en la consola, eso depende del tiempo total que se tarde la ejecución de la animación, que vas a poder especificar a través del método Commit, el cual tiene la siguiente firma:
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) Los valores del método anterior significan lo siguiente:
– owner: El propietario de la animación
– name: Un identificador que se utilizará para identificar a la animación y realizar operaciones sobre ella
– rate: El tiempo entre fotogramas (en milisegundos)
– length: La duración de la animación (en milisegundos)
– easing: Una Easing Function que será utilizada en la animación
– finished: Una acción que será llamada cuando la animación haya finalizado
– repeat: Una función que devolverá un valor true en caso de querer repetir la animación
En nuestro ejemplo, iniciaremos la animación indicando que el owner es la clase donde está definida la animación, el name es CustomAnimation, un frame rate de 16 y length de 1 segundo:
animation.Commit(this, "CustomAnimation", 16, 1000); Al ejecutar la animación anterior, recibiremos en consola un conjunto de valores similares a los siguientes:
0
0.016
0.032
0.063
0.078
...
0.938
0.953
0.969
0.985
1 En resumen, se han calculado los valores entre el 0 y el 1 en un lapso de 1 segundo, a 16 frames por segundo. Los valores anteriores pueden ser tomados para asignarlos a las propiedades de los elementos visuales. Haciendo algo más práctico, supón que deseamos rotar en el eje Y una imagen 360 grados durante un segundo. Retomando el conocimiento anterior, redefinamos el código de la siguiente forma:
var animation = new Animation(v => Image.RotationY = v, 0, 360);
animation.Commit(this, "CustomAnimation", 16, 5000); En el código anterior, puedes notar que en vez de imprimir los valores a la consola, los aplicamos a la propiedad RotationY de un control Image, con lo que obtenemos el siguiente resultado:
Animaciones hijas
Es bueno saber que también podemos lograr un comportamiento tipo “Storyboard” con animaciones en .NET MAUI. Esto significa, poder sincronizar múltiples animaciones para determinar en qué momento iniciará la ejecución de cada una de ellas. Por ejemplo, supón que queremos que una animación sobre una imagen que dure 6 segundos, durante los cuales:
1. La imagen debe rotar 360 grados en `RotationX`
– Inicio: Al segundo 1
– Fin: Al segundo 6
– Duración: 6 segundos
2. La imagen debe escalar el doble de su tamaño
– Inicio: Al segundo 1
– Fin: Al segundo 3
– Duración: 3 segundos
3. La imagen debe difuminarse al 50%
– Inicio: Al segundo 1
– Fin: Al segundo 3
– Duración: 3 segundos
4. La imagen debe aclararse al 100%
– Inicio: Al segundo 4
– Fin: Al segundo 6
– Duración: 3 segundos
4. La imagen debe volver a su tamaño normal
– Inicio: Al segundo 4
– Fin: Al segundo 6
– Duración: 3 segundos
Lo anterior involucra 5 animaciones que sucederán en diferentes tiempos. Para lograr la sincronización, vamos definir una primer animación que será la animación padre. A continuación, definiremos una serie de animaciones personalizadas modificando los valores de acuerdo a los requerimientos, como en el siguiente ejemplo:
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); A continuación, debemos utilizar el método Add de la animación padre para agregar todas las animaciones hijas. La firma del método Add es la siguiente:
public void Add(double beginAt, double finishAt, Animation animation) Los parámetros anteriores significan lo siguiente:
– beginAt: La fracción dentro de la animación padre en la cual la animación hija empezará a ser reproducida
– finishAt: La fracción dentro de la animación padre en la cual la animación hija terminará de ser reproducida
– animation: La animación a ser agregada
Los valores begintAt y finishAt, van del 0 al 1, por lo que debemos calcular la fracción de tiempo en valor decimal, que podremos obtener con al fórmula:
1 / # of seconds En nuestro caso, como la duración de la animación total es de 6 segundos, el resultado es .1666. Una vez obtenido este valor, podemos agregar las animaciones, determinando el inicio y el fin multiplicando la fracción por el segundo en el cual debe iniciar o finalizar cada animación, además de agregar la animación hija como en el siguiente ejemplo:
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); Finalmente, ejecutemos el método Commit de la animación padre para iniciar la animación final que debe durar 6 segundos:
parentAnimation.Commit(this, "childAnimations", 16, 6000); El resultado de la ejecución anterior, nos brinda una sincronización exacta de cada animación hija:
Conclusión
A lo largo de este artículo, has podido profundizar en conceptos avanzados en lo que respecta a las animaciones en .NET MAUI. Has comprendido qué son y cómo utilizar las Easing Functions, además de cómo crear animaciones personalizadas. Por último, has visto cómo es posible crear una animación con animaciones hijas, logrando un efecto tipo Storyboard para mantener una sincronización perfecta en su ejecución.
