Colin Eberhardt's Adventures in .NET

Metro In Motion Part #2 – ‘Peel’ Animations

March 27th, 2011

This blog post is part #2 of my Metro In Motion series. In this post I demonstrate how to implement the animated ‘peel’ effect seen when native Windows Phone 7 applications exit.

In my previous blog post I discussed how the Metro Design Language that heavily influences the Windows Phone 7 style is not just about static graphics, it is also about fluid transitions. In that post I demonstrated a technique for making items within lists slide gracefully as the user moves between pivot pages. The post was pretty popular, so I have decided to turn it into a series, looking at how to implement the various fluid animations that are present in Windows Phone 7 native applications.

A feature found in most native Windows Phone 7 applications is the ‘peel’ effect where when the application is exited, the various components peel away from the top of the screen to the bottom. You can see my implementation of this effect in action in the video below. You might have to watch it a few times, the animation comes right at the end as is over pretty quickly!

The implementation of this effect has to be pretty generic – each application user interface is different, and for the peel effect to be useable, it cannot be tightly coupled to a specific UI layout.

The approach I have come up with is to define an extension method that can be applied to a list of FrameworkElements. This method creates a suitable Storyboard for each element and fires them in order. An Action is invoked when the last animation completes (more on that later!):

/// <summary>
/// Animates each element in order, creating a 'peel' effect. The supplied action
/// is invoked when the animation ends.
/// </summary>
public static void Peel(this IEnumerable<FrameworkElement> elements, Action endAction)
{
  var elementList = elements.ToList();
  var lastElement = elementList.Last();
 
  // iterate over all the elements, animating each of them
  double delay = 0;
  foreach (FrameworkElement element in elementList)
  {
    var sb = GetPeelAnimation(element, delay);
 
    // add a Completed event handler to the last element
    if (element.Equals(lastElement))
    {
      sb.Completed += (s, e) =>
        {
          endAction();
        };
    }
 
    sb.Begin();
    delay += 50;
  }
}

The peel animation itself works by associating a PlaneProjection with each element and animating its rotation and offset properties:

/// <summary>
/// Creates a PlaneProjection and associates it with the given element, returning
/// a Storyboard which will animate the PlaneProjection to 'peel' the item
/// from the screen.
/// </summary>
private static Storyboard GetPeelAnimation(FrameworkElement element, double delay)
{
  Storyboard sb;
 
  var projection = new PlaneProjection()
  {
    CenterOfRotationX = -0.1
  };
  element.Projection = projection;
 
  // compute the angle of rotation required to make this element appear
  // at a 90 degree angle at the edge of the screen.
  var width = element.ActualWidth;
  var targetAngle = Math.Atan(1000 / (width / 2));
  targetAngle = targetAngle * 180 / Math.PI;
 
  // animate the projection
  sb = new Storyboard();
  sb.BeginTime = TimeSpan.FromMilliseconds(delay);      
  sb.Children.Add(CreateAnimation(0, -(180 - targetAngle), 0.3, "RotationY", projection));
  sb.Children.Add(CreateAnimation(0, 23, 0.3, "RotationZ", projection));
  sb.Children.Add(CreateAnimation(0, -23, 0.3, "GlobalOffsetZ", projection));      
  return sb;
}
 
private static DoubleAnimation CreateAnimation(double from, double to, double duration,
  string targetProperty, DependencyObject target)
{
  var db = new DoubleAnimation();
  db.To = to;
  db.From = from;
  db.EasingFunction = new SineEase();
  db.Duration = TimeSpan.FromSeconds(duration);
  Storyboard.SetTarget(db, target);
  Storyboard.SetTargetProperty(db, new PropertyPath(targetProperty));
  return db;
}

Thanks to the genius Charles Petzold for explaining to me how PlaneProjections work! rotating an element around any point other than its centre is not quite as easy as it might seem!

