Skip to content
Home » Basics of .NET MAUI – Part 20 – Animation

Basics of .NET MAUI – Part 20 – Animation

Spread the love

In the previous part of the series we created a simple game simulation where the slugs win randomly without moving at all. But in real races, slugs (or, more commonly, horses or greyhounds) do have to move to win, or even to finish the race. Our slugs are no exception to this rule. In this part of the series we’ll implement animations and make the slugs run.

We’re going to create a couple animations. Not only are the slugs going to move from one end of the racetrack to the other, but they’re also going to have some moving parts, like the tentacles with the eyes.

But before we implement any of that, let’s have a look at some theory. Let’s see how animation works in .NET MAUI and what we can do with it. We’ll make use of the TestPage at first, so that we can practice the new stuff. Then we’ll implement  the animations in our app.

Basic Animations

Animations consist in a gradual change of a property between two values over a period of time. The four basic animations supported by .NET MAUI are:

FadeTo

TranslateTo

RotateTo

ScaleTo

But there are more, like RelScaleTo, RotateXTo or ScaleYTo, to mention just a few. They can be used with VisualElement objects. The animations listed above are extension methods of the ViewExtensions class. The methods are asynchronous and return a Task<bool> object. If an animation completes, the return value is false. If it’s canceled, the return value is true.

We can use the await keyword to combine animations sequentially. These are so-called compound animations. If we don’t use the await keyword, we can combine the animations to run simultaneously. These are so-called composite animations.

We have to keep in mind, too, that on Android animations can be disabled to save power. If this is the case, the animations immediately jump to their finished state.

Let’s modify the TestPage to contain just a Label and a BoxView and a Button:

We’ll use the button to start our animations. We can now simplify the TestViewModel class like so:

We’re going to use the code-behind file to implement the animations. And now let’s have a look at some of the basic animations one by one.

Fading

Most animation methods take two parameters. The first parameter is the target value and the second parameter is the duration of the animation in milliseconds.

Let’s make the box fade out in three seconds. The method takes the current value of the Opacity property (which is 1) for the start of the animation and fades out from this value to 0:

If you now run the app and press the button, the box will start fading out:

Let’s move on to the next basic animation.

Translation

To move an element from one location to another we use the TranslateTo method. It takes three parameters. The first two are for the TranslationX and TranslationY properties, and the last one is the duration of the animation.

Let’s translate the box 200 device-independent units to the right and 50 units down over a period of 4 seconds:

Press the button and the box’s journey will begin:

Next, let’s see how rotation works.

Rotation and Relative Rotation

We can rotate around any of the three axes: X, Y or Z. The Z axis is the one that goes through the screen, so, if we rotate around it, the element will rotate in the plane of the screen. This is the most common scenario. We use the Rotate method for this rotation. We pass the target angle and duration as parameters.

So, let’s rotate the label 15 degrees in clockwise direction over a period of 2 seconds:

Here’s what it looks like:

In order to rotate around the X and Y axes, we respectively use the RotateXTo and RotateYTo methods. Let’s rotate around the X axis first:

Here’s the result:

And now around the Y axis:

Here’s the result:

The methods above rotate from the current value of Rotation, RotationX or RotationY to the target values. Let’s set the Rotation property in the XAML file to 45:

Let’s use the RotateTo method and pass the angle of 90 degrees as the first argument:

The label will now be rotated from the angle of 45 degrees to the angle of 90 degrees:

If we want to to rotate it by an angle of 90 degrees, so from 45 to 135 degrees, we should use relative rotation. This can be accomplished by using the RelRotateTo method:

Here’s the result:

Let’s now remove the Rotation property from the XAML code.

Scaling and Relative Scaling

The scaling methods work pretty much the same as the rotating ones. We have the ScaleTo method to scale the element uniformly in both directions, the ScaleXTo and ScaleYTo methods to scale it along just one axis, and RelScaleTo for relative scaling.

All these methods take the scaling factor as the first parameter and duration as the second.

So, let’s scale down the box over a period of 2 seconds:

Here’s the result:

Now let’s scale it only along the X axis:

This time, we’re making it wider:

The ScaleYTo method works in a similar way, but along the Y axis. The RelScaleTo method scales the element relative to its current value.

