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 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.
<controls:PivotItem Header="BBC News">
<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):
private static void OnIsPivotAnimatedChanged(DependencyObject d,
DependencyPropertyChangedEventArgs args)
{
ItemsControl list = d as ItemsControl;
list.Loaded += (s2, e2) =>
{
Pivot pivot = list.Ancestors<Pivot>().Single() as Pivot;
int pivotIndex = pivot.Items.IndexOf(list.Ancestors<PivotItem>().Single());
bool selectionChanged = false;
pivot.SelectionChanged += (s3, e3) =>
{
selectionChanged = true;
};
pivot.ManipulationCompleted += (s, e) =>
{
if (!selectionChanged)
return;
selectionChanged = false;
if (pivotIndex != pivot.SelectedIndex)
return;
bool fromRight = e.TotalManipulation.Translation.X <= 0;
VirtualizingStackPanel vsp =
list.Descendants<VirtualizingStackPanel>().First()
as VirtualizingStackPanel;
int firstVisibleItem = (int)vsp.VerticalOffset;
int visibleItemCount = (int)vsp.ViewportHeight;
for (int index = firstVisibleItem;
index <= firstVisibleItem + visibleItemCount; index++)
{
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)
{
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 PivotItem
s, 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 PivotItem
s.
The code which creates the required animation is given below, it simply adds a TranslateTransform
to the element and creates the required animation / storyboard.
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 storyboard
s on the composition thread in order to improve performance.
You can download the full source code here.