juin 25

This post will focus on animations. While Silverlight is a very capable technology for creating rich user interfaces, Windows Phone 7’s UI languages, codenamed Metro, has very strong red threads and principles which push typography, panoramic navigation and animation. One might find the experience a bit bland at first, but it really comes alive when one sees the UI in motion. A post dedicated to Metro is planned, but for now I suggest you read these nice articles.

If you base your design principles on Metro and try to embrace the design of WP7’s default applications, you will want to animate parts of your UI on actions or based on a timer.

 

Animations can be created in very different ways: some can be created through the Blend tool when the targets are well-defined and are static, some have to be handled through code when their targets are dynamically generated.
Let’s discuss the more complicated case, the dynamic generation of animations. We will go through Blend animations later on.

 

The goal

The goal of this tutorial is to animate items in a list randomly. Items are displayed as two-side tiles, which flip to display the “back” of the tile. This could be useful to display a list of photos for example, without having to overcrowd the UI with long titles and descriptions (i. e. secondary information). This information would be written on the back of the tile.

 

Here is a preview:

image

 

Items are animated randomly at a regular time interval. To affect visual objects with a timer, we will use the DispatcherTimer, simply because using a regular timer would cause us a headache when dealing with cross-thread invoking (don’t worry if you do not understand this, it is not required to read the article).

 

Designing the data

Let’s design the data we want to display on the tile:

   1: public class DataExampleInformation

   2: {

   3:     public string Title

   4:     {

   5:         get;

   6:         set;

   7:     }

   8:  

   9:     public string Description

  10:     {

  11:         get;

  12:         set;

  13:     }

  14: }

  15:  

  16: public class DataExample

  17: {

  18:     public ImageSource ImageSource

  19:     {

  20:         get;

  21:         set;

  22:     }

  23:  

  24:     public DataExampleInformation Description

  25:     {

  26:         get;

  27:         set;

  28:     }

  29: }

  30:  

 

These data classes are relatively straightforward. The DataExample class contains an image, the one to display on the front of the tile, and a reference to another object of type DataExampleInformation which contains both title and description, i.e. the data to be displayed on the back of the tile. The second class is purely artificial of course but it will show an applied example of deep data-binding.

When a list of items is bound to a listbox, the listbox will take each item of the list and display it according to its ItemTemplate. The ItemTemplate property on the list is of type DataTemplate. The default DataTemplate displays the data in a TextBlock whose text fields binds to the data’s string representation. Since we want to display our picture and data, we need to create our own DataTemplate.

 

Creating the usercontrol

Let’s create a new UserControl to host our work and call it AnimatedTilesUserControl. Create a listbox as the only element in this usercontrol, and let’s customize the way items will be displayed by creating a new DataTemplate.

   1: <DataTemplate x:Key="DataTemplateTileDataExampleItem">

   2:     <Grid x:Name="TileGrid" Height="150" Width="150" Margin="5">

   3:         <Grid x:Name="BackGrid">

   4:             <Grid.Background>

   5:                 <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">

   6:                     <GradientStop Color="#FFB64F4F" Offset="0"/>

   7:                     <GradientStop Color="#FFCE0F0F" Offset="1"/>

   8:                 </LinearGradientBrush>

   9:             </Grid.Background>

  10:             <Grid.RowDefinitions>

  11:                 <RowDefinition Height="Auto"/>

  12:                 <RowDefinition Height="*"/>

  13:             </Grid.RowDefinitions>

  14:             <TextBlock Grid.Row="0"

  15:                        TextWrapping="Wrap" d:LayoutOverrides="Width, Height"

  16:                        Text="{Binding Description.Title}"

  17:                        FontFamily="Segoe UI Semibold" FontSize="24" 

  18:                        HorizontalAlignment="Center"/>

  19:             <TextBlock Grid.Row="1"

  20:                        TextWrapping="Wrap" d:LayoutOverrides="Width, Height"

  21:                        Text="{Binding Description.Description}"

  22:                        FontFamily="Segoe UI Semibold" FontSize="18" 

  23:                        HorizontalAlignment="Stretch"/>                    

  24:         </Grid>

  25:         <Grid x:Name="FrontGrid">

  26:             <Grid.Background>

  27:                 <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">

  28:                     <GradientStop Color="#FF356D6E" Offset="0"/>

  29:                     <GradientStop Color="#FF00F9FF" Offset="1"/>

  30:                 </LinearGradientBrush>

  31:             </Grid.Background>

  32:             <Image Source="{Binding ImageSource}" />

  33:         </Grid>

  34:     </Grid>

  35: </DataTemplate>        

  36:  

 