In the rotating and scaling animations above, the transformation is always performed relative to the center of an element. But it doesn’t have to be the case.

Anchors

We can set the center of rotation or scaling by using the AnchorX and AnchorY properties. The values should be between 0 (left for AnchorX and top for AnchorY) to 1 (right for AnchorX and bottom for AnchorY). The default value of either property is 0.5, which corresponds to the center of the visual element.

Let’s rotate the box 45 degrees around its top-left corner:

Here’s the result:

Next, let’s scale the label relative to its bottom-right corner:

Here’s what we should see when the animation completes:

We know how to create single animations. But we can combine multiple animations to achieve all sorts of effects.

Compound Animations

Multiple animations can be combined sequentially. We just have to use the await keyword. Let’s create a compound animation on the box:

The whole compound animation takes 6.5 seconds to complete. First the box moves to the left. When this movement is complete, the rotation starts, and so on.

Composite Animations

Animations can also run simultaneously. This will happen if we omit the await keyword. Let’s translate, rotate and scale the box all at the same time:

If you run the app now and press the button, all three animations will start simultaneously:

The whole composite animation will take 3 seconds to complete.

We can also mix and match. Let’s create a composite animation where some single animations are awaited and others are not:

Here the label will be scaled and rotated simultaneously. The two rotation animations will be run sequentially at the same time as the scaling animation. Here’s the result after the whole composite animation completes:

We can also create composite animations where multiple asynchronous methods run concurrently. Let’s have a look at it next.

WhenAll and WhenAny

We can create composite animations using two methods, Task.WhenAny and Task.WhenAll. They return a Task object. We pass to them a collection of methods that also return a Task object.

The Task.WhenAny method completes when any of the methods in its collection completes. Have a look:

Here we have a Task.WhenAny method with two tasks. The first task starts translating the label to the right, the second task starts scaling it. The second task needs two seconds to complete. When it completes, the Task.WhenAny method also completes. While the translation is still running (it needs three more seconds to complete), the second ScaleTo method can start running. The second scaling and the translation will complete at the same time.

Whereas the Task.WhenAny method completes when any of its tasks completes, the Task.WhenAll method completes when all the tasks it contains complete. Have a look:

Here we have three tasks with different durations. The box first completes the rotation, then the scaling on the X axis, and then the translation. Only when the last task completes, the Task.WhenAll method completes. As last runs the scaling to zero.

Canceling Animations

We can easily cancel animations by calling the CancelAnimations method. Let’s add another button to the TestPage:

And here’s the code-behind:

When you press the first button, all three animations start running simultaneously. When you press the second button, they are immediately canceled.

Easing Functions

Animations can run with constant speed or with different speed and acceleration as they proceed. They can start slow and then accelerate. They can start fast and then decelerate. You can create your own patterns or use the ones we get out of the box.

There’s a class that contains easing functions that enable just that. The class is called Easing and it contains the following easing functions: BounceIn, BounceOut, CubicIn, CubicInOut, CubicOut, Linear, SinIn, SinInOut, SinOut, SpringIn and SpringOut. The In suffix is used if the effect should be visible at the beginning of the animation. The suffix Out – if at the end. If both suffixes are used, the effect is visible at both the beginning and the end.

To use an easing function, you just add it as the last argument to the animation method. Let’s check it out.

We’ll create a simple translation for our box. By default the Linear easing function is used. All the animations we’ve created so far use this default value. Let’s bounce the box at the beginning of the animation. To this end, we can use the BounceIn easing function:

Run the animation to see how it works.

Or let’s smoothly accelerate the box at the beginning and decelerate it at the end of the animation:

This effect is often used.

Feel free to check out the other easing functions. They also work with rotation or scaling.

Custom Animations

We can use the Animation class to create custom animations. We use the Commit method to run an animation. Let’s create and run an animation in the TestPage. Let’s first remove the second button and the related code in the code-behind. The code should now look like so:

We pass a callback, the start value and the end value as arguments to the Animation class’s constructor. As the callback we use a lambda expression that sets the box’s Rotation property to values between the start value (0 degrees) and the end value (45 degrees).

Then we run the animation. The Commit method takes a few parameters. The first parameter is the owner of the animation. This is the visual element the animation is set on or another visual element, such as the page. In our case we pass this for the page.

