Love Meter (Keyframe Animations)

2
290

Love Meter is a silly, but fun, novelty app. If you can’t decide whether to start or continue a relationship with someone, the Love Meter app can tell you how much chemistry you and your potential mate have. All you need to do is to convince the other person to hold their finger on your phone’s screen for about 8 seconds while you do the same. While you do this, Love Meter shows a beating heart and progress bar as it performs its analysis and then reports how much love exists between you two, on a scale from 0 to 100%. A red heart fills a white heart border with the same percentage, to help you visualize the percentage somewhat like a pie chart.

This app demonstrates a new category of animations— keyframe animations.

Love Meter doesn’t actually measure anything!

It should go without saying, but this app is for entertainment purposes only.Windows phones do not (yet!) have a sensor for measuring human chemistry

This app requires multi-touch!

Therefore, you cannot test it as-is on the emulator unless your computer supports multi-touch.

Keyframe Animations

Sometimes the animation behavior you desire cannot be represented by linear interpolation or any of the built-in easing functions. For example, Love Meter performs its heartbeat animation by animating the scale of a vector heart graphic marked with the following ScaleTransform:

[code]

<!– The solid red heart with a complex geometry –>
<Path …>
<Path.RenderTransform>
<!– The target of the heartbeat and final animations –>
<ScaleTransform x:Name=”HeartScale” ScaleX=”0” ScaleY=”0”/>
</Path.RenderTransform>
</Path>

[/code]

To make its scale grow in a heartbeat pattern (with alternating small and large “beats”), you might try to create a multi-animation storyboard as follows (for ScaleX, at least):

[code]

<!– Horizontal stretching and shrinking –>
<Storyboard x:Name=”HeartbeatStoryboard” Storyboard.TargetName=”HeartScale”
Storyboard.TargetProperty=”ScaleX” RepeatBehavior=”6x”>
<DoubleAnimation BeginTime=”0:0:0” To=”0”/>
<DoubleAnimation BeginTime=”0:0:.4” To=”0”/>
<DoubleAnimation BeginTime=”0:0:.6” To=”.5”/>
<DoubleAnimation BeginTime=”0:0:.8” To=”0”/>
<DoubleAnimation BeginTime=”0:0:1” To=”1”/>
<DoubleAnimation BeginTime=”0:0:1.4” To=”0”/>
</Storyboard>

[/code]

However, this fails with an InvalidOperationException that explains, “Multiple animations in the same containing Storyboard cannot target the same property on a single element.” You could split this up into multiple storyboards and start one when another one ends, but that’s cumbersome to manage.

Instead, you can use a keyframe animation that supports as many distinct segments as you want. Keyframe animations enable you to specify any number of keyframes—specific property values at specific times—rather than being limited to a single From value and a single To value. For example, the preceding heartbeat animation can be correctly written as follows:

[code]

<!– Horizontal stretching and shrinking –>
<Storyboard x:Name=”HeartbeatStoryboard” Storyboard.TargetName=”HeartScale”
Storyboard.TargetProperty=”ScaleX” RepeatBehavior=”6x”>
<DoubleAnimationUsingKeyFrames>
<LinearDoubleKeyFrame KeyTime=”0:0:0” Value=”0”/>
<LinearDoubleKeyFrame KeyTime=”0:0:.4” Value=”0”/>
<LinearDoubleKeyFrame KeyTime=”0:0:.6” Value=”.5”/>
<LinearDoubleKeyFrame KeyTime=”0:0:.8” Value=”0”/>
<LinearDoubleKeyFrame KeyTime=”0:0:1” Value=”1”/>
<LinearDoubleKeyFrame KeyTime=”0:0:1.4” Value=”0”/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>

[/code]

This only animates ScaleX, so Love Meter uses an identical keyframe animation for ScaleY as well, to grow and shrink the heart’s scale in sync.

The use of keyframes requires a keyframe-enabled animation class. For this case, DoubleAnimation’s companion DoubleAnimationUsingKeyFrames class is used. The other three animation classes have corresponding keyframe classes as well. The keyframe animation classes have the same properties as their counterparts except for From, To, and By, as that information is represented inside each child keyframe.

Interpolation can be done between each keyframe, and the interpolation can be different between each pair. This is based on which type of keyframe is used, out of the four available:

  • Linear keyframes—Perform basic linear interpolation.
  • Easing keyframes—Perform interpolation based on the specified easing function.
  • Spline keyframes—Perform interpolation based on a spline object that describes the desired motion as a cubic Bézier curve.
  • Discrete keyframes—Perform no interpolation; the value jumps to the new value at the appropriate time.

