This blog post describes the addition of a two-finger rotation and three-finger pitch gesture to the Windows Phone 8 Map control.
You can see these gesture in action below:
The WP8 release replaced the image-tile based Bing maps with a fully vector-rendered map from Nokia. Being vector-based, this map can be panned, zoomed, rotated and rendered at an angle (i.e., pitched). However, much of this new functionality is not offered to the end user!
The WP8 supports the same gestures that the Bing WP7 map did, i.e., a single-fingered pan gesture and two-fingered pinch to zoom. What about rotation and pitch? Why not allow the user to modify these via gestures?
The key here is to add some new gestures that complement the existing one. I opted for the following:
- Two-finger rotate – When the user places two fingers on the map, this is currently used to zoom the display via a ‘spreading’ motion. However, if the user instead rotates the two touch points around the centre, the map should be rotated.
- Three-finger pitch – When the user places three fingers on the map, if they drag up or down, the map should adjust its pitch accordingly.
These gestures are enabled simply by creating them with a reference to the map:
new MapRotationGesture(map);
new MapPitchGesture(map);
If you don’t care how this all works, just head over to github and grab the code. If you want to find out more, read on …
Suppressing the Existing Gestures
In order to add these new gestures to the map, there needs to be a mechanism in place to suppress the existing gestures so that they do not interfere.
The technique I used is similar to a technique I demonstrated previously for suppressing pinch and scroll in the Windows Phone Browser control. Both the Map and WebBrowser
controls have a visual tree containing a number of user interface elements. The inner structure of the Map
is shown below:
Microsoft.Phone.Maps.Controls.Map
System.Windows.Controls.Border
System.Windows.Controls.Border
Microsoft.Phone.Maps.Controls.MapPresentationContainer
MS.Internal.ExternalInputContainer
System.Windows.Controls.Grid
MS.Internal.TileHostV2
Microsoft.Phone.Maps.Controls.RootMapLayer
The technique for suppressing interactions is quite simple, just add a ManipulationDelta
event handler to each one of these elements, setting the event to handled. The complete code is shown below:
public class MapGestureBase
{
public bool SuppressMapGestures { get; set; }
protected Map Map { get; private set; }
public MapGestureBase(Map map)
{
Map = map;
map.Loaded += (s,e) => CrawlTree(Map);
}
private void CrawlTree(FrameworkElement el)
{
el.ManipulationDelta += MapElement_ManipulationDelta;
for (int c = 0; c < VisualTreeHelper.GetChildrenCount(el); c++)
{
CrawlTree(VisualTreeHelper.GetChild(el, c) as FrameworkElement);
}
}
private void MapElement_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
if (SuppressMapGestures)
e.Handled = true;
}
}
If you create an instance of this class and associate it with a map, you can turn gestures on and off via the SuppressMapGestures
property:
var gestureBase = new MapGestureBase(map);
gestureBase.SuppressMapGestures = true;
Note: Unfortunately, this doesn’t solve the ‘map in a pivot’ or ‘map in a panorama’ problem that many Windows Phone developers have struggled with – the gestures that are handled are not propagated to a parent control.
A Rotation Gesture
The user can rotate the map by placing two fingers on the screen, then rotating them around their central point. Because two fingers are also used for the pinch-to-zoom gesture, a suitable threshold needs to be introduced. I have found that disabling rotation until the user has rotated by 10 degrees feels about right.
The code that implements the rotation is really quite simple, the MapGestureBase
subclass is shown in its entirety below:
public class MapRotationGesture : MapGestureBase
{
public double MinimumRotation { get; set; }
private double? _previousAngle;
private bool _isRotating;
public MapRotationGesture(Map map)
: base(map)
{
MinimumRotation = 10.0;
Touch.FrameReported += Touch_FrameReported;
}
private void Touch_FrameReported(object sender, TouchFrameEventArgs e)
{
var touchPoints = e.GetTouchPoints(Map);
if (touchPoints.Count == 2)
{
if (!_previousAngle.HasValue)
{
_previousAngle = AngleBetweenPoints(touchPoints[0], touchPoints[1]);
}
if (!_isRotating)
{
double angle = AngleBetweenPoints(touchPoints[0], touchPoints[1]);
double delta = angle - _previousAngle.Value;
if (Math.Abs(delta) > MinimumRotation)
{
_isRotating = true;
SuppressMapGestures = true;
}
}
if (_isRotating)
{
double angle = AngleBetweenPoints(touchPoints[0], touchPoints[1]);
double delta = angle - _previousAngle.Value;
Map.Heading -= delta;
_previousAngle = angle;
}
}
else
{
_previousAngle = null;
_isRotating = false;
SuppressMapGestures = false;
}
}
private double AngleBetweenPoints(TouchPoint p1, TouchPoint p2)
{
return Math.Atan2(p1.Position.Y - p2.Position.Y, p1.Position.X - p2.Position.X)
*(180 / Math.PI);
}
}
Touch gestures are detected via the Touch.FrameReported
event. When two fingers are placed on the screen, the initial rotation angle is recorded. When the minimum rotation is exceeded, the Map.Heading
is updated with each ‘delta’ reported. Really simple code, but a fantastic feature for the user!
A Pitch Gesture
You get a real feel for the vector-nature of the maps when you set the ‘pitch’, a style of rendering that is often used on satnavs.
I initially considered using a two finger pull-down gesture, which is similar to the one that Google Maps on Android uses, but found it very hard to coordinate the three gestures, zoom, rotate, pitch, that all use the same two-fingers! So instead, I opted for a three-finger pull-down gesture to increase the pitch of the map.
The code follows a very similar pattern to the rotate gesture:
public class MapPitchGesture : MapGestureBase
{
public double Sensitivity { get; set; }
private double? _initialPitchYLocation;
public MapPitchGesture(Map map)
: base(map)
{
Sensitivity = 0.5;
Touch.FrameReported += Touch_FrameReported;
}
private void Touch_FrameReported(object sender, TouchFrameEventArgs e)
{
var touchPoints = e.GetTouchPoints(Map);
SuppressMapGestures = touchPoints.Count == 3;
if (touchPoints.Count == 3)
{
if (!_initialPitchYLocation.HasValue)
{
_initialPitchYLocation = touchPoints[0].Position.Y;
}
double delta = touchPoints[0].Position.Y - _initialPitchYLocation.Value;
double newPitch = Math.Max(0, Math.Min(75, (Map.Pitch + delta * Sensitivity)));
Map.Pitch = newPitch;
_initialPitchYLocation = touchPoints[0].Position.Y;
}
else
{
_initialPitchYLocation = null;
}
}
}
As soon as three fingers are placed on the screen, the gesture becomes active. The movement of the first finger is used to determine the delta to apply to the Pitch
property. Again, nice and simple!
The source code for these gesture, plus a demo app is available via github. Please let me know if you use this code in any of your apps.
Regards,
Colin E.