Awaitable Managed Animation classes for WinRT (Part 3)

In the previous two posts, I set up the reasoning and infrastructure for creating a different type of animation infrastructure. In this post, I’ll (finally) show an actual example of how to use this infrastructure.

For this, we’ll create a simple application. Our first goal is to place numbered cards (1-5) on our canvas. The cards are represented by a simple UserControl named NumberCardControl which has a Text property specifying the content.

Our initialization code will dynamically create the cards and place them at the bottom of the screen. The final result will look like this:

image

And this is what the code looks like (it uses a couple of non-standard extension methods to simplify working with the Canvas control):

void MainPage_Loaded(object sender, RoutedEventArgs e)
{
double width = canvas.ActualWidth;
double size = width / 20;
double top = canvas.ActualHeight / 4 * 3;
for (int i = 0; i < 5; i++)
{
NumberCardControl control = new NumberCardControl();
control.Text = i.ToString();
control.SetCanvasLocation(new Point(width / 5 * i + width / 5 / 2, top));
control.Width = control.Height = size;

// When we do the animation, we will uncomment this code.

// control.Opacity = 0;
canvas.Children.Add(control);

}
}

Animation anyone?

Right now, the code simply places the cards on the canvas. There’s no entrance animation or anything happening. It’s very abrupt. What if we want to animate these controls in? Say, by changing their opacity from 0 to  1 gradually?

That’s where our infrastructure (finally) enters… First, we’ll need a derived class that knows how to animate opacity:

public class OpacityManagedAnimation : ManagedAnimation
{
public double Opacity{ get; set; }
public TimeSpan Duration { get; set; }
public EasingFunctionBase EasingFunction { get; set; }

public OpacityManagedAnimation(double opacity, TimeSpan duration, EasingFunctionBase easingFunction = null)
{
Opacity = opacity;
Duration = duration;
EasingFunction = easingFunction;
}

There are three properties we will use:

  1. Opacity: This is the target opacity we want the element to arrive at.
  2. Duration: This is the total duration of the animation.
  3. EasingFunctionBase: This is the easing function we want the opacity to animate with.

Finally, we have a constructor that sets all of these (and has a default for the easing function).

Next, we need to implement the two virtual functions:

public override void Retain()
{
Element.Opacity = ForceFinalValueRetained ? Opacity : Element.Opacity;
}

protected override Windows.UI.Xaml.Media.Animation.Storyboard CreateStoryboard()
{
Storyboard board = new Storyboard();
var timeline = new DoubleAnimationUsingKeyFrames();
Storyboard.SetTarget(timeline, Element);
Storyboard.SetTargetProperty(timeline, "Opacity");
var frame = new EasingDoubleKeyFrame() { KeyTime = Duration, Value = Opacity, EasingFunction = EasingFunction};
timeline.KeyFrames.Add(frame);
board.Children.Add(timeline);

return board;
}

As you can see, Retain is pretty straight-forward. It either takes the target Opacity value or the current Opacity (depending on ForceFinalValueReturned that I discussed in the previous post) and sets it into the property so that when we stop the Storyboard, the element will have the Opacity we actually want.

Next, we need to create the Storyboard. This is fairly simple boilerplate code for anyone who coded Storyboards before, so just to summarize what it does:

  1. Create an instance of a Storyboard and Timeline classes.
  2. Tell the timeline what object and property it’s going to animate.
  3. Create a frame that will specify the animation to cause (with the Value property set to the target opacity).
  4. Add the frame to the timeline and the timeline to the storyboard.
  5. return the storyboard.

That’s it! We now have our first animation class and we can start using it!

Simple animation

First, let’s do some simple animation with the cards. We’ll make them all appear on the canvas at the same time, within 1 second of the page appearing.

The code is fairly simple:

TimeSpan duration = TimeSpan.FromSeconds(1);
foreach (var item in canvas.Children.OfType<FrameworkElement>())
{
new OpacityManagedAnimation(1, duration).Activate(item);
}

We new up an OpacityManagedAnimation class and immediately call the .Activate() method on it.

This is an awaitable operation (Activate() returns a Task) but in this instance we are not even awaiting it because we don’t really need to do anything with it.

Running the app, you will see the the elements appearing together!

Staggering the animation

The animation is pretty boring right now. The cards appear at the same time. What if we wanted to stagger the animation so that they appear one after the other?

private async Task StaggeredAnimationAsync()
{
TimeSpan duration = TimeSpan.FromSeconds(1);
List<Task> tasks = new List<Task>();
foreach (var item in canvas.Children.OfType<FrameworkElement>())
{
tasks.Add(new OpacityManagedAnimation(1, duration).Activate(item));
await Task.Delay(300);
}

await Task.WhenAll(tasks.ToArray());
}

Here’s what we are doing:

  1. We go over the controls, activating the opacity animation just like we did before.
  2. However, we also await a call to Delay()  which will essentially add a short pause before each activation.
  3. We also collect the animation tasks we added into a list.
  4. At the end, we await all the animation tasks so that the overall Task will be done only once all the animations are done.

Flickering the elements

To make our example even more complicated, let’s say we want to flicker the elements – go up to 0.6 opacity, then back to 0.2, then to 0.7, then to 0.3 until we reach an opacity of 1  (+0.1 for the target, and go down by 0.4 until we hit 1 as the opacity).

Again – this is a very easy piece of code to run. First, we’ll write up the piece of code that will calculate the opacities:

private async Task FlickerAndAppearAnimationAsync()
{
for (double max = 6; max <= 10; max += 1)
{
await FlickerToValueAsync(max / 10, max / 10 - 0.4);
}
}

The code simply loops and makes the calculations I described above – it then calls FlickerToValueAsync()which takes the top value, and the value we want to back down to each time (until we reach full opacity).

 

private async Task FlickerToValueAsync(double to, double backDownTo)
{
TimeSpan duration = TimeSpan.FromSeconds(0.5);
List<Task> tasks = new List<Task>();
foreach (var item in canvas.Children.OfType<FrameworkElement>())
{
tasks.Add(new OpacityManagedAnimation(to, duration).Activate(item));
}

await Task.WhenAll(tasks.ToArray());
await Task.Delay(100);

if (to < 1)
{
tasks.Clear();
foreach (var item in canvas.Children.OfType<FrameworkElement>())
{
tasks.Add(new OpacityManagedAnimation(backDownTo, duration).Activate(item));
}

await Task.WhenAll(tasks.ToArray());
}
}

The code has two loops in it. The first brings the various elements in the canvas to the “to” opacity (1st argument) and the second loop (which only runs if the target opacity is still less than 1) brings the opacity back down to the backDownTo argument.

In the middle, we add a short delay and that’s it. Voila.

Is this really easier than traditional Storyboards?

The short answer is that it is and it isn’t.

Using this method is more complicated than had you defined your Storyboard in a rigid manner in Blend, for example. However.. What if your list of 5 controls was not rigid? In the code I presented, if you want 10 controls, you simply touch that number in the initialization function and voila! Everything keeps working – no need to adjust any Blend designer or anything. No need to copy animations over in XAML. It’s all written in code and is fully dynamic. The staggered animation, for example, will always be perfectly staggered without having to manually set the timelines in XAML or Blend.

In the next post, I’ll discuss some more ManagedAnimation derivatives you can use to animate elements in your XAML application in a similar manner.

Advertisements
This entry was posted in Animation, Dev, Windows8. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s