This chapter
is taken from book "Programming Windows Phone 7" by Charles Petzold published by
Microsoft press.
http://www.charlespetzold.com/phone/index.html
Silverlight 3 introduced a new UIElement property named Projection that allows setting non-affine transforms on graphical objects, text, controls, and media. Non-affine transforms do not preserve parallelism, the type of non-affine transform allowed in Silverlight 3 is still represented by a matrix multiplication, and it still has restrictions on what it can do. Straight lines are always transformed to straight lines, and a square is always transformed into a simple convex quadrilateral. By "quadrilateral" I mean a four-sided figure (also called a tetragon or quadrangle); by "simple" I mean that the sides don't intersect except at their vertices; by "convex" I mean that the internal angles at each vertex are less than 180 degrees.
This type of non-affine transform is very useful for creating taper transforms, where opposite sides of a square or rectangle taper somewhat in one direction. Objects appear to be somewhat three dimensional because part of the object seems further away from our eyes- an effect called a perspective projection.
In a sense, the Projection property gives Silverlight a little bit of "pseudo 3D." It's not a real 3D system because there's no way to define objects in 3D space, no concept of cameras, lights, or shading, and-perhaps most crucially-no clipping of objects based on their arrangement in 3D space.
Still, working with the Projection transform requires the programmer to begin thinking about three dimensions and especially about 3D rotation. Fortunately, the developers of Silverlight have made common and simple use of the Projection property fairly easy.
You can set this Projection property to one of two objects: You can be mathematical and flexible by using Matrix3DProjection, or you can do as I'll do here and take the easy way out with PlaneProjection. Although PlaneProjection defines twelve settable properties, you can pretty much limit yourself to six of them.
The three crucial properties of PlaneProjection are RotationX,RotationY, and RotationX, which you can set to angle values to cause rotation around the X axis (which extends in a positive direction from left to right), the Y axis (which extends from top to bottom), and the Z axis (which comes out of the screen towards the viewer).
You can anticipate the direction of rotation using the right-hand rule: Point
your thumb in the direction of the positive axis. (For X, that's to the right,
for Y it's down, for Z, it's toward you.) The curve that your other fingers make indicates the direction of rotation
for positive rotation angles. Negative angles rotate in the opposite direction.
A composite rotation depends on the order in which the individual rotations
are applied. When you use PlaneProjection, you are sacrificing some flexibility in these
rotations. PlaneProjection
always applies
RotationX
first, then RotationY,
and finally RotationZ,
but in many cases you only need set one of these properties. As with
RenderTransform,
Projection doesn't
affect layout. The layout system always sees an untransformed and unprojected
element.
RotationX, RotationY,
and RotationZ are all backed by dependency properties, so they can all be
animation targets, as demonstrated by the PerspectiveRotation program. The
content area contains a
TextBlock with a PlaneProjection object set to its Projection property, and three buttons:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<Grid.RowDefinitions>
<RowDefinition
Height="*"
/>
<RowDefinition
Height="Auto"
/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition
Width="*"
/>
<ColumnDefinition
Width="*"
/>
<ColumnDefinition
Width="*"
/>
</Grid.ColumnDefinitions>
<TextBlock
Name="txtblk"
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="3"
Text="ROTATE"
FontSize="{StaticResource
PhoneFontSizeHuge}"
Foreground="{StaticResource
PhoneAccentBrush}"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock.Projection>
<PlaneProjection
x:Name="planeProjection"
/>
</TextBlock.Projection>
</TextBlock>
<Button
Grid.Row="1"
Grid.Column="0"
Content="Rotate
X"
Click="RotateXClick"
/>
<Button
Grid.Row="1"
Grid.Column="1"
Content="Rotate
Y"
Click="RotateYClick"
/>
<Button
Grid.Row="1"
Grid.Column="2"
Content="Rotate
Z"
Click="RotateZClick"
/>
</Grid>
Three storyboards defined in the Resources collection are defined to animate
the RotationX, RotationY, and RotationZ properties of the PlaneProjection
object:
<phone:PhoneApplicationPage.Resources>
<Storyboard
x:Name="rotateX">
<DoubleAnimation
Storyboard.TargetName="planeProjection"
Storyboard.TargetProperty="RotationX"
From="0"
To="360" Duration="0:0:5"
/>
</Storyboard>
<Storyboard
x:Name="rotateY">
<DoubleAnimation
Storyboard.TargetName="planeProjection"
Storyboard.TargetProperty="RotationY"
From="0"
To="360" Duration="0:0:5"
/>
</Storyboard>
<Storyboard
x:Name="rotateZ">
<DoubleAnimation
Storyboard.TargetName="planeProjection"
Storyboard.TargetProperty="RotationZ"
From="0"
To="360" Duration="0:0:5"
/>
</Storyboard>
</phone:PhoneApplicationPage.Resources>
The buttons simply start the corresponding storyboards:
void
RotateXClick(object sender,
RoutedEventArgs args)
{
rotateX.Begin();
}
void RotateYClick(object
sender, RoutedEventArgs args)
{
rotateY.Begin();
}
void RotateZClick(object
sender, RoutedEventArgs args)
{
rotateZ.Begin();
}
Here's rotation around the Y axis:
The animations are slow enough that you can click multiple buttons and see
the interactions. It almost looks as if the text is tumbling through the
weightlessness of space.
In 2D space, rotation is relative to a point; in 3D space, rotation is
relative to a line, commonly referred to as an "axis of rotation." But the
PlaneProjection class prefers to treat this center of rotation using three
numbers-the properties CenterOfRotationX, CenterOfRotationY, and
CenterOfRotationZ. In effect, these three numbers define a 3D point that remains
unchanged during rotation. CenterOfRotationX
does not affect rotation around the X axis, and similarly
for the other two properties.
The
CenterOfRotationX and CenterOfRotationY
properties are relative coordinates based on the size of
the element being rotated, where (0, 0) is the upper-left corner. The default
values are 0.5, indicating the center of the element.
If you set
CenterOfRotationX to 0, the RotationY property causes the element to rotate
around its left side. If CenterOfRotationY is set to 1, then the RotationX
property causes the element to be
rotated around its bottom.
You can use animated projection transforms for small
effects or for big effects. An example of a big effect is to change the way a
new page in your program comes into view. The SweepIntoView program has a
MainPage.xaml file containing just a little text:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<TextBlock
Text="Touch
to go to second page"
HorizontalAlignment="Center"
VerticalAlignment="Center"
/>
</Grid>
The code-behind file uses touch to navigate to Page2.xaml:
protected
override void
OnManipulationStarted(ManipulationStartedEventArgs
args)
{
this.NavigationService.Navigate(new
Uri("/Page2.xaml",
UriKind.Relative));
args.Complete();
args.Handled = true;
base.OnManipulationStarted(args);
}
For some variety (and to see more clearly what's happening) Page2.xaml colors
its content area with an accented background:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0"
Background="{StaticResource
PhoneAccentBrush}">
<TextBlock
Text="Touch
to go back"
HorizontalAlignment="Center"
VerticalAlignment="Center"
/>
</Grid>
The code-behind file also has an OnManipulationStarted override:
protected
override void
OnManipulationStarted(ManipulationStartedEventArgs
args)
{
this.NavigationService.GoBack();
args.Complete();
args.Handled = true;
base.OnManipulationStarted(args);
}
But what makes this program different is some additional markup in the
Page2.xaml file. This ensures that the page just doesn't come on the stage in a
sudden pop, but dramatically sweeps into view:
<phone:PhoneApplicationPage.Projection>
<PlaneProjection
x:Name="planeProjection"
CenterOfRotationX="0"
/>
</phone:PhoneApplicationPage.Projection>
<phone:PhoneApplicationPage.Triggers>
<EventTrigger>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="planeProjection"
Storyboard.TargetProperty="RotationY"
From="-90"
To="0" Duration="0:0:01"
/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</phone:PhoneApplicationPage.Triggers>
The PlaneProjection is set to the Projection property of the whole
PhoneApplicationPage element, and the animation is triggered when the page is
first loaded. The animation makes the RotationY property go from -90 degrees to
zero, with a CenterOfRotationX equal
to zero. This causes the page to sweep in almost like a door:
Animations and Property Precedence
The sample code for this article includes a little program called ButtonSetAndAnimate that doesn't do anything particularly useful except to illustrate how animation fits into dependency property precedence.
The XAML file contains a
Slider with a range of 0 to 100, a TextBlock showing the
Slider value, and four buttons:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<Grid.RowDefinitions>
<RowDefinition
Height="Auto"
/>
<RowDefinition
Height="*"
/>
<RowDefinition
Height="Auto"
/>
<RowDefinition
Height="Auto"
/>
<RowDefinition
Height="Auto"
/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition
Width="*"
/>
<ColumnDefinition
Width="*"
/>
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
Text="{Binding
ElementName=slider,
Path=Value}"
HorizontalAlignment="Center"
Margin="24"
/>
<Slider
Name="slider"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
Minimum="0"
Maximum="100"
Orientation="Horizontal"
VerticalAlignment="Center"
/>
<Button
Grid.Row="2"
Grid.Column="0"
Content="Set
to 0"
Click="OnSetToZeroClick"
/>
<Button
Grid.Row="2"
Grid.Column="1"
Content="Set
to 100"
Click="OnSetToOneHundredClick"
/>
<Button
Grid.Row="3"
Grid.Column="0"
Grid.ColumnSpan="2"
Content="Animate
to 50"
HorizontalAlignment="Center"
Click="OnAnimateTo50Click"
/>
<Button
Grid.Row="4"
Grid.Column="0"
Grid.ColumnSpan="2"
Content="Set
Maximum to 25"
HorizontalAlignment="Center"
Click="OnSetMaxTo40Click"
/>
</Grid>
Also in the XAML file is an animation that targets the Value property of the
Slider.
<phone:PhoneApplicationPage.Resources>
<Storyboard
x:Name="storyboard">
<DoubleAnimation
Storyboard.TargetName="slider"
Storyboard.TargetProperty="Value"
To="50"
Duration="0:0:5" />
</Storyboard>
</phone:PhoneApplicationPage.Resources>
Handlers for the four buttons are in the code-behind
file:
public
partial class
MainPage :
PhoneApplicationPage
{
public MainPage()
{
InitializeComponent();
}
void OnSetToZeroClick(object
sender, RoutedEventArgs args)
{
slider.Value = 0;
}
void OnSetToOneHundredClick(object
sender, RoutedEventArgs args)
{
slider.Value = 100;
}
void OnAnimateTo50Click(object
sender, RoutedEventArgs args)
{
storyboard.Begin();
}
void OnSetMaxTo40Click(object
sender, RoutedEventArgs e)
{
slider.Maximum = 25;
}
}
Here's the program:
You can manipulate the Slider with your finger and you can also use the
topmost two buttons to set the Slider value to its minimum or maximum. So far,
so good. Now click the "Animate to 50" button.