The second parameter is the name of the animation. The name and the owner are used jointly to uniquely identify the animation.

The third parameter is the rate of the animation. It’s expressed in milliseconds. In our case the rate is 16, so 16 milliseconds must elapse between calls to the callback method we passed to the Animation object’s constructor.

The fourth parameter is the duration of the animation. In our case it’s 3000 milliseconds, so 3 seconds.

The fifth parameter is the easing function we want to use for the animation.

The sixth parameter is a callback that will be executed when the animation completes. It takes two arguments, v and c. The v argument is the final value. The c argument, of type bool, is set to true if the animation is canceled.

Finally, the last parameter is a callback that we use to repeat the animation. In our case it returns true, so the animation will be repeated.

Now run the app and hit the button. You’ll see the box rotate, then go to the state with Rotation equal to zero. And this pattern will repeat:

Child Animations

We can combine and synchronize multiple animations in a parent-child relationship. To do that, we create a parent animation and add child animations to it. For the child animations we specify the time frame in which they are supposed to run relative to the duration of the parent animation. Let’s create an example to demonstrate how it works.

We’ll create a parent animation using the parameterless constructor. Next, we’ll create three child animations using the constructor with parameters. By the way, this time we’ll define the easing functions in the constructor, which is an alternative way of doing it. Finally, we’ll add the child animations to the parent animation. Here’s the code:

The first two arguments passed to the Add method are the time frames for the child animations. They must be between 0 and 1. So, the first animation will run for the entire duration of the parent animation. The second child animation will run during the first half of the parent animation, and the third child animation will run during the second half.

If you now run the app, you’ll see the label scale up for 5 seconds. During the first 2.5 seconds it will rotate in clockwise direction, during the next 2.5 seconds it will rotate back. Additionally, we change the Text property of the label after the animation is finished (before the next repetition):

The code above can be simplified:

Animating Properties

Visual elements implement the IAnimatable interface, which contains the Animate method. We can use this method to create and start an animation.

For example, we can animate the label’s FontSize property like so:

The Animate method takes a couple arguments. The first argument is the name, which is used as a unique key. The second argument is the animation that we want to run. Here we’re animating the FontSize property of the label from 5 to 50. The two remaining arguments are the rate and the duration of the animation. There are also other overloaded versions of the Animate method.

We can even animate the whole ContentPage. Let’s rotate it:

This animation looks like this:

Animations in MVVM

Our project uses the MVVM pattern. Let’s have a look at how to create animations in an app that uses that pattern. First, make sure your TestViewModel class looks like so:

Here we have two properties and a method. The first property will indicate whether an element (like the box in our case) should be visible or hidden. The second property will be used to indicate whether a simulated task is running.

The method will just set the IsRunning property to true, wait for 5 seconds and then set it to false. This method will be used as a command.

Let’s now have a look at the XAML file:

It hasn’t changed much. The label’s Text is now initially set to “0” and we don’t have the Clicked event anymore. The button’s Command property now binds to the method we created in the view model.

Finally, let’s have a look at the code-behind:

Here we have an instance of our view model. In the constructor we create an animation. We define a callback and the start and end values.

We create a method, HandleTransformations, and pass it as the callback. In the method we change the rotation of the box, modify the text on the label and hide or show the box.

We also have the PropertyChanged event handler in the constructor. In the Vm_PropertyChanged method we start the animation when the value of IsRunning in the view model changes to true. We cancel the animation when it changes to false.

If we now run the app and press the button, we’ll see the box rotate and the label text change. Every 20 degrees the box will disappear or reappear:

 Fine. We know enough about animations to implement them in our project.

Animations in the Slugrace App

We’re going to create several custom animations in our project. The most important one is the translation of the slug images from left to right. This is a racing game after all, so the slugs must be able to run.

We’re also going to animate the slugs’ tentacles. Rotating them will definitely bring some life to the slugs.

Finally, we’re going to implement some animations related to accidents that may happen to the slugs.

The Running Animation

So, let’s start with the running animation. The racetrack image and the slug images are inside the TrackImage control. Let’s give them names so that we can reference them later:

The slug running animations will be implemented in the code behind because they transform the graphics. But we’ll need a link to the GameViewModel in order to correlate the states of the slugs and of the game with the animated graphics.