This custom DataTemplate has two grids, one to display the front content, one to display the back content of the tile. Here, only the FrontGrid will be visible since it takes all the space available and is declared after the BackGrid. This is called the Z Order. The last items declared in XAML are always displayed front-most. The size of the items is absolute and set to width=150 and height=150 to mimic the square tiles of WP7 start experience. You could of course have other aspect ratios and sizes.

 

Now that we are done with this, we need to animate the items. To achieve a nice flip effect, we will use Silverlight’s projection feature. Every single UIElement can be displayed in 3 dimensions simply by changing 3 parameters: RotateX, RotateY and RotateZ which are 3 properties of the PlaneProjection class.

The obvious way to have a flip effect is to animate the RotationX property from 0 to 180 degrees. The RotationX value change will make the element rotate around the X axis.

image

Looking at the DataTemplate we’ve already created, and since we need to animate the whole tile, there are a few things  to be changed in the TileGrid.

   1: <DataTemplate x:Key="DataTemplateTileDataExampleItem">

   2:      <Grid x:Name="TileGrid" Height="150" Width="150" Margin="5">

   3:          <Grid.Projection>

   4:              <PlaneProjection RotationX="45"

   5:                               CenterOfRotationY="0.5"/>

   6:          </Grid.Projection>

   7:

   8:  

We declared a projection transformation on the TileGrid. The value (45 degrees) set to the RotationX property is quite random, just to show the way the whole element is projected, before actually taking care of animating the value to produce a smooth flip animation.

 

Play around with the CenterOfRotationY property to see how it affects the actual projection of the tile. For example, set it to 0 or 1 and see how it changes.

Since we want to animate each tile randomly every time our DispatcherTimer ticks, our data item must be wrapped in an element whose properties can be animated. Such a wrapper must then inherit from DependencyObject and declare the various properties that will be animated.

   1: public class AnimatedTile : DependencyObject

   2: {

   3:     public object Data { get; set; }

   4:  

   5:     public static DependencyProperty RotationXProperty = DependencyProperty.Register("RotationX", typeof(double), typeof(AnimatedTile), new PropertyMetadata(0.0));

   6:     public double RotationX

   7:     {

   8:         get { return (double)GetValue(RotationXProperty); }

   9:  

  10:         set { SetValue(RotationXProperty, value); }

  11:     }

  12:  

  13:     public double CenterOfRotationY { get; set; }

  14:  

  15:     public AnimatedTile()

  16:     {

  17:         this.CenterOfRotationY = 0.5;

  18:     }

  19:  

  20:     public AnimatedTile(object data)

  21:         : this()

  22:     {

  23:         this.Data = data;

  24:     }

  25: }

  26:  

 

This class declares a DependencyProperty, meaning the binding facility will be able to know when it gets updated automatically. It also features a Data property which will contain the actual business object, in our case an instance of the DataExample class.