As an aside, I would love to come up with an animation that more closely matches the native applications. I think what I have come up with is close, but noticeably different. If anyone is up for a challenge, please have a go at tweaking this animation and let me know what you come up with!

The above code gives us all we need in order to peel our UI, however, we now need to work out how to apply this in practice. Using the application developed in the previous post, we can see that the UI is composed of three discrete parts, the title, the pivot header and the list which resides within the currently visible pivot-item:

We’ll start with the trickiest part -the list. In my previous blog post I showed how it was possible to enumerate all the items which are currently visible within a ListBox (or ItemsControl). To make this code more re-useable I have refactored it as an extension method on ItemsControl:

/// <summary>
/// Enumerates all the items that are currently visible in am ItemsControl.
/// This implementation assumes that a VirtualizingStackPanel is
/// being used as the ItemsPanel.
/// </summary>
public static IEnumerable<FrameworkElement> GetItemsInView(this ItemsControl itemsControl)
{
    // locate the stack panel that hosts the items
  VirtualizingStackPanel vsp = itemsControl.Descendants<VirtualizingStackPanel>()
                                           .First() as VirtualizingStackPanel;
 
  // iterate over each of the items in view
  int firstVisibleItem = (int)vsp.VerticalOffset;
  int visibleItemCount = (int)vsp.ViewportHeight;
  for (int index = firstVisibleItem; index <= firstVisibleItem + visibleItemCount + 1; index++)
  {
    var item = itemsControl.ItemContainerGenerator.ContainerFromIndex(index) as FrameworkElement;
    if (item == null)
      continue;
 
    yield return item;
  }
}

When the back button is pressed, we can locate the list which is located in the visible pivot-item, using Linq-to-VisualTree, and use the extension method above to extract its visible items:

var listInView = ((PivotItem)pivot.SelectedItem).Descendants().OfType<ItemsControl>().Single();
var listItems = listInView.GetItemsInView().ToList();

By inspecting the Pivot control’s template, we can see that the header is named “HeadersListElement”, making it easy to locate using Linq again:

var header = this.Descendants().OfType<FrameworkElement>()
                  .Single(d => d.Name == "HeadersListElement");

Finally, the small text which indicates the name of the application is a named element in the XAML markup, so we already have a reference to this one. Putting all the above together, with a simple Linq Union, gives the following:

protected override void OnBackKeyPress(System.ComponentModel.CancelEventArgs e)
{
  e.Cancel = true;
 
  // obtain the list from the current pivot item - and the items currently visible in this list
  var listInView = ((PivotItem)pivot.SelectedItem).Descendants().OfType<ItemsControl>().Single();
  var listItems = listInView.GetItemsInView().ToList();
 
  // locate the pivot control  header
  var header = this.Descendants().OfType<FrameworkElement>()
              .Single(d => d.Name == "HeadersListElement");
 
  // create the list of items to peel
  var peelList = new FrameworkElement[] { TitlePanel, header }.Union(listItems);
 
  peelList.Peel(() =>
    {
      App.Quit();
    });
 
  base.OnBackKeyPress(e);
}

The final twist is that when the back ‘key’ is pressed the application exits immediately. To avoid this, we cancel the event. However, we need a way to exit the application when the animation has finished. Unfortunately Silverlight for WP7 does not have a mechanism for programatically exiting! This has been the subject of much debate. I opted for the popular throw-an-unhandled-exception route. Yes it is ugly, no, I do not like it, but let’s not get distracted. The ‘peel’ effect is now done!

To use this code in your own application, you will have to assemble your own list of elements based on your UI, however, this does give a lot of flexibility.

You can download the project sourcecode here: MetroInMotion2.zip

Regards,
Colin E.

Metro In Motion Part #1 – Fluid List Animation

March 22nd, 2011

This blog post presents an attached behaviour that gracefully slides the contents of a list into view when used in conjunction with a Pivot control, emulating the Windows Phone 7 email application.