Inside DoubleAnimationUsingKeyFrames, you choose from the four types of keyframes by using a LinearDoubleKeyFrame, EasingDoubleKeyFrame, SplineDoubleKeyFrame, or DiscreteDoubleKeyFrame. Inside ColorAnimationUsingKeyFrames, you choose by using a LinearColorKeyFrame, EasingColorKeyFrame, SplineColorKeyFrame, or DiscreteColorKeyFrame. And so on.

The type of keyframe chosen affects the interpolation between the previous value and its own value. Linear and easing keyframes enable the same familiar capabilities as nonkeyframe animations, but on a per-keyframe basis. Spline and discrete behavior is specific to keyframe animations. Figure 14.1 illustrates the motion enabled by applying the following storyboard to a heart on a canvas:

[code]

<Storyboard x:Name=”Figure14_1_Storyboard” Storyboard.TargetName=”Heart”>
<!– Move the heart vertically in a complicated pattern –>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=”(Canvas.Top)”>
<LinearDoubleKeyFrame Value=”0” KeyTime=”0:0:0”/>
<LinearDoubleKeyFrame Value=”200” KeyTime=”0:0:1”/>
<DiscreteDoubleKeyFrame Value=”0” KeyTime=”0:0:2”/>
<LinearDoubleKeyFrame Value=”0” KeyTime=”0:0:3”/>
<SplineDoubleKeyFrame Value=”200” KeySpline=”0,1 1,0” KeyTime=”0:0:4”/>
</DoubleAnimationUsingKeyFrames>
<!– Move the heart horizontally (linearly) at the same time –>
<DoubleAnimation Storyboard.TargetProperty=”(Canvas.Left)”
From=”0” To=”500” Duration=”0:0:4”/>
</Storyboard>

[/code]

The type of the first keyframe never matters, as there’s no previous value from which to interpolate. In this example, the type of the fourth keyframe is also irrelevant because the keyframe’s value (0) is identical to the preceding value.

FIGURE 14.1 Motion enabled by a mixture of linear, discrete, and spline keyframes
FIGURE 14.1 Motion enabled by a mixture of linear, discrete, and spline keyframes

Spline Keyframes and Bézier Curves

The spline keyframe classes have a KeySpline property that defines the interpolation as a cubic Bézier curve. Bézier curves (named after engineer Pierre Bézier) are commonly used in computer graphics for representing smooth curves, and are even used by fonts to mathematically describe curves in their glyphs.

The basic idea is that in addition to two endpoints, a Bézier curve has one or more control points that give the line segment its curve.These control points are not visible (and not necessarily on the curve itself ) but rather are used as input to a formula that dictates where each point on the curve exists. Intuitively, each control point acts like a center of gravity, so the line segment appears to be “pulled” toward these points.The control points specified inside KeySpline are relative, where the start of the curve is 0 and the end is 1.

Finding the right value for KeySpline that gives the desired effect can be tricky and almost certainly requires the use of a design tool such as Expression Blend. But several free tools can be found online that help you visualize Bézier curves based on the specified control points.

The User Interface

Love Meter has a single page besides its instructions and about pages, whose code isn’t shown in this chapter. Listing 14.1 contains the main page’s XAML.