Let’s update the DataTemplate to bind to these DependencyProperties:

   1: <DataTemplate x:Key="DataTemplateTileDataExampleItem">

   2:     <Grid x:Name="TileGrid" Height="150" Width="150" Margin="5">

   3:         <Grid.Projection>

   4:             <PlaneProjection RotationX="{Binding RotationX}"

   5:                              CenterOfRotationX="{Binding CenterOfRotationX}"/>

   6:         </Grid.Projection>

   7:         <Grid x:Name="BackGrid">

   8:             <Grid.Background>

   9:                 <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">

  10:                     <GradientStop Color="#FFB64F4F" Offset="0"/>

  11:                     <GradientStop Color="#FFCE0F0F" Offset="1"/>

  12:                 </LinearGradientBrush>

  13:             </Grid.Background>

  14:             <Grid.RowDefinitions>

  15:                 <RowDefinition Height="Auto"/>

  16:                 <RowDefinition Height="*"/>

  17:             </Grid.RowDefinitions>

  18:             <TextBlock Grid.Row="0"

  19:                        TextWrapping="Wrap" d:LayoutOverrides="Width, Height"

  20:                        Text="{Binding Data.Description.Title}"

  21:                        FontFamily="Segoe UI Semibold" FontSize="24" 

  22:                        HorizontalAlignment="Center"/>

  23:             <TextBlock Grid.Row="1"

  24:                        TextWrapping="Wrap" d:LayoutOverrides="Width, Height"

  25:                        Text="{Binding Data.Description.Description}"

  26:                        FontFamily="Segoe UI Semibold" FontSize="18" 

  27:                        HorizontalAlignment="Stretch"/>                    

  28:         </Grid>

  29:         <Grid x:Name="FrontGrid">

  30:             <Grid.Background>

  31:                 <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">

  32:                     <GradientStop Color="#FF356D6E" Offset="0"/>

  33:                     <GradientStop Color="#FF00F9FF" Offset="1"/>

  34:                 </LinearGradientBrush>

  35:             </Grid.Background>

  36:             <Image Source="{Binding Data.ImageSource}" />

  37:         </Grid>

  38:     </Grid>

  39: </DataTemplate>        

  40:  

You can notice here that the binding path to the business object has been slightly changed to reflect its embedding in the AnimatedTile wrapper.

 

Animating the tiles

First, declare a DispatcherTimer and register an event handler to its tick event.

   1: private Random rdm = new Random();

   2: private DispatcherTimer dispatcherTimer = new DispatcherTimer();

   3:  

   4: public AnimatedTilesUserControl()

   5: {

   6:     InitializeComponent();

   7:     this.Loaded += new RoutedEventHandler(AnimatedTilesUserControl_Loaded);

   8: }

   9:  

  10: void AnimatedTilesUserControl_Loaded(object sender, RoutedEventArgs e)

  11: {

  12:     // Binding a list of items to the listbox

  13:     this.ItemsListBox.ItemsSource = dataItems;

  14:  

  15:     // Setting up and starting the timer

  16:     dispatcherTimer.Interval = TimeSpan.FromSeconds(5);

  17:     dispatcherTimer.Tick += new System.EventHandler(dispatcherTimer_Tick);

  18:     dispatcherTimer.Start();

  19: }

  20:  

  21: private void dispatcherTimer_Tick(object sender, System.EventArgs e)

  22: {

  23:     // Animate the tiles here

  24: }

  25:  

This timer will help us execute an action every five seconds. The ItemsSource property of the ListBox is set to some very straightforward sample data similar to this:

   1: private List<AnimatedTile> dataItems = new List<AnimatedTile>()

   2: {

   3:     new AnimatedTile(new DataExample()

   4:     {

   5:         Description = new DataExampleInformation()

   6:         {

   7:             Description = "This is the description for icon 1",

   8:             Title = "Icon 1"

   9:         },

  10:         ImageSource = new BitmapImage(new Uri("/pics/appbar.1.png", UriKind.Relative))

  11:     }),

  12:     new AnimatedTile(new DataExample()

  13:     {

  14:         Description = new DataExampleInformation()

  15:         {

  16:             Description = "This is the description for icon 2",

  17:             Title = "Icon 2"

  18:             },

  19:         ImageSource = new BitmapImage(new Uri("/pics/appbar.2.png", UriKind.Relative))

  20:     })

  21: };

  22:  

  23:  

 

