This is the third in my “Metro in Motion” series where I am looking at how to re-create some of the stylish transitions and animations found in native Windows Phone 7 applications. As Silverlight developers, we have controls that adhere to the static Metro styling, but apart from Pivot and Panorama, the more dynamic features of the Metro style are something we have to come up with ourselves. So far, the previous posts have provided re-useable implementations for the fluid list animations between pivot pages and the ‘peel’ animation seen when applications exit. In this blog post, I provide a simple re-useable implementation of the title fly-out and fly-in effect. This effect is not that easy to describe, so we’ll let the following picture do the talking …
Here is a video of this effect recorded on the emulator. This code has also been tested on a real device, and performs just fine: YouTube.
Unfortunately, because the animations are quite snappy, a YouTube video doesn’t do them justice. Best viewed on a real phone!
Let’s look at how this effect is implemented …
Starting with the fly-out effect, the general principle is that when the user selects an item in the list, we handle some event in the code-behind. We must identify the on-screen element that flies off screen and animate its location using a storyboard
, whilst fading everything else from view. When this animation has completed, we navigate to the next page. When the user returns to the same page, this element must be animated to return to its original location.
In order to make the code re-useable, I have created a class which you present with the element to animate, it takes care of the fly-out effect, and then the fly-in which returns the UI to the original state.
Let’s take a look at the code …
Using this Effect
Firstly, we need to identify the elements which we wish to animate, so here we give them a name:
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Title}"
x:Name="Title"
Style="{StaticResource PhoneTextLargeStyle}"/>
<TextBlock Text="{Binding Summary}"
Style="{StaticResource PhoneTextSmallStyle}"
local:MetroInMotion.AnimationLevel="1"/>
<TextBlock Text="{Binding Date}"
Style="{StaticResource PhoneTextSmallStyle}"
local:MetroInMotion.AnimationLevel="2"/>
</StackPanel>
The page needs an instance of the class which manages this animation effect. When the ListBox
raises its SelectionChanged
event, we use a bit of LINQ-to-VisualTree to locate the element named above, then invoke ItemFlyOut
, providing an ‘action’ which is called when the animation completes.
private ItemFlyInAndOutAnimations _flyOutAnimation = new ItemFlyInAndOutAnimations();
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ListBox list = sender as ListBox;
var selectedContainer =
list.ItemContainerGenerator.ContainerFromItem(list.SelectedItem);
var exitAnimationElement = selectedContainer.Descendants()
.OfType<FrameworkElement>()
.SingleOrDefault(el => el.Name == "Title");
if (exitAnimationElement != null)
{
_flyOutAnimation.ItemFlyOut(exitAnimationElement, () =>
{
FrameworkElement root = Application.Current.RootVisual as FrameworkElement;
root.DataContext = list.SelectedItem;
NavigationService.Navigate(new Uri("/DetailsPage.xaml", UriKind.Relative));
});
}
}
When the user navigates back to the page, you simple invoke ItemFlyIn
, and this class takes care of animating the title element back into view:
private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
Dispatcher.BeginInvoke(() => _flyOutAnimation.ItemFlyIn() );
}
This class also has a static
method which can be used to animate the page title into view. With the page title named in XAML, it is as easy to use as this …
public partial class DetailsPage : PhoneApplicationPage
{
public DetailsPage()
{
InitializeComponent();
ItemFlyInAndOutAnimations.TitleFlyIn(PageTitle);
}
}
So, this aptly named ItemFlyInAndOutAnimations
does a good job of making this effect re-useable. Let’s look under the covers to see how it works …
How It Works
Probably the trickiest part of this effect for me was working out how to fade the page, via its opacity, without fading out the element being animated. The opacity property is inherited from an element to its children, therefore, if you set the opacity of the page, every element within it will have their opacity set also. To avoid this, the solution I came up with ‘clones’ the element that is being animated and places it in a popup; the popup also contains a full-screen rectangle which is used to fade-out the background.
The class which performs the animation constructs a Popup
and a Canvas
which is the popup’s child element:
public class ItemFlyInAndOutAnimations
{
private Popup _popup;
private Canvas _popupCanvas;
private FrameworkElement _targetElement;
private Point _targetElementPosition;
private Image _targetElementClone;
private Rectangle _backgroundMask;
private static TimeSpan _flyInSpeed = TimeSpan.FromMilliseconds(200);
private static TimeSpan _flyOutSpeed = TimeSpan.FromMilliseconds(300);
public ItemFlyInAndOutAnimations()
{
_popup = new Popup();
_popupCanvas = new Canvas();
_popup.Child = _popupCanvas;
}
...
}
The fly-out animation creates a full-screen rectangle which masks everything on the screen, it then clones the visuals of the element being animated using a WriteableBitmap
and places this in the popup also. A number of DoubleAnimation
s are constructed, one which animates the X location, one for the Y, and one which animates the rectangle opacity to gradually fade-out the background. Note, the curved path that the animated element follows is achieved by using a EaseOut
on the Y animation, and an EaseIn
on the X:
public void ItemFlyOut(FrameworkElement element, Action action)
{
_targetElement = element;
var rootElement = Application.Current.RootVisual as FrameworkElement;
_backgroundMask = new Rectangle()
{
Fill = new SolidColorBrush(Colors.Black),
Opacity = 0.0,
Width = rootElement.ActualWidth,
Height = rootElement.ActualHeight
};
_popupCanvas.Children.Add(_backgroundMask);
_targetElementClone = new Image()
{
Source = new WriteableBitmap(element, null)
};
_popupCanvas.Children.Add(_targetElementClone);
_targetElementPosition = element.GetRelativePosition(rootElement);
Canvas.SetTop(_targetElementClone, _targetElementPosition.Y);
Canvas.SetLeft(_targetElementClone, _targetElementPosition.X);
var sb = new Storyboard();
var db = CreateDoubleAnimation(_targetElementPosition.X,
_targetElementPosition.X + 500,
new SineEase() { EasingMode = EasingMode.EaseIn },
_targetElementClone, Canvas.LeftProperty, _flyOutSpeed);
sb.Children.Add(db);
db = CreateDoubleAnimation(_targetElementPosition.Y,
_targetElementPosition.Y + 50,
new SineEase() { EasingMode = EasingMode.EaseOut },
_targetElementClone, Canvas.TopProperty, _flyOutSpeed);
sb.Children.Add(db);
db = CreateDoubleAnimation(0, 1,
null, _backgroundMask, UIElement.OpacityProperty, _flyOutSpeed);
sb.Children.Add(db);
sb.Completed += (s, e2) =>
{
action();
element.Dispatcher.BeginInvoke(() =>
{
_popup.IsOpen = false;
});
};
element.Opacity = 0.0;
_popup.IsOpen = true;
sb.Begin();
}
private static DoubleAnimation CreateDoubleAnimation(double from, double to,
IEasingFunction easing, DependencyObject target,
object propertyPath, TimeSpan duration)
{
var db = new DoubleAnimation();
db.To = to;
db.From = from;
db.EasingFunction = easing;
db.Duration = duration;
Storyboard.SetTarget(db, target);
Storyboard.SetTargetProperty(db, new PropertyPath(propertyPath));
return db;
}
Animating the element back into view is a little easier, here we remove the background mask and animate our cloned element back to its original location. Once the animation has completed, we hide the popup and destroy our temporary visuals, and finally set the opacity of the original element back to '1.0
', returning the UI to its original state:
public void ItemFlyIn()
{
if (_popupCanvas.Children.Count != 2)
return;
_popup.IsOpen = true;
_backgroundMask.Opacity = 0.0;
Image animatedImage = _popupCanvas.Children[1] as Image;
var sb = new Storyboard();
var db = CreateDoubleAnimation(_targetElementPosition.X - 100,
_targetElementPosition.X, new SineEase(),
_targetElementClone, Canvas.LeftProperty, _flyInSpeed);
sb.Children.Add(db);
db = CreateDoubleAnimation(_targetElementPosition.Y - 50,
_targetElementPosition.Y, new SineEase(),
_targetElementClone, Canvas.TopProperty, _flyInSpeed);
sb.Children.Add(db);
sb.Completed += (s, e) =>
{
_popup.IsOpen = false;
_targetElement.Opacity = 1.0;
_popupCanvas.Children.Clear();
};
sb.Begin();
}
And there we have it, Metro style flying titles!
You can download the full source code here.