Learn how to implement some more advanced animation approaches in your .NET MAUI app, like easing functions to smooth out an animation.
In a previous blog post, Leomaris Reyes explained how to use basic animations in .NET MAUI. In this article, we’ll go a step further and see how to implement advanced animations in .NET MAUI, which will allow you to have maximum control to create unique experiences. Let’s begin!
Let’s start by understanding how we can modify an animation’s behavior through the use of easing functions. An easing function allows you to specify the rate of change of a property value over time—or, in other words, the smoothness of an animation.
To better understand this, imagine when we drop a basketball on the ground. This ball doesn’t fall to the ground and stop bouncing immediately, but rather performs a series of bounces before coming to rest. This smoothness is what we can apply to objects in the animations we create. In .NET MAUI, we have the following easing functions by default:
BounceIn
BounceOut
CubicIn
CubicOut
CubicInOut
SinIn
SinOut
SinInOut
SpringIn
SpringOut
Linear
In the basic animations article for .NET MAUI, Leomaris discussed a set of existing methods in the framework that allow us to apply animations to elements. For example:
await image.TranslateTo(0, 200, 2000);
The methods TranslateTo
, ScaleTo
, RotateTo
, ScaleTo
and TranslateTo
allow you to specify a final parameter to change the easing function used, which by default is Linear
. Below, I’ll show you an example of using the different easing functions available in .NET MAUI, starting with the code in a sample XAML page:
<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>
The code that will start the animations is as follows:
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();
}
}
When running the application, this is the result:
Although the default easing functions will be useful in most cases, it’s also possible to create your own easing functions following the different methods described in the official documentation.
If we want to create custom animations in our applications, we need to create a variable of type Animation
, whose first constructor takes no parameters, while the second does. The signature of the method that takes parameters is as follows:
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)
The above parameters serve the following purposes:
callback
: Defines an Action
that will be executed with successive animation valuesstart
: The fraction of the current animation at which the animation will startend
: The fraction of the current animation at which the animation will endeasing
: An easing function that will be used in the animationfinished
: An action that is called when the animation has finishedUnderstanding how to use all these parameters together can be a bit trivial, so it’s important to start with the basics. Let’s start by creating a basic custom animation:
var animation = new Animation(v => Debug.WriteLine(v), 0, 1);
The above animation defines a callback that will be called the number of times calculated according to the duration of the animation, and which will display in the console a set of values between 0 and 1, which are the start
and end
values.
You’re probably wondering how many values will be shown in the console. That depends on the total time the animation takes to execute, which you can specify through the Commit
method, which has the following signature:
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)
The values of the above method mean the following:
owner
: The owner of the animationname
: An identifier that will be used to identify the animation and perform operations on itrate
: The time between frames (in milliseconds)length
: The duration of the animation (in milliseconds)easing
: An easing function that will be used in the animationfinished
: An action that will be called when the animation has finishedrepeat
: A function that will return true if you want to repeat the animationIn our example, we’ll start the animation indicating that the owner
is the class where the animation is defined, the name
is CustomAnimation
, a frame rate
of 16 and length
of 1 second:
animation.Commit(this, "CustomAnimation", 16, 1000);
When running the above animation, we’ll receive in the console a set of values similar to the following:
0
0.016
0.032
0.063
0.078
...
0.938
0.953
0.969
0.985
1
In summary, values between 0 and 1 have been calculated over a period of 1 second, at 16 frames per second. The above values can be taken to assign them to the properties of visual elements. Doing something more practical, suppose we want to rotate an image 360 degrees on the Y-axis for five seconds. Taking up the previous knowledge, let’s redefine the code as follows:
var animation = new Animation(v => Image.RotationY = v, 0, 360);
animation.Commit(this, "CustomAnimation", 16, 5000);
In the code above, you can notice that instead of printing the values to the console, we apply them to the RotationY
property of an Image control, which gives us the following result:
It’s good to know that we can also achieve “storyboard”-like behavior with animations in .NET MAUI. This means being able to synchronize multiple animations to determine when each one will start executing. For example, suppose we want an animation on an image that lasts 6 seconds, during which:
RotationX
This involves 5 animations that will happen at different times. To achieve synchronization, we will define a first animation that will be the parent animation. Next, we will define a series of custom animations by modifying the values according to the requirements, as in the following example:
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);
Next, we must use the Add
method of the parent animation to add all the child animations. The signature of the Add
method is as follows:
public void Add(double beginAt, double finishAt, Animation animation)
The above parameters mean the following:
beginAt
: The fraction within the parent animation at which the child animation will start playingfinishAt
: The fraction within the parent animation at which the child animation will finish playinganimation
: The animation to be addedThe beginAt
and finishAt
values range from 0 to 1, so we need to calculate the time fraction in decimal value, which we can obtain with the formula:
1 / # of seconds
In our case, since the total animation duration is 6 seconds, the result is .1666
. Once we have this value, we can add the animations, determining the start and end by multiplying the fraction by the second in which each animation should start or end, plus adding the child animation as in the following example:
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);
Finally, let’s execute the Commit
method of the parent animation to start the final animation that should last 6 seconds:
parentAnimation.Commit(this, "childAnimations", 16, 6000);
The result of the above execution gives us exact synchronization of each child animation:
Throughout this article, you have been able to delve into advanced concepts regarding animations in .NET MAUI. You have understood what easing functions are and how to use them, as well as how to create custom animations. Finally, you have seen how it is possible to create an animation with child animations, achieving a storyboard-like effect to maintain perfect synchronization in their execution.
Héctor Pérez is a Microsoft MVP with more than 10 years of experience in software development. He is an independent consultant, working with business and government clients to achieve their goals. Additionally, he is an author of books and an instructor at El Camino Dev and Devs School.