LISTING 14.1 MainPage.xaml—The Main User Interface for Love Meter

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”Portrait” shell:SystemTray.IsVisible=”True”>
<!– The application bar, for instructions and about –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<!– Add three storyboards to the page’s resource dictionary –>
<phone:PhoneApplicationPage.Resources>
<!– The storyboard for the heartbeat scale animation (repeated 6 times) –>
<Storyboard x:Name=”HeartbeatStoryboard” Storyboard.TargetName=”HeartScale”
RepeatBehavior=”6x”>
<!– The horizontal stretching and shrinking –>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=”ScaleX”>
<LinearDoubleKeyFrame KeyTime=”0:0:0” Value=”0”/>
<LinearDoubleKeyFrame KeyTime=”0:0:.4” Value=”0”/>
<LinearDoubleKeyFrame KeyTime=”0:0:.6” Value=”.5”/>
<LinearDoubleKeyFrame KeyTime=”0:0:.8” Value=”0”/>
<LinearDoubleKeyFrame KeyTime=”0:0:1” Value=”1”/>
<LinearDoubleKeyFrame KeyTime=”0:0:1.4” Value=”0”/>
</DoubleAnimationUsingKeyFrames>
<!– The vertical stretching and shrinking (in sync) –>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=”ScaleY”>
<LinearDoubleKeyFrame KeyTime=”0:0:0” Value=”0”/>
<LinearDoubleKeyFrame KeyTime=”0:0:.4” Value=”0”/>
<LinearDoubleKeyFrame KeyTime=”0:0:.6” Value=”.5”/>
<LinearDoubleKeyFrame KeyTime=”0:0:.8” Value=”0”/>
<LinearDoubleKeyFrame KeyTime=”0:0:1” Value=”1”/>
<LinearDoubleKeyFrame KeyTime=”0:0:1.4” Value=”0”/>
</DoubleAnimationUsingKeyFrames>
<!– Ensure the result text is hidden when beginning –>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName=”ResultTextBlock”
Storyboard.TargetProperty=”Opacity”>
<DiscreteDoubleKeyFrame KeyTime=”0:0:0” Value=”0”/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<!– The storyboard that animates the progress bar –>
<Storyboard x:Name=”ProgressStoryboard”
Completed=”ProgressStoryboard_Completed”>
<!– Show the progress bar at the beginning and hide it at the end –>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName=”ProgressPanel”
Storyboard.TargetProperty=”Opacity”>
<DiscreteDoubleKeyFrame KeyTime=”0:0:0” Value=”1”/>
<DiscreteDoubleKeyFrame KeyTime=”0:0:8.4” Value=”0”/>
</DoubleAnimationUsingKeyFrames>
<!– Animate its value from 0 to 100% –>
<DoubleAnimation From=”0” To=”100” Duration=”0:0:8.4”
Storyboard.TargetName=”ProgressBar”
Storyboard.TargetProperty=”Value”/>
</Storyboard>
<!– A final random animation before displaying the result –>
<Storyboard x:Name=”FinalStoryboard” Storyboard.TargetName=”HeartScale”>
<!– Horizontal stretching and shrinking, set via code-behind –>
<DoubleAnimationUsingKeyFrames x:Name=”FinalAnimationX”
Storyboard.TargetProperty=”ScaleX”/>
<!– Vertical stretching and shrinking, set via code-behind –>
<DoubleAnimationUsingKeyFrames x:Name=”FinalAnimationY”
Storyboard.TargetProperty=”ScaleY”/>
<!– Show the result at the end of the animation –>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName=”ResultTextBlock”
Storyboard.TargetProperty=”Opacity”>
<DiscreteDoubleKeyFrame KeyTime=”0:0:1” Value=”1”/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</phone:PhoneApplicationPage.Resources>
<!– Transparent background to receive touch input –>
<Grid Background=”Transparent”>
<!– Mini-header –>
<TextBlock Text=”LOVE METER” Margin=”24,16,0,12”
Style=”{StaticResource PhoneTextTitle0Style}”/>
<!– The progress bar and corresponding text block –>
<StackPanel x:Name=”ProgressPanel” Opacity=”0” Margin=”0,60,0,0”>
<TextBlock Margin=”24,0” Text=”Measuring chemistry…”/>
<ProgressBar x:Name=”ProgressBar” VerticalAlignment=”Top” Margin=”12,24”/>
</StackPanel>
<!– The solid red heart with a complex geometry –>
<Path Width=”436” Stretch=”Uniform” Fill=”#E51400” Margin=”12,0”
HorizontalAlignment=”Center” VerticalAlignment=”Center”
RenderTransformOrigin=”.5,.5”
Data=”F1 M 349.267,270.347C 374.787,266.867 401.253,269.427
425.267,278.92C 453.48,289.173 477.067,309.027 496.333,331.6C
507.533,345.013 516.68,360 524.547,375.56C 527.587,381.733
529.893,388.253 533.333,394.24C 537.573,386.76 540.2,378.52
544.467,371.08C 555.253,351.573 567.667,332.667 583.84,317.173C
597.32,303.027 613.707,291.773 631.36,283.467C 660.36,269.16
694.16,265.547 725.76,271.92C 746.72,276.547 766.8,285.627
783.72,298.92C 799.147,311.693 812.573,327.133 821.52,345.173C
831.867,366.267 837.827,389.773 837.373,413.333C 838.707,448.413
829.133,483.093 814.987,514.933C 793.107,563.24 760.693,606.053
724.373,644.413C 712.653,658 699.253,669.973 686.2,682.213C
640.48,724.373 590.373,761.52 538.667,795.96C 536.653,797.013
534.6,798.733 532.213,798.493C 528.067,796.613 524.6,793.573
520.84,791.067C 468.253,756.28 417.973,717.8 372.107,674.493C
356,659.96 341.453,643.813 326.933,627.733C 311.28,609.84
296.267,591.293 283.4,571.28C 265.067,544.44 250.013,515.24
239.92,484.307C 233.48,462.133 228.32,439.227 229.28,416C
228.64,403.027 230.867,390.187 233.533,377.547C 241.507,342.733
263.187,311.213 293.16,291.72C 310.107,280.76 329.293,273.32
349.267,270.347 Z”>
<Path.RenderTransform>
<!– The target of the heartbeat and final animations –>
<ScaleTransform x:Name=”HeartScale” ScaleX=”0” ScaleY=”0”/>
</Path.RenderTransform>
</Path>
<!– The same heart with no fill and outlined in white or black –>
<Path Width=”456” Stretch=”Uniform” StrokeThickness=”12” Margin=”12,0”
Stroke=”{StaticResource PhoneForegroundBrush}”
Data=”…”/>
<!– A text block for displaying the resulting percentage –>
<TextBlock x:Name=”ResultTextBlock” Opacity=”0” FontSize=”108”
HorizontalAlignment=”Center” VerticalAlignment=”Center”/>
</Grid>
</phone:PhoneApplicationPage>