The Windows Phone 7 user interface is based on the Metro Design Language, which favours clear typography, content over chrome and simplicity. If you want to see practical applications of the Metro approach to design, I would highly recommend visiting Scott Barnes’ blog over at riagenic.

Silverlight for Windows Phone 7 provides a basic Metro styling for elements such as Buttons and Checkboxes, it also has a few phone specific controls such as Pivot and Panorama. These controls make it easy to create a basic Metro interface, although again, I refer you to Scott Barnes’ blog, Metro is not all about black and white! However, when using a WP7 phone you will probably notice that the native applications, email, maps and settings, have a bit more ‘flair’, lists gracefully slide into view, or ‘peel’ off the screen when an item is selected. Metro is not just about static style, it is “alive in motion”.

The code in this blog post replicates the graceful slide effect seen in WP7 native applications when a moves from one list to another within a pivot as seen below. The code has been tested on real phone hardware to ensure that it performs well.

To use this code set the attached property ListAnimation.IsPivotAnimated to true for the ListBox (or ItemsControl) contained within a PivotItem. Then apply the ListAnimation.AnimationLevel to any element which you wish to animate as the list slides into view. The animation level describes the delay before each element is animated, for example, the markup below causes the summary to slide in just after the title, with the date following behind.

<controls:PivotItem Header="BBC News">
  <!-- animating an ListBox -->
  <ListBox x:Name="bbcNews"
        local:ListAnimation.IsPivotAnimated="True">
    <ListBox.ItemTemplate>
      <DataTemplate>
        <StackPanel Orientation="Vertical">
          <TextBlock Text="{Binding Title}"
                  Style="{StaticResource PhoneTextLargeStyle}"/>
          <TextBlock Text="{Binding Summary}"
                  Style="{StaticResource PhoneTextSmallStyle}"
                  local:ListAnimation.AnimationLevel="1"/>
          <TextBlock Text="{Binding Date}"
                  Style="{StaticResource PhoneTextSmallStyle}"
                  local:ListAnimation.AnimationLevel="2"/>
        </StackPanel>
      </DataTemplate>
    </ListBox.ItemTemplate>
  </ListBox>
</controls:PivotItem>

The code that achieves this effect is relatively straightforward, so I am going to present it all in one go (omitting all the usual attached property boiler-plate code):

// handles changes in the IsPivotAnimated attached property
private static void OnIsPivotAnimatedChanged(DependencyObject d,
                                                  DependencyPropertyChangedEventArgs args)
{
  ItemsControl list = d as ItemsControl;
 
  list.Loaded += (s2, e2) =>
    {
      // locate the pivot control that this list is within
      Pivot pivot = list.Ancestors<Pivot>().Single() as Pivot;
 
      // and its index within the pivot
      int pivotIndex = pivot.Items.IndexOf(list.Ancestors<PivotItem>().Single());
 
      bool selectionChanged = false;
 
      pivot.SelectionChanged += (s3, e3) =>
        {
          selectionChanged = true;
        };
 
      // handle manipulation events which occur when the user
      // moves between pivot items
      pivot.ManipulationCompleted += (s, e) =>
        {
          if (!selectionChanged)
            return;
 
          selectionChanged = false;
 
          if (pivotIndex != pivot.SelectedIndex)
            return;
 
          // determine which direction this tab will be scrolling in from
          bool fromRight = e.TotalManipulation.Translation.X <= 0;
 
          // locate the stack panel that hosts the items
          VirtualizingStackPanel vsp = list.Descendants<VirtualizingStackPanel>().First()
                                                      as VirtualizingStackPanel;
 
          // iterate over each of the items in view
          int firstVisibleItem = (int)vsp.VerticalOffset;
          int visibleItemCount = (int)vsp.ViewportHeight;
          for (int index = firstVisibleItem; index <= firstVisibleItem + visibleItemCount; index++)
          {
            // find all the item that have the AnimationLevel attached property set
            var lbi = list.ItemContainerGenerator.ContainerFromIndex(index);
            if (lbi == null)
              continue;
 
            vsp.Dispatcher.BeginInvoke(() =>
              {
                var animationTargets = lbi.Descendants()
                                       .Where(p => ListAnimation.GetAnimationLevel(p) > -1);
                foreach (FrameworkElement target in animationTargets)
                {
                  // trigger the required animation
                  GetAnimation(target, fromRight).Begin();
                }
              });
          };
        };
    };
}

