Metro In Motion Part #1 - Fluid List Animation

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.

blog comments powered by Disqus