So far in the “Metro In Motion” series, I have covered fluid list animations, ‘peel’ animations, and flying title. In this blog post, I am looking at the 'tilt’ effect seen in native Windows Phone 7 applications where list items, tiles, check-boxes, and other user interface elements tilt slightly when the user presses them. This is a subtle 3D effect that makes the phone interface more tactile and playful.
I was in two minds about whether to implement my own tilt effect since there is an implementation of this effect available via the Silverlight (for Windows Phone 7) Toolkit. However, I am not that keen on the toolkit implementation of this feature. Firstly, the effect is applied by specifying the types of element that should tilt; unfortunately, this fails in various scenarios; secondly, the tilt is a bit over-the-top. Personally, I think this effect works best when it is at its most subtle.
This video shows the tilt effect in action, together with the other Metro In Motion effects: YouTube.
The implementation of this effect is actually rather simple, relying heavily on the PlaneProjection
transform that makes 3D transformations accessible to mere mortals like myself (for more powerful and flexible 3D effects, I would thoroughly recommend René Schule’s Matrix3DEx library). The effect is added to any element by applying an attached property:
<Button local:MetroInMotion.Tilt="6"/>
The value of the Tilt
property defines how pronounced the effect is. When the Tilt
property is attached, event handlers are added to the UI element:
private static double TiltAngleFactor = 4;
private static double ScaleFactor = 100;
private static void OnTiltChanged(DependencyObject d,
DependencyPropertyChangedEventArgs args)
{
FrameworkElement targetElement = d as FrameworkElement;
double tiltFactor = GetTilt(d);
var projection = new PlaneProjection();
var scale = new ScaleTransform();
var translate = new TranslateTransform();
var transGroup = new TransformGroup();
transGroup.Children.Add(scale);
transGroup.Children.Add(translate);
targetElement.Projection = projection;
targetElement.RenderTransform = transGroup;
targetElement.RenderTransformOrigin = new Point(0.5, 0.5);
targetElement.MouseLeftButtonDown += (s, e) =>
{
var clickPosition = e.GetPosition(targetElement);
double maxDimension = Math.Max(targetElement.ActualWidth,
targetElement.ActualHeight);
double distanceFromCenterX = targetElement.ActualWidth / 2 - clickPosition.X;
double normalisedDistanceX = 2 * distanceFromCenterX / maxDimension;
projection.RotationY = normalisedDistanceX * TiltAngleFactor * tiltFactor;
double distanceFromCenterY = targetElement.ActualHeight / 2 - clickPosition.Y;
double normalisedDistanceY = 2 * distanceFromCenterY / maxDimension;
projection.RotationX = -normalisedDistanceY * TiltAngleFactor * tiltFactor;
double distanceToCentre = Math.Sqrt(normalisedDistanceX * normalisedDistanceX
+ normalisedDistanceY * normalisedDistanceY);
double scaleVal = tiltFactor * (1 - distanceToCentre) / ScaleFactor;
scale.ScaleX = 1 - scaleVal;
scale.ScaleY = 1 - scaleVal;
};
targetElement.ManipulationCompleted += (s, e) =>
{
var sb = new Storyboard();
sb.Children.Add(CreateAnimation(null, 0, 0.1, "RotationY", projection));
sb.Children.Add(CreateAnimation(null, 0, 0.1, "RotationX", projection));
sb.Children.Add(CreateAnimation(null, 1, 0.1, "ScaleX", scale));
sb.Children.Add(CreateAnimation(null, 1, 0.1, "ScaleY", scale));
sb.Begin();
};
}
When the element is clicked, the distance of the click location to the centre is computed. A PlaneProjection
is used to tilt the element based on the click location, with a click at the top causing it to tilt around the X axis, and a click on the side tilting around the Y axis. A ScaleTransform
is used to make the element shrink slightly, giving a user the feeling that the element has been pushed into the phone slightly.
When the manipulation is complete, the properties of these various transforms are animated back to their original values using CreateAnimation
which is a simple utility method for creating DoubleAnimation
instances:
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;
}
The magnitude of the ScaleTransform
and PlaneProjection
depends on the click location, with clicks to the centre of the element resulting in a scaling, but no rotation, and clicks at the edge causing rotation but no scaling. The net result can be observed below where the effect of clicking at various locations of an element is shown:
You can also see the effect of clicking a square element below:
Finally, native phone applications have another subtle effect where elements that are tilted at the bottom of the screen appear to be viewed from above, whilst ones at the top are viewed from below. This can be achieved by applying a local offset to the PlaneProjection
and an equal and opposite vertical TranslateTransform
. The following code is added …
targetElement.MouseLeftButtonDown += (s, e) =>
{
var rootElement = Application.Current.RootVisual as FrameworkElement;
var relativeToCentre = (targetElement.GetRelativePosition(rootElement).Y -
rootElement.ActualHeight / 2) / 2;
translate.Y = -relativeToCentre;
projection.LocalOffsetY = +relativeToCentre;
};
targetElement.ManipulationCompleted += (s, e) =>
{
var sb = new Storyboard();
sb.Children.Add(CreateAnimation(null, 0, 0.1, "RotationY", projection));
sb.Children.Add(CreateAnimation(null, 0, 0.1, "RotationX", projection));
sb.Children.Add(CreateAnimation(null, 1, 0.1, "ScaleX", scale));
sb.Children.Add(CreateAnimation(null, 1, 0.1, "ScaleY", scale));
sb.Begin();
translate.Y = 0;
projection.LocalOffsetY = 0;
};
Which results in the following effect:
With that, the effect is complete!
Please note, the images in this blog post use a Tilt
‘factor’ of 6 in order to make the effect easier to see in these static images. I would urge you to use a much lower factor, perhaps 2, to make this effect much more subtle. I think it works best if the user almost doesn’t notice the effect, rather they ‘feel’ it.
You can download the full source code for this blog post here.