Let’s create that link by adding an instance of GameViewModel in the code-behind:

We’ll create four animations, one for each slug. The slugs will run along the racetrack. This is why we need to know the length of the racetrack. However, the Width property of the track is unavailable in the constructor because at the time when the constructor is run, the children are not yet laid out.

To make sure that the Width property is set, we have to override the LayoutChildren method. We create a trackLength variable to hold the distance the slugs should cover.

Also in the LayoutChildren method we instantiate the four animations. As the first argument, we pass methods to them that we define below. These methods just take care of translating the SlugImage objects. As the start and end values we pass 0 and trackLength respectively:

We have the animations, but we haven’t run them yet. When do we want them to run? Well, they should start when the race begins. And this happens when the race status changes to Started.

So, we need a way to tell when the status changes. To this end, we’ll use the PropertyChanged event handler. Let’s add a BindingContextChanged to the TrackImage class:

In the code-behind, let’s assign the binding context to the view model instance and let’s assign a Vm_PropertyChanged method to the event handler:

Before we proceed, let’s add a RunningTime property to the SlugViewModel class:

We’ll also need three time-related properties in the GameViewModel:

What do they refer to?

The first property, RaceTime, is the time of the entire race, so until the last slug finishes the race.

The second property, MinTime, is the time the fastest slug needs to finish the race.

The FinishTime property is the time when the race should be considered resolved because the winner is already known, but the slugs are still running.

And now let’s take care of all the different scenarios as far as the change of the race status is concerned.

So, if the status changes to Started, we first define the running times of all the slugs. The shorter the time, the faster the slug will run. The first slug, for example, will have a random running time between 3000 and 7000 milliseconds. This is going to be the statistically fastest slug. No other slug will be able to cover the distance in 3 seconds.

Then we create an array of the running times. Next, we set the three time properties we just added to the GameViewModel.

The RaceTime is set to the running time of the slowest slug.

The MinTime is set to the running time of the fastest slug.

The FinishTime is set to 79% of MinTime.

We’re going to use these properties in the RunRace method in the view model. With this in place, we start the animations.

If the race status changes to NotYetStarted, which happens after each race when we hit the Next Race button, the slugs’ TranslationX property is reset and the images move to where they started off.

If the status changes to Finished, the animations are canceled and the slugs stop running. Here you can see all three cases:

Now, let’s have a look at the RunRace method in the GameViewModel:

We introduce a delay of a couple seconds. During this time, the race status is Started and the slugs are running. The RaceWinnerSlug is now set to the fastest slug. We have the winner, but the slugs are still running until the slowest one finishes. Then we continue like before.

There is also one change in the WinnerInfo class. We want the winner info to be visible not when all slugs finish the race, but rather when the winner is known, so after the fastest slug crosses the finish line.

So, we bind the IsVisible property to RaceWinnerSlug. We also use the IsNotNullConverter from the Community Toolkit, so that the WinnerInfo view should be visible only when there is a RaceWinnerSlug:

As you can see, we don’t need the EnumToBoolConverter anymore.

We also must make sure that the RaceWinnerSlug is set to null in the NextRace method in GameViewModel:

This way the WinnerInfo view will be invisible when the next race begins.

If we now run the app and then start the first race, the slugs start running:

The moment the first slug crosses the finish line, the WinnerInfo is displayed:

The slugs continue running until all of them have completed the race:

Here’s what you should see if you ran the app on Android:

Now the race is finished and we can hit the Next Race button. This will move the slugs to the beginning of the track.

Rotating the Tentacles

The tentacle rotation (or eye rotation, it really doesn’t matter what you call it) is pretty simple. The two tentacles will constantly rotate at a random speed, different for each slug.

We’re going to implemenet this animation using child animations. Each slug has two tentacles. Let’s give them names so that we can reference them in code. Here’s the SlugImage class in XAML:

And now let’s implement the rotation in the code-behind:

Here we have four child animations, two for the left eye and two for the right eye. Each eye rotates in one direction during the first half of the animation and then back. This animation is repeated.

Now, as soon as the slugs appear on the screen, they rotate their tentacles:

Great, we have animations in our app now. But how cool would it be if we had some background music and sound effects as well? Well, this is what we’re going to see to in the next part of the series.


Spread the love

Leave a Reply