[/code]

Notes:

  • HeartbeatStoryboard contains the keyframe animation shown earlier for the horizontal component of the beating visualization (ScaleX), as well as one for the vertical component (ScaleY). With RepeatBehavior on the storyboard, the beat pattern occurs six times.
  • HeartbeatStoryboard also contains a keyframe animation that “animates” the result text (shown at the end of the whole process) to an opacity of 0. This is done for the benefit of subsequent runs during the same session, because the result text already has an opacity of 0 before the first run. Rather than making the text fade out, the animation instantly sets the opacity to 0 with a single discrete keyframe that takes effect at the start of the storyboard.
  • The first animation inside ProgressStoryboard uses the same technique to instantly show the progress bar and its text block (inside ProgressPanel) at the start of the storyboard and instantly hide it at the end. The normal DoubleAnimation smoothly and linearly animates the progress bar’s value from 0 to 100 over the course of 8.4 seconds, which is how long it takes for HeartbeatStoryboard to finish. The codebehind initiates HeartbeatStoryboard and ProgressStoryboard simultaneously when two fingers touch the screen. The progress UI inside ProgressPanel is shown in Figure 14.2.
FIGURE 14.2 While the heart beats, the text and progress bar inside ProgressPanel shows how much more time is needed to finish the app’s “analysis.”
FIGURE 14.2 While the heart beats, the text and progress bar inside ProgressPanel shows how much more time is needed to finish the app’s “analysis.”
  • FinalStoryboard is started by code-behind after HeartbeatStoryboard and ProgressStoryboard finish. It randomly shrinks and stretches the heart for a second before revealing ResultTextBlock. This is done by adding keyframes with random values in code-behind.
  • The grid uses a transparent background, so the fingers can be pressed anywhere on the screen and the appropriate event gets raised.
  • The heart is vector-based, so it can scale to any size and still look crisp. Although not shown here, the outline’s Data property is set to the same long string used for the heart’s Data property.
  • On the ScaleTransform, ScaleX is a multiplier for the element’s width, and ScaleY is a multiplier for its height. (A ScaleX value of 0.5 shrinks an element’s rendered width in half, whereas a ScaleX value of 2 doubles the width.) Their default value is 1, but they are both initialized to 0 in this case, so the red heart is initially invisible.

How do transforms such as ScaleTransform affect the values returned by an element’s ActualHeight and ActualWidth properties?

Applying a transform never changes the values of these properties.Therefore, because of transforms, these properties can “lie” about the size of an element on the screen. For example, the red heart in Love Meter always reports 436 as its ActualWidth value, despite the initial ScaleTransform that makes its actual size 0.

Such “lies” might surprise you, but they’re for the best. First, it’s debatable how such values should even be expressed for some transforms.More importantly, the point of transforms is to alter an element’s appearance without the element’s knowledge. Giving elements the illusion that they are being rendered normally enables custom elements to be plugged in and transformed without special handling.

The Code-Behind

Listing 14.2 contains the code-behind for the main page.

LISTING 14.2 MainPage.xaml.cs—The Code-Behind for Love Meter’s Main Page

[code]

