Click here to Skip to main content
15,886,518 members
Articles / Mobile Apps / Windows Phone 7

Metro in Motion Part #1 – Fluid List Animation

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
17 May 2011CPOL3 min read 24.9K   13   2
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.

Image 1

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 an item 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: YouTube video.

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.

XML
<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):

C#
// 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 and 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.

C#
/// <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 source code here.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Scott Logic
United Kingdom United Kingdom
I am CTO at ShinobiControls, a team of iOS developers who are carefully crafting iOS charts, grids and controls for making your applications awesome.

I am a Technical Architect for Visiblox which have developed the world's fastest WPF / Silverlight and WP7 charts.

I am also a Technical Evangelist at Scott Logic, a provider of bespoke financial software and consultancy for the retail and investment banking, stockbroking, asset management and hedge fund communities.

Visit my blog - Colin Eberhardt's Adventures in .NET.

Follow me on Twitter - @ColinEberhardt

-

Comments and Discussions

 
QuestionGreat starting point Pin
Diamonddrake24-Aug-11 1:24
Diamonddrake24-Aug-11 1:24 
AnswerRe: Great starting point Pin
Colin Eberhardt24-Aug-11 2:07
Colin Eberhardt24-Aug-11 2:07 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.