This chapter
is taken from book "Programming Windows Phone 7" by Charles Petzold published by
Microsoft press.
http://www.charlespetzold.com/phone/index.html
Animating Attached Properties
You can use Silverlight animations in a couple different ways to move an element around the screen. One way is to target a TranslateTransform set to the element's RenderTransform property. But programmers who are more comfortable with Canvas might want to animate the Canvas.Left and Canvas.Top attached properties. A special syntax is required to animate attached properties, but it's fairly simple.
This program defines a
Canvas that is 450 pixels square, centers it in the
content area, instantiates an Ellipse that is 50 pixels in size, and then moves
that Ellipse around the perimeter of the Canvas
in four seconds, repeated forever.
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<Canvas
Width="450"
Height="450"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Ellipse
Name="ball"
Fill="{StaticResource
PhoneAccentBrush}"
Width="50" Height="50"
/>
<Canvas.Triggers>
<EventTrigger>
<BeginStoryboard>
<Storyboard
RepeatBehavior="Forever">
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="ball"
Storyboard.TargetProperty="(Canvas.Left)">
<DiscreteDoubleKeyFrame
KeyTime="0:0:0"
Value="0"
/>
<LinearDoubleKeyFrame
KeyTime="0:0:1"
Value="400" />
<DiscreteDoubleKeyFrame
KeyTime="0:0:2"
Value="400"
/>
<LinearDoubleKeyFrame
KeyTime="0:0:3"
Value="0" />
<DiscreteDoubleKeyFrame
KeyTime="0:0:4"
Value="0"
/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="ball"
Storyboard.TargetProperty="(Canvas.Top)">
<DiscreteDoubleKeyFrame
KeyTime="0:0:0"
Value="0"
/>
<DiscreteDoubleKeyFrame
KeyTime="0:0:1"
Value="0"
/>
<LinearDoubleKeyFrame
KeyTime="0:0:2"
Value="400" />
<DiscreteDoubleKeyFrame
KeyTime="0:0:3"
Value="400"
/>
<LinearDoubleKeyFrame
KeyTime="0:0:4"
Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Canvas.Triggers>
</Canvas>
</Grid>
Notice that the
Storyboard.TargetName is set to reference the Ellipse
element, and the
Storyboard.TargetProperty attributes are set to the
strings "(Canvas.Left)" and "(Canvas.Top)". When targeting attached properties
in an animation, put the fully-qualified property names in parentheses.
And now, the downside: Animations that target properties of type
Point are
not handled in the GPU
on the render thread. If that's a concern, stick to animating properties of type
double.
If you value fun more than performance, you can construct a
PathGeometry using
explicit PathFigure,
LineSegment,
ArcSegment,
BezierSegment, and
QuadraticBezierSegment
objects, and every property of type
Point can be an animation target.
Here's a program that stretches that concept to an extreme. It creates a
circle from four Bezier splines, and then animates the various
Point properties,
turning the circle into a square and solving a geometric problem that's been
bedeviling mathematicians since the days of Euclid:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<Path
HorizontalAlignment="Center"
VerticalAlignment="Center"
Fill="{StaticResource
PhoneAccentBrush}"
Stroke="{StaticResource
PhoneForegroundBrush}"
StrokeThickness="3"
>
<Path.Data>
<PathGeometry>
<PathFigure
x:Name="bezier1"
IsClosed="True">
<BezierSegment
x:Name="bezier2"
/>
<BezierSegment
x:Name="bezier3"
/>
<BezierSegment
x:Name="bezier4"
/>
<BezierSegment
x:Name="bezier5"
/>
</PathFigure>
<PathGeometry.Transform>
<TransformGroup>
<ScaleTransform
ScaleX="2"
ScaleY="2"
/>
<RotateTransform
Angle="45"
/>
<TranslateTransform
X="200"
Y="200"
/>
</TransformGroup>
</PathGeometry.Transform>
</PathGeometry>
</Path.Data>
<Path.Triggers>
<EventTrigger>
<BeginStoryboard>
<Storyboard
RepeatBehavior="Forever"
AutoReverse="True"
>
<PointAnimation
Storyboard.TargetName="bezier1"
Storyboard.TargetProperty="StartPoint"
From="0 100" To="0
125" />
<PointAnimation
Storyboard.TargetName="bezier2"
Storyboard.TargetProperty="Point1"
From="55 100" To="62.5
62.5" />
<PointAnimation
Storyboard.TargetName="bezier2"
Storyboard.TargetProperty="Point2"
From="100 55" To="62.5
62.5" />
<PointAnimation
Storyboard.TargetName="bezier2"
Storyboard.TargetProperty="Point3"
From="100 0" To="125
0" />
<PointAnimation
Storyboard.TargetName="bezier3"
Storyboard.TargetProperty="Point1"
From="100 -55"
To="62.5 -62.5" />
<PointAnimation
Storyboard.TargetName="bezier3"
Storyboard.TargetProperty="Point2"
From="55
-100" To="62.5
-62.5" />
<PointAnimation
Storyboard.TargetName="bezier3"
Storyboard.TargetProperty="Point3"
From="0
-100" To="0 -125"
/>
<PointAnimation
Storyboard.TargetName="bezier4"
Storyboard.TargetProperty="Point1"
From="-55
-100" To="-62.5
-62.5" />
<PointAnimation
Storyboard.TargetName="bezier4"
Storyboard.TargetProperty="Point2"
From="-100 -55"
To="-62.5 -62.5" />
<PointAnimation
Storyboard.TargetName="bezier4"
Storyboard.TargetProperty="Point3"
From="-100
0" To="-125 0" />
<PointAnimation
Storyboard.TargetName="bezier5"
Storyboard.TargetProperty="Point1"
From="-100 55"
To="-62.5 62.5" />
<PointAnimation
Storyboard.TargetName="bezier5"
Storyboard.TargetProperty="Point2"
From="-55
100" To="-62.5
62.5" />
<PointAnimation
Storyboard.TargetName="bezier5"
Storyboard.TargetProperty="Point3"
From="0
100" To="0 125"
/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Path.Triggers>
</Path>
</Grid>
Here's halfway between a square and a circle:
Splines and Key Frames
Three of the key-frame classes begin with the word Spline:SplineDoubleKeyFrame,SplinePointKeyFrame, and SplineColorKeyFrame. These classes have KeyTime and Value properties like the Discrete and Linear keyframes, but they also define a property named KeySpline. This property allows you to create a key frame that speeds up or slows down (or both) during its course but still ending at the correct value by the time KeyTimecomes around. The change in velocity is governed by a Bezier spline, Certainly the best way to get a feel for spline-based key frames is to experiment with them, and I have just the program. It's even called SplineKeyFrameExperiment:
You can move the control points of the spline using the blue semi-translucent
circles. The ApplicationBar has only
one button labeled "animate":
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton
IconUri="/Images/appbar.transport.play.rest.png"
Text="animate"
Click="OnAppbarAnimateButtonClick"
/>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
When you press it, the white ball on the bottom of the
grid moves linearly from left to right, representing the linear increase in
time. The white ball at the right of the grid moves nonlinearly from top to
bottom based on the shape of the spline.
For purposes of simplicity, the layout of the screen is
based on a grid with a fixed width and height of 400 pixels, so the program will
need to be modified a bit for a smaller screen.
Here are the two little white balls that appear on the
bottom and right, one representing time and the other representing the animated
object:
<Path
Fill="{StaticResource
PhoneForegroundBrush}">
<Path.Data>
<EllipseGeometry
x:Name="timeBall"
RadiusX="10"
RadiusY="10"
Center="0
400" />
</Path.Data>
</Path>
<Path
Fill="{StaticResource
PhoneForegroundBrush}">
<Path.Data>
<EllipseGeometry
x:Name="animaBall"
RadiusX="10"
RadiusY="10"
Center="400
0" />
</Path.Data>
</Path>
You can't see it when the program is inactive, but two
lines-one horizontal and one vertical-connect the small balls with the spline
curve. These lines track the spline curve when the small balls are moving:
<Line
x:Name="timeTrackLine"
Stroke="{StaticResource
PhoneBackgroundBrush}"
Y2="400"
/>
<Line
x:Name="animaTrackLine"
Stroke="{StaticResource
PhoneBackgroundBrush}"
X2="400"
/>
Finally, two semi-transparent circles respond to touch
input and are used to drag the control points within the grid:
<Path
Name="dragger1"
Fill="{StaticResource
PhoneAccentBrush}"
Opacity="0.5">
<Path.Data>
<EllipseGeometry
x:Name="dragger1Geometry"
RadiusX="50"
RadiusY="50"
Center="200
80" />
</Path.Data>
</Path>
<Path
Name="dragger2"
Fill="{StaticResource
PhoneAccentBrush}"
Opacity="0.5">
<Path.Data>
<EllipseGeometry
x:Name="dragger2Geometry"
RadiusX="50"
RadiusY="50"
Center="200
320" />
</Path.Data>
</Path>
The centers of these two
EllipseGeometry objects provide the two control
points of the KeySpline object. In the code-behind file, the constructor
initializes the TextBlock at the
bottom with the values, normalized to the range of 0 to 1:
public
partial class
MainPage :
PhoneApplicationPage
{
public MainPage()
{
InitializeComponent();
UpdateTextBlock();
}
void UpdateTextBlock()
{
txtblk.Text = String.Format("pt1
= {0:F2}\npt2 = {1:F2}",
NormalizePoint(dragger1Geometry.Center),
NormalizePoint(dragger2Geometry.Center));
}
Point NormalizePoint(Point
pt)
{
return new
Point(pt.X / 400, pt.Y / 400);
}
.....
}
When the button in the
ApplicationBar is
pressed, the program needs to set four different animations with identical
KeySpline
objects and then start the Storyboard
going:
void
OnAppbarAnimateButtonClick(object sender,
EventArgs args)
{
Point controlPoint1 =
NormalizePoint(dragger1Geometry.Center);
Point controlPoint2 =
NormalizePoint(dragger2Geometry.Center);
splineKeyFrame1.KeySpline = new
KeySpline();
splineKeyFrame1.KeySpline.ControlPoint1 = controlPoint1;
splineKeyFrame1.KeySpline.ControlPoint2 = controlPoint2;
splineKeyFrame2.KeySpline = new
KeySpline();
splineKeyFrame2.KeySpline.ControlPoint1 = controlPoint1;
splineKeyFrame2.KeySpline.ControlPoint2 = controlPoint2;
splineKeyFrame3.KeySpline = new
KeySpline();
splineKeyFrame3.KeySpline.ControlPoint1 = controlPoint1;
splineKeyFrame3.KeySpline.ControlPoint2 = controlPoint2;
splineKeyFrame4.KeySpline = new
KeySpline();
splineKeyFrame4.KeySpline.ControlPoint1 = controlPoint1;
splineKeyFrame4.KeySpline.ControlPoint2 = controlPoint2;
storyboard.Begin();
}
The storyboard is defined in the
Resources collection
of the page, try it out: If you set both control points to (1, 0) you get an
animation that starts off slow and then gets very fast. Setting both control
points to (0, 1) has the opposite effect. Set the first control point to (1, 0)
and the second to (0, 1) and you get ananimation that starts off slow, then gets
fast, and ends up slow. Switch them and get the opposite effect.
The Easing Functions
You might prefer something more "canned" that gives you an overall impression of adherence to physical law without requiring a lot of thought. This is the purpose of the animation easing functions. These are classes that derive from EasingFunctionBase with common types of transitions that you can add to the beginning or end (or both beginning and end) of your animations.DoubleAnimation,PointAnimation, and ColorAnimationall have properties named EasingFunction of type EasingFunctionBase. There are also EasingDoubleKeyFrame, EasingColorKeyFrame, and EasingPointKeyFrame classes.
EasingFunctionBase defines just one property: EasingMode of the enumeration type EasingMode, either EaseOut (the default, which uses the transition only at the end of the animation), EaseIn, or EaseInOut. Eleven classes derive from EasingFunctionBase and you can derive your own if you want to have even more control and power.
The project named TheEasingLife lets you choose among the eleven
EasingFunctionBase derivatives to see their effect on a simple PointAnimation involving a ball-like object. The
content area is populated with two Polyline elements and a Path but no
coordinates are supplied; that's done in code.
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<Polyline
Name="polyline1"
Stroke="{StaticResource
PhoneForegroundBrush}" />
<Polyline
Name="polyline2"
Stroke="{StaticResource
PhoneForegroundBrush}" />
<Path
Fill="{StaticResource
PhoneAccentBrush}">
<Path.Data>
<EllipseGeometry
x:Name="ballGeometry"
RadiusX="25"
RadiusY="25"
/>
</Path.Data>
</Path>
</Grid>
The coordinates for the two Polyline elements and EllipseGeometry are set
during the Loaded event handler based on the size of the content panel. The ball
is intended to be animated between a Polyline at the top and a Polyline at the
bottom; the actual points are stored in the ballPoints array. The direction
(going down or going up) is governed by the isForward
field.
public
partial class
MainPage : PhoneApplicationPage
{
PointCollection ballPoints =
new PointCollection();
bool isForward =
true;
public MainPage()
{
InitializeComponent();
Loaded += OnMainPageLoaded;
}
public
EasingFunctionBase EasingFunction { get;
set; }
void OnMainPageLoaded(object
sender, RoutedEventArgs args)
{
double left = 100;
double right =
ContentPanel.ActualWidth - 100;
double center =
ContentPanel.ActualWidth / 2;
double top = 100;
double bottom =
ContentPanel.ActualHeight - 100;
polyline1.Points.Add(new
Point(left, top));
polyline1.Points.Add(new
Point(right, top));
polyline2.Points.Add(new
Point(left, bottom));
polyline2.Points.Add(new
Point(right, bottom));
ballPoints.Add(new
Point(center, top));
ballPoints.Add(new
Point(center, bottom));
ballGeometry.Center = ballPoints[1 -
Convert.ToInt32(isForward)];
}
void
OnAppbarPlayButtonClick(object sender,
EventArgs args)
{
pointAnimation.From = ballPoints[1 -
Convert.ToInt32(isForward)];
pointAnimation.To = ballPoints[Convert.ToInt32(isForward)];
pointAnimation.EasingFunction = EasingFunction;
storyboard.Begin();
}
void OnStoryboardCompleted(object
sender, EventArgs args)
{
isForward ^= true;
}
void OnAppbarSettingsButtonClick(object
sender, EventArgs args)
{
NavigationService.Navigate(new
Uri("/EasingFunctionDialog.xaml",
UriKind.Relative));
}
protected
override void OnNavigatedFrom(NavigationEventArgs
args)
{
if (args.Content
is
EasingFunctionDialog)
{
(args.Content as
EasingFunctionDialog).EasingFunction =
EasingFunction;
}
base.OnNavigatedTo(args);
}
protected
override void OnNavigatedTo(NavigationEventArgs
args)
{
ApplicationTitle.Text = "THE EASING LIFE
- " +
(EasingFunction != null ?
EasingFunction.GetType().Name : "none");
base.OnNavigatedTo(args);
}
}
Keep in mind that these EasingFunctionBase derivatives have all default
property settings, including the EasingMode property that restricts the effect
only to the end of the animation. You'll find that a couple of these
effects-specifically BackEase and ElasticEase-actually overshoot the
destination. While this doesn't matter in many cases, for some properties it
might result in illegal values.