On every timer tick, let’s pick one random item and animate it.

   1: private void dispatcherTimer_Tick(object sender, System.EventArgs e)

   2: {

   3:     // Get the index of the tile to animate

   4:     int itemToAnimate = rdm.Next(this.dataItems.Count);

   5:  

   6:     // Get the tile/wrapper

   7:     AnimatedTile tileToAnimate = this.dataItems[itemToAnimate];

   8:  

   9:     // Animating the RotationX property with the default linear function

  10:     Storyboard sb = new Storyboard();

  11:     DoubleAnimation da = new DoubleAnimation();

  12:     da.From = tileToAnimate.RotationX;

  13:     da.To = tileToAnimate.RotationX + 180;

  14:     da.Duration = TimeSpan.FromSeconds(1);

  15:  

  16:     Storyboard.SetTarget(da, tileToAnimate);

  17:     Storyboard.SetTargetProperty(da, new PropertyPath(AnimatedTile.RotationXProperty));

  18:     sb.Children.Add(da);

  19:  

  20:     sb.Begin();

  21: }

  22:  

 

To animate an object, it is necessary to create a storyboard. Think of it as the higher level animation manager that can receive high-level commands such as Begin and Stop. Specific element animations can then be attached to it. In our case, we decided to animate the RotationX DependencyProperty of the AnimatedTile wrapper with a DoubleAnimation. The value of this property must be increased by 180 degrees for an actual flip. Setting the duration of the animation to 1 second will change the value of the property from the From value to the To value in a linear fashion. Advanced easing functions can also be specified, see http://msdn.microsoft.com/en-us/library/cc189019(VS.95).aspx#easing_functions .

 

image

Now if you watch this animation unfold, you will notice the tile is flipped correctly. However, once the animation is complete, you will see the back of the FrontGrid element, not even the back of the BackGrid. To remedy this, we will need to change the order of the displayed elements at the middle of the animation. The display order of the elements is handled through the ZIndex property. This property’s type being an int, a DoubleAnimation would not do. To change this property through the storyboard, we will thus use an ObjectAnimationWithKeyFrames.

The ObjectAnimationWithKeyFrames describes an animation through the use of KeyFrames. You set the value you want the property to have at the time you specify, it’s that simple. Here’s what we need to do to change the ZIndex.

 

Change the AnimatedTile wrapper to include a ZIndex DependencyProperty.

   1: public static DependencyProperty ZIndexProperty = DependencyProperty.Register("ZIndex", typeof(int), typeof(AnimatedTile), new PropertyMetadata(0));

   2:  

   3: public int ZIndex

   4: {

   5:     get { return (int)GetValue(ZIndexProperty); }

   6:     set { SetValue(ZIndexProperty, value); }

   7: }

 

Change the DataTemplate and bind the BackGrid’s ZIndex to the AnimatedTile’s ZIndex DependencyProperty.

   1: <DataTemplate x:Key="DataTemplateTileDataExampleItem">

   2:     <Grid x:Name="TileGrid" Height="150" Width="150" Margin="5">

   3:         <Grid.Projection>

   4:             <PlaneProjection RotationX="{Binding RotationX}"

   5:                              CenterOfRotationX="{Binding CenterOfRotationX}"/>

   6:         </Grid.Projection>

   7:         <Grid x:Name="BackGrid"

   8:               Canvas.ZIndex="{Binding ZIndex}">

   9:

 

