Telerik blogs

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!

What Is an Easing Function?

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:

  • Bounce functions: Create a bouncing effect, adding a dynamic and natural touch to animations:
    • BounceIn
    • BounceOut

Example of bounce animation

  • Cubic functions: Provide control over acceleration and deceleration.
    • CubicIn
    • CubicOut
    • CubicInOut

Example of cubic animation

  • Sin (sine) functions: Based on the sine mathematical function, offer natural and smooth transitions.
    • SinIn
    • SinOut
    • SinInOut

Example of sin animation

  • Spring functions: Provide a bounce effect, starting with a stretch and ending with a contraction.
    • SpringIn
    • SpringOut

Example of spring animation

  • Linear function: Maintains a constant speed throughout the animation, resulting in less natural movement than the previous ones.
    • Linear

Example of linear animation

Using Easing Functions in .NET MAUI

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:

The execution of an application showing all the animations available on .NET MAUI

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.

Custom Animations

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 values
  • start: The fraction of the current animation at which the animation will start
  • end: The fraction of the current animation at which the animation will end
  • easing: An easing function that will be used in the animation
  • finished: An action that is called when the animation has finished

Understanding 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 animation
  • name: An identifier that will be used to identify the animation and perform operations on it
  • rate: The time between frames (in milliseconds)
  • length: The duration of the animation (in milliseconds)
  • easing: An easing function that will be used in the animation
  • finished: An action that will be called when the animation has finished
  • repeat: A function that will return true if you want to repeat the animation

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

A custom animation applied to the RotationY property of an Image control

Child Animations

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:

  1. The image must rotate 360 degrees in RotationX
    • Start: At second 1
    • End: At second 6
    • Duration: 6 seconds
  2. The image must scale to double its size
    • Start: At second 1
    • End: At second 3
    • Duration: 3 seconds
  3. The image must fade to 50%
    • Start: At second 1
    • End: At second 3
    • Duration: 3 seconds
  4. The image must brighten to 100%
    • Start: At second 4
    • End: At second 6
    • Duration: 3 seconds
  5. The image must return to its normal size
    • Start: At second 4
    • End: At second 6
    • Duration: 3 seconds

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 playing
  • finishAt: The fraction within the parent animation at which the child animation will finish playing
  • animation: The animation to be added

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

Child animations being executed and synchronized through the definition of start and end playback times for each one

Conclusion

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.


About the Author

Héctor Pérez

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.

 

Related Posts

Comments

Comments are disabled in preview mode.