using System;
using System.Windows.Input;
using System.Windows.Media.Animation;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// The secret chemistry-measuring algorithm is just choosing a random number!
Random random = new Random();
public MainPage()
{
InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// This is application-wide, so only listen while on this page
Touch.FrameReported += Touch_FrameReported;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Unhook the handler attached in OnNavigatedTo
Touch.FrameReported -= Touch_FrameReported;
}
void Touch_FrameReported(object sender, TouchFrameEventArgs e)
{
TouchPointCollection fingers = e.GetTouchPoints(this);
// Stop the storyboards if there aren’t two fingers currently on the screen
if (fingers.Count != 2 || (fingers.Count == 2 &&
(fingers[0].Action == TouchAction.Up ||
fingers[1].Action == TouchAction.Up)))
{
this.HeartbeatStoryboard.Stop();
this.ProgressStoryboard.Stop();
}
// Start the storyboards if two fingers are in contact and the second one
// just made contact, AND if the storyboards aren’t already running
else if (fingers.Count == 2 && (fingers[0].Action == TouchAction.Down ||
fingers[1].Action == TouchAction.Down)
&& this.HeartbeatStoryboard.GetCurrentState() != ClockState.Active)
{
this.HeartbeatStoryboard.Begin();
this.ProgressStoryboard.Begin();
}
}
// Called when the progress bar reaches 100%
void ProgressStoryboard_Completed(object sender, EventArgs e)
{
this.FinalStoryboard.Stop(); // So we can clear its keyframes
// Fill the X & Y animations with 10 random keyframes
this.FinalAnimationX.KeyFrames.Clear();
this.FinalAnimationY.KeyFrames.Clear();
for (int i = 0; i < 10; i++)
{
this.FinalAnimationX.KeyFrames.Add(new LinearDoubleKeyFrame {
KeyTime = TimeSpan.FromMilliseconds(i * 100),
Value = (double)random.Next(0, 101) / 100 });
this.FinalAnimationY.KeyFrames.Add(new LinearDoubleKeyFrame {
KeyTime = TimeSpan.FromMilliseconds(i * 100),
Value = (double)random.Next(0, 101) / 100 });
}
// Choose the result
double finalPercentage = random.Next(0, 101);
// Ensure that the otherwise-random animations end up at the right value
this.FinalAnimationX.KeyFrames.Add(new LinearDoubleKeyFrame { KeyTime =
TimeSpan.FromMilliseconds(1100), Value = finalPercentage / 100 });
this.FinalAnimationY.KeyFrames.Add(new LinearDoubleKeyFrame { KeyTime =
TimeSpan.FromMilliseconds(1100), Value = finalPercentage / 100 });
// Update the text block now, which still has an opacity of 0.
// It will be shown when FinalStoryboard finishes.
this.ResultTextBlock.Text = finalPercentage + “%”;
// Start the new random animations
this.FinalStoryboard.Begin();
}
// Application bar handlers
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=Love Meter”, UriKind.Relative));
}
}
}

[/code]

Notes:

  • This application has special handling to initiate the first two storyboards only when two fingers are simultaneously pressed on the screen, and to stop the storyboards otherwise. This is done with the multi-touch FrameReported event and corresponding functionality.
  • We only want to start the first two storyboards if they aren’t already in progress. This is especially important inside the FrameReported event handler because it gets called repeatedly while the two fingers are pressed down. To check the status of the storyboards, it calls the GetCurrentState method on one of them, which returns either Active, Stopped, or Filling. Filling represents the case for which an animation has completed, but the animated property values remain at their postanimation values. This always happens for a completed storyboard until its Stop method is called, unless its FillBehavior property is set to Stop to make it stop automatically on completion.
  • Inside ProgressStoryboard_ Completed, FinalStoryboard is filled with random-value keyframes before revealing the (also-randomly chosen) chemistry value in the final keyframe. Although the final keyframe uses the same value for both ScaleX and ScaleY, the intermediate keyframes do not. This produces a more interesting animation that morphs the heart in either dimension, as demonstrated in Figure 14.3.
FIGURE 14.3 The red heart compresses horizontally and/or vertically during the final random storyboard.
FIGURE 14.3 The red heart compresses horizontally and/or vertically during the final random storyboard.

The Finished Product

Love Meter (Keyframe Animations)

2 COMMENTS

  1. I’m a beginner in programming and I want to know how to make a specific path. (in your project there is a data for the heart path, how did you get that?)

    How can I get the data for that path or any path?

Comments are closed.