When the IsPivotAnimated property is first attached, Linq-to-VisualTree is used to locate the parent PivotControl in order to handle SelectionChanged events. However, this is where things get tricky! If a Pivot control contains just two PivotItems, a change in selection is not enough to determine whether the pivot is scrolling to the left or the right! Therefore, we need to handle the ManipulationCompleted event that is fired after the SelectionChanged event to determine the direction of movement.

Once this is done, we can iterate over all of the items in the list, this assumes that the items are being hosted within a VirtualizingStackPanel which is true for a ListBox. For each item that is visible, another Linq query is used to find any that have the AnimationLevel attached property set on them. For each element the animation is created an fired.

Dispatcher.BeginInvoke is used to start each group of animations in order to lessen the impact of starting 10-20 animations simultaneously. Without the use of the Dispatcher, when testing on real hardware there was a small, but noticeable, judder in the sideways scrolling of the Pivot control at the point where the animations were fired. The use of Dispatcher.BeginInvoke means that the construction and firing of the animations are now packaged as separate ‘tasks’ for each element in the list. This means that they do not have to be executed as a single unit of work, allowing the phone to fire a few animations, then perform other tasks. The net result is that the Pivot control still scrolls smoothly between the PivotItems.

The code which creates the required animation is given below, it simply adds a TranslateTransform to the element and creates the required animation / storyboard.

/// <summary>
/// Creates a TranslateTransform and associates it with the given element, returning
/// a Storyboard which will animate the TranslateTransform with a SineEase function
/// </summary>
private static Storyboard  GetAnimation(FrameworkElement element, bool fromRight)
{
  double from = fromRight ? 80 : -80;
 
  Storyboard sb;
  double delay = (ListAnimation.GetAnimationLevel(element)) * 0.1 + 0.1;
 
  TranslateTransform trans = new TranslateTransform() { X = from };
  element.RenderTransform = trans;
 
  sb = new Storyboard();
  sb.BeginTime = TimeSpan.FromSeconds(delay);
 
  DoubleAnimation db = new DoubleAnimation();
  db.To = 0;
  db.From = from;
  db.EasingFunction = new SineEase();
  sb.Duration = db.Duration = TimeSpan.FromSeconds(0.8);
  sb.Children.Add(db);
  Storyboard.SetTarget(db, trans);
  Storyboard.SetTargetProperty(db, new PropertyPath("X"));
 
  return sb;
}

Interestingly I tried using the Artefact Animator, which has a nice concise API for creating animations in code-behind. However, because it animates elements by setting properties directly, it does not perform well on Windows Phone 7, which can execute storyboards on the composition thread in order to improve performance.

You can download the full sourcecode here: ListAnimation.zip

Regards,
Colin E.

A Silverlight Resizable TextBlock (and other resizable things)

March 14th, 2011

In this blog post I present a simple attached behaviour that uses a Thumb control within a Popup to adorn any UI element so that the user can re-size it.

A simple feature that has become quite popular on the web is to attache a small handle to text areas so that the user can resize them, this is useful if a user wants to add a large piece of text to a small comment form for example. Interestingly the Google Chrome browser makes all text areas resizeable by default, which leads to web developers wondering how to turn this feature off for their website. I thought that this was a pretty useful feature, so decided to implement a little attached behaviour that would do the same think for Silverlight:

You can make any element resizable by setting the following attached property:

<TextBox Text="This is a re-sizeable textbox"
          local:ResizeHandle.Attach="True">
