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 this video. You might have to watch it a few times, the animation comes right at the end and is over pretty quickly! Watch video here.
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 FrameworkElement
s. 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!):
public static void Peel(this IEnumerable<FrameworkElement> elements, Action endAction)
{
var elementList = elements.ToList();
var lastElement = elementList.Last();
double delay = 0;
foreach (FrameworkElement element in elementList)
{
var sb = GetPeelAnimation(element, delay);
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:
private static Storyboard GetPeelAnimation(FrameworkElement element, double delay)
{
Storyboard sb;
var projection = new PlaneProjection()
{
CenterOfRotationX = -0.1
};
element.Projection = projection;
var width = element.ActualWidth;
var targetAngle = Math.Atan(1000 / (width / 2));
targetAngle = targetAngle * 180 / Math.PI;
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
:
public static IEnumerable<FrameworkElement>
GetItemsInView(this ItemsControl itemsControl)
{
VirtualizingStackPanel vsp =
itemsControl.Descendants<VirtualizingStackPanel>()
.First() as VirtualizingStackPanel;
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;
var listInView = ((PivotItem)pivot.SelectedItem).
Descendants().OfType<ItemsControl>().Single();
var listItems = listInView.GetItemsInView().ToList();
var header = this.Descendants().OfType<FrameworkElement>()
.Single(d => d.Name == "HeadersListElement");
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 programmatically 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 source code here.