Finally, let’s add the animation of that ZIndex value in the timer tick event.

   1: private void dispatcherTimer_Tick(object sender, System.EventArgs e)

   2: {

   3:     // Get the index of the tile to animate

   4:     int itemToAnimate = rdm.Next(this.dataItems.Count);

   5:  

   6:     // Get the tile/wrapper

   7:     AnimatedTile tileToAnimate = this.dataItems[itemToAnimate];

   8:  

   9:     // Animating the RotationX property with the default linear function

  10:     Storyboard sb = new Storyboard();

  11:     DoubleAnimation da = new DoubleAnimation();

  12:     da.From = tileToAnimate.RotationX;

  13:     da.To = tileToAnimate.RotationX + 180;

  14:     da.Duration = TimeSpan.FromSeconds(1);

  15:  

  16:     Storyboard.SetTarget(da, tileToAnimate);

  17:     Storyboard.SetTargetProperty(da, new PropertyPath(AnimatedTile.RotationXProperty));

  18:     sb.Children.Add(da);

  19:  

  20:     // Animating the ZIndex property to play on the display element

  21:     ObjectAnimationUsingKeyFrames oa = new ObjectAnimationUsingKeyFrames();

  22:     DiscreteObjectKeyFrame deokf = new DiscreteObjectKeyFrame();

  23:     deokf.KeyTime = TimeSpan.FromSeconds(0);

  24:     deokf.Value = tileToAnimate.ZIndex;

  25:     oa.KeyFrames.Add(deokf);

  26:  

  27:     // In the middle of rotation, change ZIndex to 1 to show back of tile

  28:     deokf = new DiscreteObjectKeyFrame();

  29:     deokf.KeyTime = TimeSpan.FromSeconds(0.5);

  30:     deokf.Value = (tileToAnimate.ZIndex == 0) ? 1 : 0;

  31:     oa.KeyFrames.Add(deokf);

  32:  

  33:     Storyboard.SetTarget(oa, tileToAnimate);

  34:     Storyboard.SetTargetProperty(oa, new PropertyPath(AnimatedTile.ZIndexProperty));

  35:     sb.Children.Add(oa);

  36:  

  37:     sb.Begin();

  38: }

 

To recap the animation:

  • start to flip at t=0s from 0 degrees
  • set ZIndex of BackGrid to 0 at t=0s
  • set ZIndex of BackGrid to 1 at t=0.5s; at that time, the TileGrid’s RotationX DependencyProperty’s value is 90 degrees
  • at 1s, ZIndex of BackGrid is 1 and TileGrid’s RotationX DependencyProperty’s value is 180 degrees

You will notice now that the back of the tile is shown correctly once the animation has reached the second half. However, the content of the back part is flipped! It makes sense when you think about it. To correct this, simply specify a transformation:

   1: <Grid x:Name="BackGrid"

   2:       Canvas.ZIndex="{Binding ZIndex}"

   3:       RenderTransformOrigin="0.5,0.5">

   4:     <Grid.RenderTransform>

   5:         <CompositeTransform ScaleY="-1"/>

   6:     </Grid.RenderTransform>

   7:     <Grid.Background>

   8:

   9:  

 

When specifying the origin of the transformation and the value of the ScaleY DependencyProperty to -1, it will flip the content of the BackGrid and display it as it should.

image

The center of the transform is important as well: you have to specify coordinates the middle of the element to achieve the expected effect, so (0.5, 0.5).

Post comments if you want more details, I intend to share the source code for it in a little while but you should have all the information you need to build your own project.

Partager :
  • Print
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • LinkedIn
  • Twitter

3 Responses to “Windows Phone 7 (2 of n): Animation Basics”

  1. [...] This post was mentioned on Twitter by Larry King, Larry King. Larry King said: Windows Phone 7 (2 of n): Animation Basics http://bit.ly/aJsY36 #SL #RIA [...]

  2. [...] (2 of n) – Animation Basics [...]

  3. Tim dit :

    Thanks for the sharing your tutorial. I’m really struggling to get this working on my project. Any idea when the source will be available? Thanks again for sharing.

Leave a Reply

preload preload preload