</TextBox>

The implementation of this behaviour is pretty straightforward. When the Attach property changes, i.e. when it is set on an element a Thumb and a Popup are created:

private static Dictionary<FrameworkElement, Popup> _popups =
    new Dictionary<FrameworkElement, Popup>();
 
private static void OnAttachedPropertyChanged(DependencyObject d,
  DependencyPropertyChangedEventArgs e)
{
  FrameworkElement element = d as FrameworkElement;
 
  // Create a popup.
  var popup = new Popup();
 
  // and associate it with the element that can be re-sized
  _popups[element] = popup;
 
  // Add a thumb to the pop-up
  Thumb thumb = new Thumb()
  {
    Style = Application.Current.Resources["MyThumbStyle"] as Style
  };
  popup.Child = thumb;
 
  // add a relationship from the thumb to the target
  thumb.Tag = element;
 
  popup.IsOpen = true;
 
  thumb.DragDelta += new DragDeltaEventHandler(Thumb_DragDelta);
  element.SizeChanged += new SizeChangedEventHandler(Element_SizeChanged);
}

A static dictionary is used to relate the elements which have this property set o their respective Popup. The Thumb is styled via a Style which is looked up from the application resources. The following style re-templates the Thumb, replacing its visuals with an image:

<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
             x:Class="ResizableTextBox.App">
  <Application.Resources>
 
    <Style TargetType="Thumb" x:Key="MyThumbStyle">
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="Thumb">
            <Image Source="handle.png"
                   Margin="2"
                   Opacity="0.6"
                   Cursor="SizeNWSE"/>
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>
 
  </Application.Resources>
</Application>

Whenever the element changes size we need to reposition the Popup so that the thumb always remains on the bottom-right edge of the element:

/// <summary>
/// Handle size changes repositioning the thumb / popup
/// </summary>
private static void Element_SizeChanged(object sender, SizeChangedEventArgs e)
{
  FrameworkElement element = sender as FrameworkElement;
 
  // find the popup that hosts the thumb
  var popup = _popups[element];
 
  // reposition the popup
  FrameworkElement root = Application.Current.RootVisual as FrameworkElement;
  Point elementLocation = GetRelativePosition(element, root);
 
  var childElement = popup.Child as FrameworkElement;
 
  // Set where the popup will show up on the screen.
  popup.VerticalOffset = elementLocation.Y + element.ActualHeight - childElement.ActualHeight;
  popup.HorizontalOffset = elementLocation.X + element.ActualWidth - childElement.ActualWidth;
 
}
 
public static Point GetRelativePosition(UIElement element, UIElement otherElement)
{
  return element.TransformToVisual(otherElement)
                .Transform(new Point(0, 0));
}

And finally, when the thumb is dragged, we resize the element to which it is associated:

/// <summary>
/// Handles drag events from the thumb, resizing the associated element
/// </summary>
private static void Thumb_DragDelta(object sender, DragDeltaEventArgs e)
{
  Thumb thumb = sender as Thumb;
  var fe = thumb.Tag as FrameworkElement;
 
  // set the Width / Height of the element
  if (fe.Width == 0.0 || double.IsNaN(fe.Width))
  {
    fe.Width = fe.ActualWidth;
  }
  if (fe.Height == 0.0 || double.IsNaN(fe.Height))
  {
    fe.Height = fe.ActualHeight;
  }
 
  // apply the drag deltas
  double newWidth = fe.Width + e.HorizontalChange;
  fe.Width = Math.Max(0, newWidth);
 
  double newHeight = fe.Height + e.VerticalChange;
  fe.Height = Math.Max(0, newHeight);
}

And that’s it! Nice and simple.

You can download the sourcecode here: Resizable.zip

Regards,
Colin E.

CodeProject Article on WP7 Jump List

March 10th, 2011

I have just published a new CodeProject article on developing a Windows Phone 7 jump list control.

Enjoy!

Regards, Colin E.